Closed sokolej79 closed 3 years ago
@sokolej79 Hi Here is a basic example of creating custom widget https://github.com/joanpablo/reactive_forms/blob/master/lib/src/widgets/reactive_switch.dart
Could you write a demo code which clearly describes your idea and makes the experience less clumsy?
Little background first, For demo code I still thinking for right design approach. We use custom dynamic forms with the reactive_forms package. All data for form and fields are obtained from the server as json: FormOptions, FieldConfig, templateOptions.... Then we dynamicly build form and fields. How to add additional state properties to an existing ReactiveFormField widget? We need some Base form field that accepts models: fieldConfig, formOptions, templateOptions and everything else must work the same as ReactiveFormField does. This base field would be extended from each custom form field with different UI and settings (text, select...) and listened to control state, value, fielddConfig settings if any of them changed, then must rebuild field. Another important thing are dependend fields, that trigger some logic if one field value or other properties changes.
I agree that this feels a bit "clumsy" as you say, and in Flutter it is better to wrap in another widget than to extend. This can probably be refactored but would be a big breaking change, main problems being the handling of focus nodes and text controllers.
I was thinking base field with Hooks, but still don't know if it is the right approach.
Pseudo code:
class ReactiveField extends HookWidget {
final FieldConfig fieldConfig;
final String formControlName;
final ValidationMessagesFunction
ReactiveField( {Key? key, required this.formControlName, this.valueAccessor, this.showErrors, this.validationMessages, required this.fieldConfig, : super(key: key);
@override Widget build(BuildContext context) { final state = useFieldHook(fieldConfig, valueAccessor, showErrors, validationMessages); return TextField( onChange:(val)=>state.value=val, ... } }
Hook useFieldHook would mantain field state and changes and then some kind of factory to create form fields with input settings.
@sokolej79 I do not see much difference between pseudo code and current approach
We have to do the same things:
+
extend from custom class ReactiveFormField
vs HookWidget
+
pass through all props
+
map reactive_forms
control elements to the widget
Maybe I'm missing something
Yes maybe will need something better, but difference is that with hook approach (can be something else), we can create custom widget in build method and not extending StatefullWidget and build widget in constructor in super call. In constructor builder you cannot access class methods only static or extend state. This is very wrong approach.
For now I have extended ReactiveFormField and added custom properties and methods on state and every custom field extends this new base field widget. But I had to create all 17 custom field widgets we with this new base class and that is not good. I couldn't add additional properties and methods in some other way. And I dont control the rebuilds.
Could you give me quick example of your implementation and usage. Maybe I'll suggest you something
Currently I share field logic with inheritence in base Field model. In sample field BarcodeField I can't use private Widget methods because of constructor and super, I know can be used with decomposition smaller widget with inputs and callback functions but sometimes I prefer private methods and is not possible, to add methods on state as below is little strange.
Another question, maybe not for these thread, is where and how to listen for dependent field provided in FieldConfig settings from server. Expressions like: if field with key changes value show/hide another field or autocomplete selected first value depends on second autocomplete (company and person). All expressions provided in settings from server. Listen for expression, field changes in Form controller or in Field? It is some how difficult if you have multiple dependent fields with multiple expressions from settings. Listen each field separately or all in form valeuchanges and them on expression trigger some function that changes form field properties and rebuild? Forgot to tell that FieldConfig, FormOptionsState and TemplateOptions models in controller can change at runtime and must be updated fields accordingly.
Some current code and flow: 1.) On top I have FormController with Getx state management, which provides form data and schema, settings from server to build Formgroup and controls or FormGroup with nested Formgroups if is Form in tabs also has tracking form state changes, form mode insert, update, delete, submit...
2.) Fields
typedef ReactiveFieldBuilder<T, K> = Widget Function(ReactiveFieldState<T, K> field);
class ReactiveField<ModelDataType, ViewDataType> extends ReactiveFormField<ModelDataType, ViewDataType> {
final FieldConfig fieldConfig;
final ReactiveFieldBuilder<ModelDataType, ViewDataType> builder;
final ValidationMessagesFunction? validationMessage;
final ShowErrorsFunction? showError;
ReactiveField({required this.fieldConfig, required this.builder, this.validationMessage, this.showError})
: super(
formControlName: fieldConfig.key,
validationMessages: validationMessage,
showErrors: showError,
builder: (field) {
return Visibility(visible: fieldConfig.visible, child: builder(field as ReactiveFieldState<ModelDataType, ViewDataType>));
});
@override
ReactiveFieldState<ModelDataType, ViewDataType> createState() =>
ReactiveFieldState<ModelDataType, ViewDataType>(fieldConfig: fieldConfig);
}
// State for base reactive form Field
class ReactiveFieldState<ModelDataType, ViewDataType> extends ReactiveFormFieldState<ModelDataType, ViewDataType> {
final FieldConfig fieldConfig;
ReactiveFieldState({required this.fieldConfig});
bool get readOnly => (fieldConfig.templateOptions?.readonly ?? false) || control.disabled;
bool get isTouchedAndInvalid => control.touched && control.invalid && control.hasErrors;
// Other shared props and methods that uses field control state
}
// Sample custom field:
class BarcodeScannerFormField extends ReactiveField<dynamic, String> {
BarcodeScannerFormField({required FieldConfig fieldConfig})
: super(
fieldConfig: fieldConfig,
showError: fieldConfig.hasValidators ? (control) => showErrorMessage(control, fieldConfig.key) : null,
validationMessage: fieldConfig.hasValidators ? (control) => getValidationMessage(control, fieldConfig.key) : null,
builder: (field) {
final state = field as BarcodeScannerFormFieldState..setFocusNode();
return TextField(
onChanged: state.didChange,
enabled: state.control.enabled,
autocorrect: false,
controller: state.textController,
focusNode: state.focusNode,
readOnly: state.readOnly,
decoration: InputDecoration(
enabled: !state.readOnly,
labelText: fieldConfig.templateOptions?.label ?? '',
hintText: fieldConfig.templateOptions?.placeholder,
labelStyle: TextStyle(color: Theme.of(state.context).primaryColor),
border: OutlineInputBorder(),
filled: false,
errorText: state.errorText,
contentPadding: EdgeInsets.all(16),
suffixIcon: (state.control.value == null || state.control.value.isEmpty || !state.control.valid)
? IconButton(
onPressed: () async {
await state.scanBarcode();
},
icon: Icon(Icons.scanner),
)
: SizedBox(
width: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
onPressed: () async {
await state.scanBarcode();
},
icon: Icon(Icons.scanner),
),
IconButton(
onPressed: () {},
icon: Icon(Icons.done),
),
],
),
)),
keyboardType: TextInputType.text,
);
});
@override
BarcodeScannerFormFieldState createState() => BarcodeScannerFormFieldState(fieldConfig: fieldConfig);
}
// Extends InputFormFieldState already used in custom InputField and provide only additional methods
class BarcodeScannerFormFieldState extends InputFormFieldState {
BarcodeScannerFormFieldState({required FieldConfig fieldConfig}) : super(fieldConfig: fieldConfig);
Future scanBarcode() async {
try {
final barcodeScanRes = await FlutterBarcodeScanner.scanBarcode('#ff6666', 'Abandon', true, ScanMode.QR);
if (barcodeScanRes.isEmpty || barcodeScanRes.trim() == '-1') {
return;
}
updateBarcodeValue(barcodeScanRes.trim());
} catch (e, trace) {
logError('Error read barcode', '$e', trace);
toastError('Error read barcode. $e');
updateBarcodeValue('');
}
}
void updateBarcodeValue(dynamic value) {
try {
if (!control.dirty) {
control.markAsDirty(emitEvent: false);
}
control.updateValue(value);
if (!control.touched) {
control.markAsTouched();
}
} catch (e, trace) {
logError('BarcodeField - updateBarcodeValue ${fieldConfig.key}', e, trace);
}
}
}
// Some models for field and form settings
class FieldConfig {
final String id; // unique auto generated if not provided
final String key;
final int? groupId;
final FieldType type;
final dynamic defaultValue;
final dynamic initallValue;
final TemplateOptions? templateOptions;
final Map<String, String>? validationMessages;
final List<Map<String, dynamic>? Function(AbstractControl value)>? validators;
final List? asyncValidators;
final List? fieldGroup;
final FieldConfig? fieldArray;
final Map<String, dynamic>? expressionProperties;
final bool hidden;
}
// field template options
class TemplateOptions {
final String? label;
final String? placeholder;
final bool disabled;
final bool readonly;
final bool isRequired;
final List<SelectOption?>? options;
final Settings? settings;
final Function? onFocus;
final Function? onChange;
// for container field (tabs..)
final String? icon;
final int? selectedIndex;
final int? order;
}
// Settings for form and shared fields from server json:
class FormOptionsState {
final String? queryVariable;
final String? dataQuery;
final String? insertForm;
final String? updateForm;
final String? deleteForm;
final DataField? dataField;
final Map<String, dynamic>? selectCustomFileds;
}
// Field factory creates field widget:
class FieldFactory {
static Widget fromType({required FieldConfig fieldConfig}) {
switch (fieldConfig.type) {
case FieldType.text:
case FieldType.numeric:
case FieldType.textarea:
case FieldType.decimal:
return InputFormField(fieldConfig: fieldConfig);
case FieldType.datetime:
return DateTimeFormField(fieldConfig: fieldConfig);
//... other fields
default:
return InvalidFormField(fieldConfig: fieldConfig);
}
}
}
enum FieldType {
numeric,
decimal,
text,
textarea,
select,
multiselect,
date,
time,
datetime,
datetimenow,
dateRange,
autocomplete,
autocompletemultiselect,
radiogroup,
checkbox,
checkboxgroup,
switchfield,
attachment,
barcodeScanner,
unsupportedType,
errorField,
hiddenField
}
enum FormMode {insert, update, readOnly, delete }
Please format your code with tripple backticks ```dart
around the blocks.
Wow ) Long read)
I tried some simple sample test with hooks and is working. Detects error messages, control state, touched, if needed value changed... Parameters and output state can be anything desired and controlled.
final FieldConfig fieldConfig;
ReactiveTextField({required this.fieldConfig}) : super();
@override
Widget build(BuildContext context) {
final state = useFieldHook<String, String>(
fieldConfig); // can be any other parameters inputs
return TextField(
onTap: state.control.markAsTouched,
onChanged: (val) => state.changeValue(val),
decoration: InputDecoration(
errorText: state.errorText,
labelText: fieldConfig.templateOptions!.label,
hintText: fieldConfig.templateOptions!.placeholder),
);
}
}
Closing.
Creating custom reactive form field is is a little clumsy. Flutter promotes composition over inheritance as all software does. For new field we must extends statefullWidget ReactiveFormField<ModelDataType, ViewDataType> and create widget in builder in constructor super method. It will be better to create some kind of extendible base form field widget with abstract method build and some decorator pattern and create new custom field something like that :
class TestReactiveField extends StatelessWidget /Or StatefullWidget/ { final FieldConfig fieldConfig; TestReactiveField({required this.fieldConfig, Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return BaseReactiveField<String,String>( // state is model with field state and if required additional properties fieldConfig:fieldConfig, child:(state) =>MyCustomWidget(state:state), ); } } Or maybe replaced ReactiveFormField with hook widget to mantain state and custom field could extends hook widget and pass child. Another problem with ReactiveFormField is that is not easy "extandable" to add new properties and functions to base field that can be inherited from every custom field. Otherwise I would like to thank you for great package.