injectFormField<T> method Null safety

InjectedFormField<T> injectFormField<T>(
  1. T initialValue,
  2. {List<String? Function(T value)>? validators,
  3. bool? validateOnValueChange,
  4. bool? validateOnLoseFocus,
  5. void onValueChange(
    1. InjectedFormField formField
    )?,
  6. bool autoDispose = true,
  7. bool isReadOnly = false,
  8. bool isEnabled = true}
)

Inject form inputs other than for text editing

This injected state abstracts the best practices to come out with a simple, clean, and testable approach deal with form inputs and form validation.

See also :

Example:

In most cases, any input form widget have a value and onChanged properties. You must set these properties to the exposed value and onChanged of OnFormFieldBuilder. This is an example of CheckBox input field

  final myCheckBox = RM.injectFormField<bool>(false);

  //In the widget tree
  OnFormFieldBuilder<bool>(
   listenTo: myCheckBox,
   builder: (value, onChanged) {
     return CheckboxListTile(
       value: value,
       onChanged: onChanged,
       title: Text('I accept the licence'),
     );
   },
  ),

See bellow for more examples.

Parameters:

text: Required initial value of the generic type.

The initial value the linked OnFormFieldBuilder should expose.

validators: Optional List of callbacks.

Set of validation rules the field should pass.

Validators expose the value that the user entered.

If any of the validation callbacks return a non-empty strings, the filed is considered non valid. For the field to be valid all validators must return null.

example:

     final _email = RM.injectedFormField(
      false,
      validators: [
        (value) {
           //Frontend validation
           if(!value){
              return 'You must accept the license'.
            }
         },
      ]
    );

The validations performed here are frontend validation. To do backend validation you must use InjectedForm.

validateOnValueChange: Optional bool.

Whether to validate the input when the user change its value.

The default value depends on whether the linked OnFormFieldBuilder is inside or OnFormBuilder:

If validateOnValueChange is set to false, the text is not validated on input change. The text can be validate manually by invoking InjectedFormField.validate.

validateOnLoseFocus: Optional bool.

Whether to validate the input just after the field loses focus.

It defaults to true if the linked OnFormFieldBuilder is inside OnFormBuilder and defaults to false if it is outside.

Once the OnFormFieldBuilder loses focus and if it fails to validate, the field will auto validate on typing the next time the user starts typing.

isReadOnly: Optional bool. Defaults to false.

If true the input is clickable and selectable but not editable. Later on, you can set it using InjectedFormField.isReadOnly

See OnFormBuilder.isReadOnlyRM to set a group of input fields to read only.

isEnabled: Optional bool. Defaults to true.

If false the OnFormFieldBuilder is disabled. Later on, you can set it using InjectedFormField.isEnabled.

See OnFormBuilder.isEnabledRM to set a group of input fields to read only.

onValueChange: Optional callback.

Callback for side effects. It is fired whenever the input is changed

autoDisposeWhenNotUsed: Optional bool (Default true)

Whether to auto dispose the injected model when no longer used (listened to).

It is important to note that:

  • A state never listened to for rebuild, never auto dispose even after it is mutated.
  • By default, all states consumed in the widget tree will auto dispose.
  • It is recommended to manually dispose state that are not auto disposed using InjectedBaseState.dispose. You can dispose all states of the app using RM.disposeAll.
  • A state will auto dispose if all states it depends on are disposed of.
  • Non disposed state may lead to unexpected behavior.
  • To debug when state is initialized and disposed of use debugPrintWhenNotifiedPreMessage parameter (See below)

Examples

Checkbox

    final myCheckBox = RM.injectFormField<bool>(false);

    //In the widget tree
    OnFormFieldBuilder<bool>(
     listenTo: myCheckBox,
     builder: (value, onChanged) {
       return CheckboxListTile(
         value: value,
         onChanged: onChanged,
         title: Text('I accept the licence'),
       );
     },
    ),

Switch

      final switcher = RM.injectFormField(false);

      OnFormFieldBuilder<bool>(
        listenTo: switcher,
        inputDecoration: InputDecoration(
          labelText: 'switcher label',
          hintText: 'switcher hint',
          helperText: 'switcher helper text',
          suffixIcon: dropdownMenu.hasError
              ? const Icon(Icons.error, color: Colors.red)
              : const Icon(Icons.check, color: Colors.green),
        ),
        builder: (val, onChanged) {
          return SwitchListTile(
            value: val,
            onChanged: onChanged,
            title: Text('I Accept the terms and conditions'),
          );
        },
      ),
    ),

Date picker

    final dateTime = RM.injectFormField<DateTime?>(
      null,
      validators: [
        (date) {
          if (date == null || date.isAfter(DateTime.now())) {
            return 'Not allowed';
          }
        }
      ],
      validateOnLoseFocus: true,
    );


    OnFormFieldBuilder(
      listenTo: dateTime,
      inputDecoration: InputDecoration(
        labelText: 'DatePicker label',
        hintText: 'DatePicker hint',
        helperText: 'DatePicker helper text',
      ),
      builder: (value, onChanged) => ListTile(
        dense: true,
        title: Text('${value ?? ''}'),
        //clear the state
        trailing: IconButton(
          icon: Icon(Icons.clear),
          onPressed: () => dateTime.value = null,
        ),
        onTap: () async {
          final result = await showDatePicker(
            context: context,
            initialDate: dateTime.value ?? DateTime.now(),
            firstDate: DateTime(2000, 1, 1),
            lastDate: DateTime(2040, 1, 1),
          );
          if (result != null) {
            dateTime.value = result;
          }
        },
      ),
    ),

