Skip to content

Commit 113991d

Browse files
authored
Rethink Forms. (flutter#6569)
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 flutter#6097 and based on feedback from the same.
1 parent 43b4fc9 commit 113991d

File tree

6 files changed

+454
-330
lines changed

6 files changed

+454
-330
lines changed

examples/flutter_gallery/lib/demo/expansion_panels_demo.dart

+95-78
Original file line numberDiff line numberDiff line change
@@ -189,23 +189,25 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
189189
});
190190
}
191191

192-
return new CollapsibleBody(
193-
margin: const EdgeInsets.symmetric(horizontal: 16.0),
194-
child: new Form(
195-
child: new Padding(
196-
padding: const EdgeInsets.symmetric(horizontal: 16.0),
197-
child: new Input(
198-
hintText: item.hint,
199-
labelText: item.name,
200-
value: new InputValue(text: item.value),
201-
formField: new FormField<String>(
202-
setter: (String val) { item.value = val; }
192+
return new Form(
193+
child: new Builder(
194+
builder: (BuildContext context) {
195+
return new CollapsibleBody(
196+
margin: const EdgeInsets.symmetric(horizontal: 16.0),
197+
onSave: () { Form.of(context).save(); close(); },
198+
onCancel: () { Form.of(context).reset(); close(); },
199+
child: new Padding(
200+
padding: const EdgeInsets.symmetric(horizontal: 16.0),
201+
child: new InputFormField(
202+
hintText: item.hint,
203+
labelText: item.name,
204+
initialValue: new InputValue(text: item.value),
205+
onSaved: (InputValue val) { item.value = val.text; },
206+
),
203207
),
204-
),
205-
),
206-
),
207-
onSave: close,
208-
onCancel: close
208+
);
209+
}
210+
)
209211
);
210212
}
211213
),
@@ -221,54 +223,61 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
221223
});
222224
}
223225

224-
void changeLocation(_Location newLocation) {
225-
setState(() {
226-
item.value = newLocation;
227-
});
228-
}
229226

230-
return new CollapsibleBody(
231-
child: new Column(
232-
mainAxisSize: MainAxisSize.min,
233-
crossAxisAlignment: CrossAxisAlignment.start,
234-
children: <Widget>[
235-
new Row(
236-
mainAxisSize: MainAxisSize.min,
237-
children: <Widget>[
238-
new Radio<_Location>(
239-
value: _Location.Bahamas,
240-
groupValue: item.value,
241-
onChanged: changeLocation
242-
),
243-
new Text('Bahamas')
244-
]
245-
),
246-
new Row(
247-
mainAxisSize: MainAxisSize.min,
248-
children: <Widget>[
249-
new Radio<_Location>(
250-
value: _Location.Barbados,
251-
groupValue: item.value,
252-
onChanged: changeLocation
253-
),
254-
new Text('Barbados')
255-
]
256-
),
257-
new Row(
258-
mainAxisSize: MainAxisSize.min,
259-
children: <Widget>[
260-
new Radio<_Location>(
261-
value: _Location.Bermuda,
262-
groupValue: item.value,
263-
onChanged: changeLocation
264-
),
265-
new Text('Bermuda')
266-
]
267-
)
268-
]
269-
),
270-
onSave: close,
271-
onCancel: close
227+
return new Form(
228+
child: new Builder(
229+
builder: (BuildContext context) {
230+
return new CollapsibleBody(
231+
onSave: () { Form.of(context).save(); close(); },
232+
onCancel: () { Form.of(context).reset(); close(); },
233+
child: new FormField<_Location>(
234+
initialValue: item.value,
235+
onSaved: (_Location result) { item.value = result; },
236+
builder: (FormFieldState<_Location> field) {
237+
return new Column(
238+
mainAxisSize: MainAxisSize.min,
239+
crossAxisAlignment: CrossAxisAlignment.start,
240+
children: <Widget>[
241+
new Row(
242+
mainAxisSize: MainAxisSize.min,
243+
children: <Widget>[
244+
new Radio<_Location>(
245+
value: _Location.Bahamas,
246+
groupValue: field.value,
247+
onChanged: field.onChanged,
248+
),
249+
new Text('Bahamas')
250+
]
251+
),
252+
new Row(
253+
mainAxisSize: MainAxisSize.min,
254+
children: <Widget>[
255+
new Radio<_Location>(
256+
value: _Location.Barbados,
257+
groupValue: field.value,
258+
onChanged: field.onChanged,
259+
),
260+
new Text('Barbados')
261+
]
262+
),
263+
new Row(
264+
mainAxisSize: MainAxisSize.min,
265+
children: <Widget>[
266+
new Radio<_Location>(
267+
value: _Location.Bermuda,
268+
groupValue: field.value,
269+
onChanged: field.onChanged,
270+
),
271+
new Text('Bermuda')
272+
]
273+
)
274+
]
275+
);
276+
}
277+
),
278+
);
279+
}
280+
)
272281
);
273282
}
274283
),
@@ -284,22 +293,30 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
284293
});
285294
}
286295

287-
return new CollapsibleBody(
288-
child: new Slider(
289-
value: item.value,
290-
min: 0.0,
291-
max: 100.0,
292-
divisions: 5,
293-
activeColor: Colors.orange[100 + (item.value * 5.0).round()],
294-
label: '${item.value.round()}',
295-
onChanged: (double value) {
296-
setState(() {
297-
item.value = value;
298-
});
296+
return new Form(
297+
child: new Builder(
298+
builder: (BuildContext context) {
299+
return new CollapsibleBody(
300+
onSave: () { Form.of(context).save(); close(); },
301+
onCancel: () { Form.of(context).reset(); close(); },
302+
child: new FormField<double>(
303+
initialValue: item.value,
304+
onSaved: (double value) { item.value = value; },
305+
builder: (FormFieldState<double> field) {
306+
return new Slider(
307+
min: 0.0,
308+
max: 100.0,
309+
divisions: 5,
310+
activeColor: Colors.orange[100 + (field.value * 5.0).round()],
311+
label: '${field.value.round()}',
312+
value: field.value,
313+
onChanged: field.onChanged,
314+
);
315+
},
316+
),
317+
);
299318
}
300-
),
301-
onSave: close,
302-
onCancel: close
319+
)
303320
);
304321
}
305322
)

