Skip to content

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

Merged
merged 3 commits into from
Nov 2, 2016
Merged

Rethink Forms. #6569

merged 3 commits into from
Nov 2, 2016

Conversation

mpcomplete
Copy link
Contributor

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.

@mpcomplete
Copy link
Contributor Author

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:

  1. It works with any type of widget.
  2. It's easy to make helper functions/widgets a la InputFormField to simplify creating FormFields that contain a specific widget.
  3. FormFields work outside of a Form, if you just want to use it for managing the state of a widget.
  4. Forms save and validate as a unit, making it easy to reset the form state (see the ExpansionPanelDemo's cancel).
  5. Input no longer has a bunch of special case code for working inside a Form.

@@ -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?
Copy link
Contributor

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)

Copy link
Contributor

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.

Copy link
Contributor Author

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(); },
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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?
Copy link
Contributor

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();
Copy link
Contributor

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

Copy link
Contributor Author

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.

Copy link
Contributor

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);
Copy link
Contributor

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);

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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() {
Copy link
Contributor

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.

@Hixie
Copy link
Contributor

Hixie commented Oct 27, 2016

I like this general approach.

@Hixie
Copy link
Contributor

Hixie commented Oct 27, 2016

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
)
Copy link
Contributor

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).

@Hixie
Copy link
Contributor

Hixie commented Oct 27, 2016

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.
@mpcomplete
Copy link
Contributor Author

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(); },
Copy link
Contributor

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

Copy link
Contributor Author

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>(
Copy link
Contributor

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?

Copy link
Contributor Author

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) {
Copy link
Contributor

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>(
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@Hixie
Copy link
Contributor

Hixie commented Nov 2, 2016

LGTM modulo minor comments

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rethink Forms
3 participants