In my flutter application, I am displaying a dialog with a series of fields controlled by a JSON representation of the form. The function that controls the form widgets is outside of the dialog implementation (so that it is reusable in widgets other than dialogs).
Inspired by the jsonToForm module (https://github.com/VictorRancesCode/json_to_form) my code is as follows:
List<Widget> jsonToForm() {
List<Widget> widgetList = new List<Widget>();
for (var count = 0; count < formItems.length; count++) {
FormItem field = FormItem.fromJson( formItems[count] );
switch( field.type ) {
case FieldType.input:
case FieldType.password:
case FieldType.email:
case FieldType.numeric:
case FieldType.textarea:
widgetList.add( _buildLabel(field.title) );
widgetList.add( _buildTextField(field) );
break;
case FieldType.radiobutton:
widgetList.add( _buildLabel(field.title) );
for (var i = 0; i < field.fields.length; i++) {
widgetList.add( _buildRadioField( field, field.fields[i] ));
}
break;
case FieldType.color:
widgetList.add( _buildColorPicker(field) );
break;
case FieldType.toggle:
widgetList.add( _buildSwitch(field) );
break;
case FieldType.checkbox:
widgetList.add( _buildLabel(field.title) );
for (var i = 0; i < field.fields.length; i++) {
widgetList.add( _buildCheckbox(field, field.fields[i] ) );
}
break;
}
}
return widgetList;
}
When a form value changes, it calls _handleChanged and then passes the form field list to the parent through an event callback.
void _handleChanged( FormItem field, { FieldItem item } ) {
var fieldIndex = formItems.indexWhere( (i) => i['name'] == field.name );
if( fieldIndex != -1 ) {
// If specified, update the subitem
if( item != null ) {
var itemIndex = field.fields.indexWhere( (i) => i.title == item.title );
field.fields.replaceRange(itemIndex, itemIndex+1, [item]);
}
// Now update the state
this.setState(() {
formItems.replaceRange(fieldIndex, fieldIndex+1, [ field.toJson() ]);
});
// Notify parents
widget.onChanged(formItems);
}
}
The problem with this approach (especially for a text field), there is no onComplete event which is only fired after all text has been entered. onChanged as well as the TextEditingController approach fire as each character is typed. Yet, I don't want to have to put the dialog button in the jsonToForm routine because then it becomes no longer reusable in other (non-dialog) screens.
Also I am not fond of the form performing an onChanged callback returning until all fields when only some have been updated. By signaling on each change event, it also ends up re-building on each character that is typed (something I would also like to avoid).
What would be ideal, is to only perform a callback withing jsonToForm when all editing for all fields are complete, but without the button, there is nothing in the jsonToForm module which can signal "I'm done".
var response;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Settings"),
actions: <Widget>[
DialogButton(
child: new Text( 'Save', style: TextStyle(color: Colors.white,fontWeight: FontWeight.w600, fontSize: 16)),
color: Colors.blue,
width: 110,
onPressed: () {
var data = List<Property>();
if( response != null ) {
response.forEach((item) => data.add( Property( name: item['name'], value: item['value'],
type: AppUtils.enumFromString( PropertyType.values, item['type'], PropertyType.string ) )));
}
var lib = this.widget.item.libraryItem;
var logger = AppConfig.newLogger(lib.id, lib.valueType, properties: data);
Navigator.pop(context, logger);
})
],
),
body: SingleChildScrollView(
child: Container(
child: Column(children: <Widget>[
JsonForm(
form: json.encode(this.widget.item.formItems),
onChanged: (dynamic response) {
this.setState(() => this.response = response);
},
),
]),
),
),
);
}
Am I stuck with putting the scaffold/widget build in the jsonToForm and then replicating this widget for screens, sub-widgets, etc or is there a more elegant solution to split the form from the container?