examples/flutter_gallery/lib/demo/text_field_demo.dart

+39-35
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,39 @@ class TextFieldDemoState extends State<TextFieldDemo> {
3030
));
3131
}
3232

33+
GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
34+
GlobalKey<FormFieldState<InputValue>> _passwordFieldKey = new GlobalKey<FormFieldState<InputValue>>();
3335
void _handleSubmitted() {
34-
// TODO(mpcomplete): Form could keep track of validation errors?
35-
if (_validateName(person.name) != null ||
36-
_validatePhoneNumber(person.phoneNumber) != null ||
37-
_validatePassword(person.password) != null) {
36+
FormState form = _formKey.currentState;
37+
if (form.hasErrors) {
3838
showInSnackBar('Please fix the errors in red before submitting.');
3939
} else {
40+
form.save();
4041
showInSnackBar('${person.name}\'s phone number is ${person.phoneNumber}');
4142
}
4243
}
4344

44-
String _validateName(String value) {
45-
if (value.isEmpty)
45+
String _validateName(InputValue value) {
46+
if (value.text.isEmpty)
4647
return 'Name is required.';
4748
RegExp nameExp = new RegExp(r'^[A-za-z ]+$');
48-
if (!nameExp.hasMatch(value))
49+
if (!nameExp.hasMatch(value.text))
4950
return 'Please enter only alphabetical characters.';
5051
return null;
5152
}
5253

53-
String _validatePhoneNumber(String value) {
54+
String _validatePhoneNumber(InputValue value) {
5455
RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$');
55-
if (!phoneExp.hasMatch(value))
56+
if (!phoneExp.hasMatch(value.text))
5657
return '###-###-#### - Please enter a valid phone number.';
5758
return null;
5859
}
5960

60-
String _validatePassword(String value) {
61-
if (person.password == null || person.password.isEmpty)
61+
String _validatePassword(InputValue value) {
62+
FormFieldState<InputValue> passwordField = _passwordFieldKey.currentState;
63+
if (passwordField.value == null || passwordField.value.text.isEmpty)
6264
return 'Please choose a password.';
63-
if (person.password != value)
65+
if (passwordField.value.text != value.text)
6466
return 'Passwords don\'t match';
6567
return null;
6668
}
@@ -73,55 +75,57 @@ class TextFieldDemoState extends State<TextFieldDemo> {
7375
title: new Text('Text fields')
7476
),
7577
body: new Form(
78+
key: _formKey,
7679
child: new Block(
7780
padding: const EdgeInsets.symmetric(horizontal: 16.0),
7881
children: <Widget>[
79-
new Input(
80-
hintText: 'What do people call you?',
81-
labelText: 'Name',
82-
formField: new FormField<String>(
83-
// TODO(mpcomplete): replace with person#name=
84-
setter: (String val) { person.name = val; },
85-
validator: _validateName
86-
)
82+
// It's simpler to use an InputFormField, as below, but a FormField
83+
// that builds an Input is equivalent.
84+
new FormField<InputValue>(
85+
initialValue: InputValue.empty,
86+
onSaved: (InputValue val) { person.name = val.text; },
87+
validator: _validateName,
88+
builder: (FormFieldState<InputValue> field) {
89+
return new Input(
90+
hintText: 'What do people call you?',
91+
labelText: 'Name',
92+
value: field.value,
93+
onChanged: field.onChanged,
94+
errorText: field.errorText
95+
);
96+
},
8797
),
88-
new Input(
98+
new InputFormField(
8999
hintText: 'Where can we reach you?',
90100
labelText: 'Phone Number',
91101
keyboardType: TextInputType.phone,
92-
formField: new FormField<String>(
93-
setter: (String val) { person.phoneNumber = val; },
94-
validator: _validatePhoneNumber
95-
)
102+
onSaved: (InputValue val) { person.phoneNumber = val.text; },
103+
validator: _validatePhoneNumber,
96104
),
97-
new Input(
105+
new InputFormField(
98106
hintText: 'Tell us about yourself (optional)',
99107
labelText: 'Life story',
100108
maxLines: 3,
101-
formField: new FormField<String>()
102109
),
103110
new Row(
104111
crossAxisAlignment: CrossAxisAlignment.start,
105112
children: <Widget>[
106113
new Flexible(
107-
child: new Input(
114+
child: new InputFormField(
115+
key: _passwordFieldKey,
108116
hintText: 'How do you log in?',
109117
labelText: 'New Password',
110118
hideText: true,
111-
formField: new FormField<String>(
112-
setter: (String val) { person.password = val; }
113-
)
119+
onSaved: (InputValue val) { person.password = val.text; }
114120
)
115121
),
116122
new SizedBox(width: 16.0),
117123
new Flexible(
118-
child: new Input(
124+
child: new InputFormField(
119125
hintText: 'How do you log in?',
120126
labelText: 'Re-type Password',
121127
hideText: true,
122-
formField: new FormField<String>(
123-
validator: _validatePassword
124-
)
128+
validator: _validatePassword,
125129
)
126130
)
127131
]

0 commit comments

Comments
 (0)