Dynamic Forms In Flutter
Writing a custom component
Requirement
Add the following dependencies to your pubspec.yaml
file:
flutter_dynamic_forms: <latest version>
dynamic_forms: ^1.0.0
To implement a custom component you need to provide 3 classes: Parser
, Model
, and Renderer
. Parsers and Models then need to be registered when you are building the form. Let's show it on the CheckBox
example:
Parser
This class controls how the component would be deserialized into a corresponding model class. It works on both XML and JSON. The parserNode
parameter contains a collection of methods that let you parse values from the current XML/JSON node. Use the ElementParserFunction parser
parameter of the parse method to recursively parse children nodes.
import 'package:dynamic_forms/dynamic_forms.dart';
import './checkbox.dart';
class CheckBoxParser extends FormElementParser<CheckBox> {
@override String get name => 'checkBox';
@override FormElement getInstance() => CheckBox();
@override void fillProperties(
CheckBox checkBox,
ParserNode parserNode,
Element? parent,
ElementParserFunction parser,)
{
super.fillProperties(checkBox, parserNode, parent, parser);
checkBox
..labelProperty = parserNode.getStringProperty('label')
..valueProperty = parserNode.getBoolProperty(
'value', isImmutable: false,
);
}
}
Model
Model is the main component definition without any Flutter dependency. A component can extend another component inheriting all the properties. It can also contain components as its children. Every property can contain either a simple value or an expression that is evaluated to the value. To be able to cover both of those cases all the properties must be defined using Property<T>
syntax. Properties are stored in a single map called properties
so you can easily traverse the whole component tree. It is a good idea to create getters and setters around this map so you can easily access and set property values.
import 'package:dynamic_forms/dynamic_forms.dart';
class CheckBox extends FormElement {
static const String labelPropertyName = 'label';
static const String valuePropertyName = 'value';
Property<String> get labelProperty {
return properties[labelPropertyName] as Property<String>; }
set labelProperty(Property<String> value) =>
registerProperty(labelPropertyName, value);
String get label =>labelProperty.value;
Stream<String> get labelChanged => labelProperty.valueChanged;
Property<bool> get valueProperty => properties[valuePropertyName] as Property<bool>;
set valueProperty(Property<bool> value) => registerProperty(valuePropertyName, value);
bool get value => valueProperty.value;
Stream<bool> get valueChanged => valueProperty.valueChanged;
@override FormElement getInstance() {
return CheckBox();
}
}
Renderer
This class simply takes the model and returns a Flutter widget. You can also subscribe to the changes in the properties so your widget will be properly updated when something happens on the model. For this purpose use the Stream defined on each property or use the component property propertyChanged
which returns Stream and emits value whenever any property changes. To redefine the UI of the default components inside flutter_dynamic_forms_components
simply define your renderers for the existing models. You can even have multiple renderers and show a different UI on a different screen. Use the FormElementRendererFunction renderer
parameter of the render method to recursively render children.
import 'package:flutter/material.dart';
import 'package:flutter_dynamic_forms/flutter_dynamic_forms.dart';
import 'checkbox.dart';
class CheckBoxRenderer extends FormElementRenderer<CheckBox> {
@override Widget render(
CheckBox element,
BuildContext context,
FormElementEventDispatcherFunction dispatcher,
FormElementRendererFunction renderer) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
StreamBuilder<dynamic>(
initialData: element.value,
stream: element.valueChanged,
builder: (context, snapshot) {
return Checkbox(
onChanged: (value) => dispatcher(
ChangeValueEvent(
value: value,
elementId: element.id!,
),
),
value: snapshot.data,
);
},
),
Padding(
padding: EdgeInsets.only(left: 8),
child: StreamBuilder<String>(
initialData: element.label,
stream: element.labelChanged,
builder: (context, snapshot) {
return Container(
height: 40,
width: 300,
child: Column(
children: [
Text(snapshot.data!),
],
),
);
},
),)
],),);}
}
Custom Component Implementation Example
import 'package:flutter/material.dart';
import 'package:dynamic_forms/dynamic_forms.dart';
import 'package:flutter_dynamic_forms/flutter_dynamic_forms.dart';
class SimpleFormJson extends StatefulWidget {
@override
_SimpleFormJsonState createState() => _SimpleFormJsonState();
}
class _SimpleFormJsonState extends State<SimpleFormJson> {
bool isLoading = true;
late String checkboxContent;
@override
void initState() {
super.initState();
_buildForm();
}
Future _buildForm() async {
checkboxContent= await rootBundle.loadString('assets/checkbox.json', cache: false);
setState(() {
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: ParsedFormProvider(
create: (_) => JsonFormManager(), // Or use XmlFormManager() to parse JSON data
content: checkboxContent, // jsonString,
parsers: custom_component.getCustomParserList(), //add your custom parsers
child: FormRenderer<JsonFormManager>( // Use matching FormManager type registered above
renderers: custom_component_renderer.getCustomRenderers(), // add your custom renderers
),
),
),
);
}
}
JsonString Example (assets/checkbox.dart)
{
"@name": "checkBox",
"id": "hideWelcomeCheckBox",
"value": "false",
"label": "This is the custom check box button i created form this json"
}
Custom Parsers List
import 'package:dynamic_forms/dynamic_forms.dart';
import 'checkbox_parser.dart';
List<FormElementParser<FormElement>> getCustomParserList() {
return [
CheckBoxParser(),
];
}
Custom Renderers List
import 'package:dynamic_forms/dynamic_forms.dart';
import 'package:example/custom_components/checkbox_renderer.dart';
import 'package:flutter_dynamic_forms/flutter_dynamic_forms.dart';
List<FormElementRenderer<FormElement>> getCustomRenderers() {
return [
CheckBoxRenderer()
];
}
Another Example with Drop Down button
Json File
{
"@name": "form",
"id": "form1",
"children": [
{
"@name": "dropdownButton",
"id": "selectScanner",
"value": "AF",
"hint": "Select Scanner",
"choices": [
{
"@name": "dropdownOption",
"label": "front camera",
"value": "AF"
},
{
"@name": "dropdownOption",
"label": "scanner",
"value": "AR"
}
]
},
{
"@name": "formGroup",
"id": "formgroup3",
"name": "Allowed document type is PDF and Max size 1MB",
"children": [
{
"@name": "dropdownButton",
"id": "proofOfAddress",
"value": "AF",
"hint": "Address Proof",
"uploadButton": "Upload",
"choices": [
{
"@name": "dropdownOption",
"label": "Driver License",
"value": "AF"
},
{
"@name": "dropdownOption",
"label": "Passport",
"value": "AR"
}
]
},
{
"@name": "dropdownButton",
"id": "proofOfIdentity",
"value": "AF",
"hint": "Identity Proof",
"uploadButton": "Upload",
"choices": [
{
"@name": "dropdownOption",
"label": "Driver License",
"value": "AF"
},
{
"@name": "dropdownOption",
"label": "Passport",
"value": "AR"
}
]
},
{
"@name": "dropdownButton",
"id": "consentProof",
"value": "AF",
"hint": "Consent Proof",
"uploadButton": "Upload",
"choices": [
{
"@name": "dropdownOption",
"label": "Consent",
"value": "AF"
}
]
}
]
}
]
}
From the above Json we were able to create the following dynamic UI