Using an Async Iterator in Typescript
I have been experimenting with async iterators in Typescript. One area these could be useful is in simplifying the code for forms, specifically handling button presses.
Currently, I create forms programmatically using my form class, which creates Bootstrap modal forms. "Action buttons" are the buttons that sit in the modal-footer area. When creating "ActionButton"s to add to the form an onClick handler is specified and the form takes care of calling onClick() when the button is clicked.
Here's a trivial example - an About box with an Ok button and second button just for testing.
export function about(): void
{
let aboutBox = new forms.Form( `About ${app.productName}` );
aboutBox.addButton(
{
name: 'Ok',
onClick: ()=>
{
aboutBox.close();
}
} );
aboutBox.addButton(
{
name: 'Test',
tooltip: 'Test some stuff',
onClick: ()=>
{
// do some stuff
}
});
aboutBox.show();
}
Note that onClick() is actually called with the following parameters - form, button, and event - which I've just ignored for now.
Ok, here's the new version - which uses the form's show2() function, which not only shows the form but returns an async iterator. This iterator returns a tuple for each button press consisting of the button name, a map of the values of each field in the form (obviously *real* forms usually have input fields which have values - currently supported are TextInput, TextArea, RadioInput, CheckBox, Tree, Select, and Button), and the event.
export async function about( e: UnionEvent ): Promise
{
let aboutBox = new forms.Form( `About ${app.productName}` );
aboutBox.addButton({ name: 'Ok' } );
aboutBox.addButton({ name: 'Test', tooltip: 'Test some stuff' });
let buttonIterator = aboutBox.show2();
// process button clicks
for await ( const [ button, fieldValues, e ] of buttonIterator() )
{
switch( button )
{
case 'Ok':
aboutBox.close();
break;
case 'Test':
// do some stuff
break;
}
}
}
Hopefully that makes sense?
Here's the show2 function in the form class.
class Form
{
//[snip ... most of the class removed ]
show2(): ()=> AsyncIterableIterator<[ string, ValueMap, JQueryEventObject ]>
{
let _resolve:( val: [ string, ValueMap, JQueryEventObject ] )=>void = null;
// when form.close() is called, resolved is set to true
this.resolved = false;
// the form's onClose() is called whenever the form is hidden
this.onClose = ()=>
{
if ( !this.resolved && _resolve )
{
// send the Dismiss text as the resolution
_resolve([ this.dismiss, this.getInputMap(), null ]);
}
};
// each button gets the same handler - this one
let onClick = ( f: Form, b: ActionButton, e: JQueryEventObject ) =>
{
if ( _resolve )
{
_resolve([ b.name, this.getInputMap(), e ]);
}
};
for( const button of this.buttons )
{
// exclude the help button, all other buttons get the specified click handler
if ( button.name !== '?' )
{
button.onClick = onClick;
}
}
// show the form
this.show();
return async function* (): AsyncIterableIterator<[ string, ValueMap, JQueryEventObject ]>
{
while( true )
{
yield new Promise( resolve =>
{
_resolve = ( ...args )=>{ resolve( ...args ); }
}) as Promise<[ string, ValueMap, JQueryEventObject ]>;
}
}
}
}
Currently, I create forms programmatically using my form class, which creates Bootstrap modal forms. "Action buttons" are the buttons that sit in the modal-footer area. When creating "ActionButton"s to add to the form an onClick handler is specified and the form takes care of calling onClick() when the button is clicked.
Here's a trivial example - an About box with an Ok button and second button just for testing.
export function about(): void
{
let aboutBox = new forms.Form( `About ${app.productName}` );
aboutBox.addButton(
{
name: 'Ok',
onClick: ()=>
{
aboutBox.close();
}
} );
aboutBox.addButton(
{
name: 'Test',
tooltip: 'Test some stuff',
onClick: ()=>
{
// do some stuff
}
});
aboutBox.show();
}
Note that onClick() is actually called with the following parameters - form, button, and event - which I've just ignored for now.
Ok, here's the new version - which uses the form's show2() function, which not only shows the form but returns an async iterator. This iterator returns a tuple for each button press consisting of the button name, a map of the values of each field in the form (obviously *real* forms usually have input fields which have values - currently supported are TextInput, TextArea, RadioInput, CheckBox, Tree, Select, and Button), and the event.
export async function about( e: UnionEvent ): Promise
{
let aboutBox = new forms.Form( `About ${app.productName}` );
aboutBox.addButton({ name: 'Ok' } );
aboutBox.addButton({ name: 'Test', tooltip: 'Test some stuff' });
let buttonIterator = aboutBox.show2();
// process button clicks
for await ( const [ button, fieldValues, e ] of buttonIterator() )
{
switch( button )
{
case 'Ok':
aboutBox.close();
break;
case 'Test':
// do some stuff
break;
}
}
}
Hopefully that makes sense?
Here's the show2 function in the form class.
class Form
{
//[snip ... most of the class removed ]
show2(): ()=> AsyncIterableIterator<[ string, ValueMap, JQueryEventObject ]>
{
let _resolve:( val: [ string, ValueMap, JQueryEventObject ] )=>void = null;
// when form.close() is called, resolved is set to true
this.resolved = false;
// the form's onClose() is called whenever the form is hidden
this.onClose = ()=>
{
if ( !this.resolved && _resolve )
{
// send the Dismiss text as the resolution
_resolve([ this.dismiss, this.getInputMap(), null ]);
}
};
// each button gets the same handler - this one
let onClick = ( f: Form, b: ActionButton, e: JQueryEventObject ) =>
{
if ( _resolve )
{
_resolve([ b.name, this.getInputMap(), e ]);
}
};
for( const button of this.buttons )
{
// exclude the help button, all other buttons get the specified click handler
if ( button.name !== '?' )
{
button.onClick = onClick;
}
}
// show the form
this.show();
return async function* (): AsyncIterableIterator<[ string, ValueMap, JQueryEventObject ]>
{
while( true )
{
yield new Promise( resolve =>
{
_resolve = ( ...args )=>{ resolve( ...args ); }
}) as Promise<[ string, ValueMap, JQueryEventObject ]>;
}
}
}
}
Comments