injectFormField<T> static method
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 :
- OnFormFieldBuilder to listen to the injected input.
- InjectedTextEditing to inject a TextEditingController,
- InjectedForm and OnFormBuilder to work with form.
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 outside: it default to true if
validateOnLoseFocus
is false. - If inside: it defaults to false if InjectedForm.autovalidateMode is AutovalidateMode.disabled, otherwise it defaults to true.
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,
bool? isEnabled,
}) {
return InjectedFormFieldImp<T>(
initialValue,
validator: validators,
validateOnValueChange: validateOnValueChange,
validateOnLoseFocus: validateOnLoseFocus,
autoDispose: autoDispose,
onValueChange: onValueChange,
isReadOnly: isReadOnly,
isEnabled: isEnabled,
);
}