Date range picker

  final dateTimeRange = RM.injectFormField<DateTimeRange?>(null);

  OnFormFieldBuilder<DateTimeRange?>(
    listenTo: dateTimeRange,
    inputDecoration: InputDecoration(
      labelText: 'DateRangePicker label',
      hintText: 'DateRangePicker hint',
      helperText: 'DateRangePicker helper text',
    ),
    builder: (value, onChanged) {
      return ListTile(
        dense: true,
        title: Text('${value ?? ''}'),
        trailing: IconButton(
          icon: Icon(Icons.close),
          onPressed: () {
            dateTimeRange.value = null;
          },
        ),
        onTap: () async {
          final result = await showDateRangePicker(
            context: context,
            firstDate: DateTime(2000, 1, 1),
            lastDate: DateTime(2040, 1, 1),
          );
          if (result != null) {
            dateTimeRange.value = result;
          }
        },
      );
    },
  ),

Slider

  final slider = RM.injectFormField<double>(
      6.0,
      validators: [
        (value) {
          if (value < 6.0) {
            return 'Not allowed';
          }
        }
      ],
    );

  OnFormFieldBuilder<double>(
    listenTo: slider,
    autofocus: true,
    inputDecoration: InputDecoration(
      labelText: 'Slider label',
      hintText: 'Slider hint',
      helperText: 'Slider helper text: ${slider.value}',
    ),
    builder: (value, onChanged) {
      return Slider(
        value: value,
        onChanged: onChanged,
        min: 0.0,
        max: 10.0,
      );
    },
  ),

RangeSlider

    OnFormFieldBuilder<RangeValues>(
      listenTo: rangeSlider,
      inputDecoration: InputDecoration(
        labelText: 'Slider label',
        hintText: 'Slider hint',
        helperText: 'Slider helper text',
      ),
      builder: (value, onChanged) {
        return RangeSlider(
          values: value,
          onChanged: onChanged,
          min: 0.0,
          max: 100.0,
          divisions: 20,
        );
      },
    ),

DropdownButton

    const genders = ['Male', 'Female', 'Other'];
    final dropdownMenu = RM.injectFormField<String?>(null);
    OnFormFieldBuilder<String?>(
      listenTo: dropdownMenu,
      inputDecoration: InputDecoration(
        labelText: 'DropDownMenu label',
        hintText: 'DropDownMenu hint',
        helperText: 'DropDownMenu helper text',
        suffixIcon: dropdownMenu.hasError
            ? const Icon(Icons.error, color: Colors.red)
            : const Icon(Icons.check, color: Colors.green),
      ),
      builder: (val, onChanged) {
        return DropdownButtonHideUnderline(
          child: DropdownButton<String>(
            value: val,
            items: genders
                .map(
                  (gender) => DropdownMenuItem(
                    value: gender,
                    child: Text(gender),
                  ),
                )
                .toList(),
            onChanged: onChanged,
          ),
        );
      },
    ),

Radio Options

    final radioOptions = ['Dart', 'Kotlin', 'Java', 'Swift', 'Objective-C'];
    final radioButtons = RM.injectFormField<String>('');
    OnFormFieldBuilder<String>(
      listenTo: radioButtons,
      inputDecoration: InputDecoration(
        labelText: 'Radio buttons label',
        hintText: 'Radio buttons hint',
        helperText: 'Radio buttons helper text',
        suffixIcon: radioButtons.hasError
            ? const Icon(Icons.error, color: Colors.red)
            : const Icon(Icons.check, color: Colors.green),
      ),
      builder: (val, onChanged) {
        return Row(
          children: radioOptions
              .map(
                (e) => InkWell(
                  onTap: () => radioButtons.onChanged(e),
                  child: Row(
                    children: [
                      Radio<String>(
                        value: e,
                        groupValue: val,
                        onChanged: onChanged,
                      ),
                      Text(e),
                      const SizedBox(width: 8),
                    ],
                  ),
                ),
              )
              .toList(),
        );
      },
    ),

Multi Check Boxes

      final multiCheckBoxes = RM.injectFormField<List<String>>(
        [],
        validators: [
          (val) {
            if (val.length < 3) {
              return 'choose more than three items';
            }
          }
        ],
      );


    OnFormFieldBuilder<List<String>>(
      listenTo: multiCheckBoxes,
      inputDecoration: InputDecoration(
        labelText: 'multiCheckBoxes label',
        hintText: 'multiCheckBoxes hint',
        helperText: 'multiCheckBoxes helper text',
      ),
      builder: (val, onChanged) {
        return Row(
          children: radioOptions
              .map(
                (e) => Row(
                    children: [
                      Checkbox(
                        value: val.contains(e),
                        onChanged: (checked) {
                          if (checked!) {
                            multiCheckBoxes.value = [...val, e];
                          } else {
                            multiCheckBoxes.value =
                                val.where((el) => e != el).toList();
                          }
                        },
                      ),
                      Text(e),
                      const SizedBox(width: 8),
                    ],
                  ),
              )
              .toList(),
        );
      },
    ),

Implementation

static InjectedFormField<T> injectFormField<T>(
  T initialValue, {
  List<String? Function(T value)>? validators,
  bool? validateOnValueChange,
  bool? validateOnLoseFocus,
  void Function(InjectedFormField formField)? onValueChange,
  bool autoDispose = true,
  bool isReadOnly = false,
  bool isEnabled = true,
}) {
  return InjectedFormFieldImp<T>(
    initialValue,
    validator: validators,
    validateOnValueChange: validateOnValueChange,
    validateOnLoseFocus: validateOnLoseFocus,
    autoDispose: autoDispose,
    onValueChange: onValueChange,
    isReadOnly: isReadOnly,
    isEnabled: isEnabled,
  );
}