-
Notifications
You must be signed in to change notification settings - Fork 60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ReactiveFormGroupRenderer dynamically? #65
Comments
Hi @ilubnon, Thanks for the great question! Some time ago we were also discussing this feature in our team. The concrete example might be that you have an address form group with multiple address fields like street, city, etc. and you would want to add another address on a button click.
Although it is not impossible to solve, we decided to take a simple path for now: download the new form from the server and let the server add those fields. That being said, if you don't need to have this behavior described in the XML/JSON, I think there is a possible solution to this problem on the client-side. However, it will not be implemented in the renderer as you are suggesting but rather in the model layer. Specifically, there is a To make it simple I would start by making a special component, for example Because I find this problem very interesting I may prepare some example of what I just described in the following weeks. |
@OndrejKunc Thank you very much! I took the liberty of creating some tests, as you mentioned. However, I just copied the ReactiveFormGroupRenderer by changing the class name and consequently the model and also the parser. Something like that: Model Groupimport 'package:dynamic_forms/dynamic_forms.dart';
import 'package:flutter_dynamic_forms_components/flutter_dynamic_forms_components.dart';
class Group extends Container {
static const String namePropertyName = 'name';
Property<String> get nameProperty => properties[namePropertyName];
set nameProperty(Property<String> value) =>
registerProperty(namePropertyName, value);
String get name => nameProperty.value;
Stream<String> get nameChanged => nameProperty.valueChanged;
@override
FormElement getInstance() {
return Group();
}
} Parser Groupimport 'package:flutter_dynamic_forms_components/flutter_dynamic_forms_components.dart';
import 'package:dynamic_forms/dynamic_forms.dart';
import 'package:tovtec_dynamic_forms/models/groupModel.dart' as model;
class GroupParser<TGroup extends model.Group>
extends ContainerParser<TGroup> {
@override
String get name => 'group';
@override
FormElement getInstance() => model.Group();
@override
void fillProperties(
TGroup formGroup,
ParserNode parserNode,
Element parent,
ElementParserFunction parser,
) {
super.fillProperties(formGroup, parserNode, parent, parser);
formGroup
..nameProperty = parserNode.getStringProperty(
'name',
defaultValue: ParserNode.defaultString,
isImmutable: true,
);
}
}
Renderer Groupimport 'package:dynamic_forms/dynamic_forms.dart';
import 'package:expression_language/expression_language.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_dynamic_forms/flutter_dynamic_forms.dart';
import 'package:tovtec_dynamic_forms/models/groupModel.dart' as model;
class ReactiveGroupRenderer extends FormElementRenderer<model.Group> {
@override
Widget render(
model.Group element,
BuildContext context,
FormElementEventDispatcherFunction dispatcher,
FormElementRendererFunction renderer) {
return StreamBuilder<List<ExpressionProviderElement>>(
initialData: element.children,
stream: element.childrenChanged,
builder: (context, snapshot) {
List<Widget> childrenWidgets = [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
element.name,
style: TextStyle(color: Colors.grey),
),
)
];
childrenWidgets.addAll(
snapshot.data.whereType<FormElement>().where((f) => f.isVisible).map(
(child) => renderer(child, context),
),
);
childrenWidgets.add(new IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
// Widget elementCopy = renderer(element, context);
// childrenWidgets.add(elementCopy); ???
}));
return Column(children: childrenWidgets);
},
);
}
}
|
So I gave it a try and was able to find a solution. Here is what you need to do: BehaviorSubject<int> changedSubject = BehaviorSubject<int>.seeded(0);
Stream<int> get changedStream => changedSubject.stream; Create a new event which will allow you to pass your component through the dispatcher: class CopyFirstChildEvent extends FormElementEvent {
final CopyContainer copyContainer; //Pass your Group component instead
CopyFirstChildEvent(this.copyContainer);
} In your Renderer wrap the outer StreamBuilder in another StreamBuilder listening to the class CopyContainerRenderer extends FormElementRenderer<CopyContainer> {
@override
Widget render(
CopyContainer element,
BuildContext context,
FormElementEventDispatcherFunction dispatcher,
FormElementRendererFunction renderer) {
return StreamBuilder<int>(
initialData: 0,
stream: element.changedStream,
builder: (context, itemCount) {
return StreamBuilder<List<ExpressionProviderElement>>(
initialData: element.children,
stream: element.childrenChanged,
builder: (context, snapshot) {
return Column(
children: [
...snapshot.data
.whereType<FormElement>()
.where((f) => f.isVisible)
.map(
(child) => renderer(child, context),
)
.toList(),
IconButton(
icon: const Icon(Icons.add_circle_outline),
onPressed: () {
dispatcher(CopyFirstChildEvent(element));
},
)
],
);
},
);
},
);
}
} Finally, process your event in the same place you are processing void _onFormElementEvent(FormElementEvent event) {
if (event is ChangeValueEvent) {
_formManager.changeValue(
value: event.value,
elementId: event.elementId,
propertyName: event.propertyName,
ignoreLastChange: event.ignoreLastChange);
}
if (event is CopyFirstChildEvent) {
var children = event.copyContainer.children;
if (children.isEmpty) {
return;
}
// Create copy of the first children
var clonedRoot = children[0].clone(null);
var clonedElements =
getFormElementIterator<FormElement>(clonedRoot).toList();
// Change id of each element in the cloned subtree
for (var i = 0; i < clonedElements.length; i++) {
var clonedElement = clonedElements[i];
if (clonedElement.id == null) {
continue;
}
clonedElement.id = "${clonedElement.id}_$i";
_formManager.formElementMap[clonedElement.id] = clonedElement;
}
// Build expressions in the cloned subtree
var clonedExpressions =
getFormPropertyIterator<CloneableExpressionProperty>(clonedRoot);
for (var expressionValue in clonedExpressions) {
expressionValue.buildExpression(_formManager.formElementMap);
}
// Add subscriptions to existing expressions
for (var expressionValue in clonedExpressions) {
var elementsValuesCollectorVisitor =
ExpressionProviderCollectorVisitor();
expressionValue.getExpression().accept(elementsValuesCollectorVisitor);
for (var sourceProperty
in elementsValuesCollectorVisitor.expressionProviders) {
(sourceProperty as Property).addSubscriber(expressionValue);
}
}
(clonedRoot as FormElement).parentProperty = children[0].parentProperty;
// Add back to the children list
children.add(clonedRoot);
// Notify view about the change
event.copyContainer.changedSubject.add(children.length);
}
} And that's it. Please let me know if this solved your problem or if you need any further help. |
This is pretty interesting, is there any chance this gets rebased and put into master? I'm looking to create a number of form groups based on a select field and this looks like it would be a good base. |
Hello @OndrejKunc, congratulations on the project!
Is it possible to duplicate (creating a new formGroup with default values) a formGroup dynamically?
In the ReactiveFormGroupRenderer class, I would like to do something like that, rendering with a button to dynamically add another formGroup
Something like that:
The text was updated successfully, but these errors were encountered: