-
Notifications
You must be signed in to change notification settings - Fork 28.7k
Rethink Forms. #6569
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
Rethink Forms. #6569
Conversation
One more attempt at making sense of Forms. This is based on customer feedback from #6097 . There's a number of advantages to this approach, IMO:
|
@@ -185,27 +189,30 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> { | |||
builder: (DemoItem<String> item) { // ignore: argument_type_not_assignable, https://github.com/flutter/flutter/issues/5771 | |||
void close() { | |||
setState(() { | |||
// Clear the input state. | |||
// TODO(mpcomplete): is there a way to have a Form.reset that purges its children? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(please make sure you answer this question before check-in)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this be straight-forward? I don't really understand the problem here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it should! I don't know how, though. :)
Tips?
); | ||
}, | ||
), | ||
onSave: () { form3Key.currentState.save(); close(); }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's sad we have to get the state via a key here... not sure what to suggest instead though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe onSave should give you a BuildContext you can use to find the Form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That could work here where we have a custom widget that implements the onSave function. But sadly there doesn't seem to be a general solution to this issue (see text_field_demo, where we want to call save when a generic RaisedButton is pressed).
I've actually found myself wanting something like this multiple times in this patch - a way for a child widget to refer to its ancestor. The whole reason FormField takes a builder function instead of a child widget is that I want to use FormField's state in the child's builder. A builder function is the only way for child widgets to depend on their ancestor's state in some way.
} | ||
/// A [FormField] that contains an [Input]. | ||
class InputFormField extends FormField<InputValue> { | ||
// TODO(mpcomplete): is there some way to keep args these in sync with the Input constructor? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sadly not.
|
||
@override | ||
void deactivate() { | ||
super.deactivate(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: call super.deactivate last
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW, is there a better way to do this registration? I noticed you did something fancy with inheritFromWidgetOfExactType, but I didn't quite understand it and wasn't sure if it applied here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my patch the Form didn't need to do anything except cause the registered widgets to rebuild, so it could be vastly simpler. Here you're actually doing things with the widgets, so it's much more complicated.
super.deactivate(); | ||
FormScope formScope = FormScope.of(context); | ||
if (formScope != null) | ||
formScope._formState._unregister(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rather than these three lines, we should just make the API be Form.register(context, this);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it doesn't matter since this is all private stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't actually work. Consider what happens if you're moved around in the tree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the right way to do this is to use the pattern I had in the other patch, where Form itself is Inherited and so on, and then for your functions that call things on all the dependents, just add a visitDependents method to InheritedElement that lets you walk all the _dependents, similar to visitChildren, and then call them that way, similar to how I called dispatchDependenciesChanged
in my fieldChanged
function in my patch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't actually work. Consider what happens if you're moved around in the tree.
I thought deactivate
was called when this object was moved around in the tree. That's why I'm unregistering here. My understanding was that it would re-register at its new location in the build function. Is this wrong?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, my bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make sure you have a test for this particular case. I'm not confident we get the order of operations right here. It's possible we detach you from the tree before calling deactivate.
int generation = 0; | ||
Set<FormFieldState> _fields = new Set<FormFieldState>(); | ||
|
||
void _onFieldChanged() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our convention is:
- onFoo for variables that take callbacks
- handleFoo for functions that are assigned to onFoo variables
- FooCallback for the types of those variables
- didFoo for methods that you call to notify the object
So I think here you want fieldDidChange or some such.
I like this general approach. |
My main concern is over the need to use GlobalKeys so much to make use of this. |
onCancel: close | ||
onSave: () { form1Key.currentState.save(); close(); }, | ||
onCancel: close | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, minor side-note, please move the onSave and onCancel arguments to above the child argument, so that it's clearer which widget it belongs to. Both Adam and I independently misread this the first time (I thought they were on Form, he thought they were on InputFormField).
This seems like a net improvement on what we have now, so, modulo the comments above, travis being angry, and the need for tests, LGTM. We should loop back with customer:leafy once this is in and see how they like it. |
FormField is now a widget that can contain any type of field. Input no longer has special code to handle form fields. Instead, there is a helper widget InputFormField for using an Input inside a FormField. Fixes #6097 and based on feedback from the same.
OK, added a test for the FormState.deactivate case. I think this is ready for commit (pending travis approval). |
key: form1Key, | ||
child: new CollapsibleBody( | ||
margin: const EdgeInsets.symmetric(horizontal: 16.0), | ||
onSave: () { form1Key.currentState.save(); close(); }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you're in the form here, so you could avoid using a Key if you instead put a Builder below the form and used Form.of
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
child: new CollapsibleBody( | ||
onSave: () { form2Key.currentState.save(); close(); }, | ||
onCancel: () { form2Key.currentState.reset(); close(); }, | ||
child: new FormField<_Location>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you imagining that eventually we'll have a RadioButtonGroupField or something which generates all the radio buttons and so forth, similar to how we do InputFormField for an Input?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I imagine we'll have helper Field widgets for the common cases. But it's hopefully easy enough to use FormField directly for the cases we don't cover.
_validatePhoneNumber(person.phoneNumber) != null || | ||
_validatePassword(person.password) != null) { | ||
FormState form = _formKey.currentState; | ||
if (form.hasErrors) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice
setter: (String val) { person.name = val; }, | ||
validator: _validateName | ||
) | ||
new FormField<InputValue>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not an InputFormField here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added a comment - I want to demonstrate the equivalence of the 2 ways of doing it.
LGTM modulo minor comments |
FormField is now a widget that can contain any type of field. Input no
longer has special code to handle form fields. Instead, there is a
helper widget InputFormField for using an Input inside a FormField.
Fixes #6097 and based on
feedback from the same.