0
votes

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?

1

1 Answers

0
votes

Seems I have discovered what the issue actually is.

The problem with the jsonToForm library that I modeled my code after puts the creation of the form in the Build function rather than in the InitState function. This causes the form to be rebuilt on each change (very bad) and subsequently if you add a TextController to a TextInputField, it goes crazy.

Anyone using the jsonToForm will need to update the code.

I also don't like the fact that it creates an array of widgets instead of using a future builder approach. .... I will slay that dragon on another day....