Skip to content

Commit

Permalink
Rationalize text input widgets (#9119)
Browse files Browse the repository at this point in the history
After this patch, there are three major text input widgets:

 * EditableText. This widget is a low-level editing control that
   interacts with the IME and displays a blinking cursor.

 * TextField. This widget is a Material Design text field, with all the
   bells and whistles. It is highly configurable and can be reduced down
   to a fairly simple control by setting its `decoration` property to
   null.

 * TextFormField. This widget is a FormField that wraps a TextField.

This patch also replaces the InputValue data model for these widgets
with a Listenable TextEditingController, which is much more flexible.

Fixes #7031
  • Loading branch information
abarth committed Apr 2, 2017
1 parent 91dbb3c commit ae89948
Show file tree
Hide file tree
Showing 23 changed files with 1,357 additions and 1,330 deletions.
12 changes: 4 additions & 8 deletions dev/manual_tests/card_collection.dart
Expand Up @@ -7,12 +7,12 @@ import 'package:flutter/rendering.dart' show debugDumpRenderTree;

class CardModel {
CardModel(this.value, this.height) {
inputValue = new InputValue(text: 'Item $value');
textController = new TextEditingController(text: 'Item $value');
}
int value;
double height;
int get color => ((value % 9) + 1) * 100;
InputValue inputValue;
TextEditingController textController;
Key get key => new ObjectKey(this);
}

Expand Down Expand Up @@ -245,11 +245,7 @@ class CardCollectionState extends State<CardCollection> {
new Center(
child: new TextField(
key: new GlobalObjectKey(cardModel),
onChanged: (InputValue value) {
setState(() {
cardModel.inputValue = value;
});
},
controller: cardModel.textController,
),
)
: new DefaultTextStyle.merge(
Expand All @@ -261,7 +257,7 @@ class CardCollectionState extends State<CardCollection> {
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(cardModel.inputValue.text, textAlign: _textAlign),
new Text(cardModel.textController.text, textAlign: _textAlign),
],
),
),
Expand Down
Expand Up @@ -26,9 +26,11 @@ class _InputDropdown extends StatelessWidget {
Widget build(BuildContext context) {
return new InkWell(
onTap: onPressed,
child: new InputContainer(
labelText: labelText,
style: valueStyle,
child: new InputDecorator(
decoration: new InputDecoration(
labelText: labelText,
),
baseStyle: valueStyle,
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
Expand Down Expand Up @@ -133,11 +135,15 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
padding: const EdgeInsets.all(16.0),
children: <Widget>[
new TextField(
labelText: 'Event name',
decoration: const InputDecoration(
labelText: 'Event name',
),
style: Theme.of(context).textTheme.display1,
),
new TextField(
labelText: 'Location',
decoration: const InputDecoration(
labelText: 'Location',
),
style: Theme.of(context).textTheme.display1.copyWith(fontSize: 20.0),
),
new _DateTimePicker(
Expand Down Expand Up @@ -170,9 +176,11 @@ class _DateAndTimePickerDemoState extends State<DateAndTimePickerDemo> {
});
},
),
new InputContainer(
labelText: 'Activity',
hintText: 'Choose an activity',
new InputDecorator(
decoration: const InputDecoration(
labelText: 'Activity',
hintText: 'Choose an activity',
),
isEmpty: _activity == null,
child: new DropdownButton<String>(
value: _activity,
Expand Down
Expand Up @@ -148,10 +148,11 @@ class DemoItem<T> {
this.hint,
this.builder,
this.valueToString
});
}) : textController = new TextEditingController(text: valueToString(value));

final String name;
final String hint;
final TextEditingController textController;
final DemoItemBodyBuilder<T> builder;
final ValueToString<T> valueToString;
T value;
Expand Down Expand Up @@ -205,18 +206,20 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
onCancel: () { Form.of(context).reset(); close(); },
child: new Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: new TextField(
hintText: item.hint,
labelText: item.name,
initialValue: new InputValue(text: item.value),
onSaved: (InputValue val) { item.value = val.text; },
child: new TextFormField(
controller: item.textController,
decoration: new InputDecoration(
hintText: item.hint,
labelText: item.name,
),
onSaved: (String value) { item.value = value; },
),
),
);
}
)
},
),
);
}
},
),
new DemoItem<_Location>(
name: 'Location',
Expand All @@ -229,8 +232,6 @@ class _ExpansionPanelsDemoState extends State<ExpasionPanelsDemo> {
item.isExpanded = false;
});
}


return new Form(
child: new Builder(
builder: (BuildContext context) {
Expand Down
2 changes: 1 addition & 1 deletion examples/flutter_gallery/lib/demo/material/material.dart
Expand Up @@ -27,6 +27,6 @@ export 'slider_demo.dart';
export 'snack_bar_demo.dart';
export 'tabs_demo.dart';
export 'tabs_fab_demo.dart';
export 'text_field_demo.dart';
export 'text_form_field_demo.dart';
export 'tooltip_demo.dart';
export 'two_level_list_demo.dart';
Expand Up @@ -6,13 +6,13 @@ import 'dart:async';

import 'package:flutter/material.dart';

class TextFieldDemo extends StatefulWidget {
TextFieldDemo({ Key key }) : super(key: key);
class TextFormFieldDemo extends StatefulWidget {
TextFormFieldDemo({ Key key }) : super(key: key);

static const String routeName = '/material/text-field';
static const String routeName = '/material/text-form-field';

@override
TextFieldDemoState createState() => new TextFieldDemoState();
TextFormFieldDemoState createState() => new TextFormFieldDemoState();
}

class PersonData {
Expand All @@ -21,7 +21,7 @@ class PersonData {
String password = '';
}

class TextFieldDemoState extends State<TextFieldDemo> {
class TextFormFieldDemoState extends State<TextFormFieldDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();

PersonData person = new PersonData();
Expand All @@ -35,7 +35,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
bool _autovalidate = false;
bool _formWasEdited = false;
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
final GlobalKey<FormFieldState<InputValue>> _passwordFieldKey = new GlobalKey<FormFieldState<InputValue>>();
final GlobalKey<FormFieldState<String>> _passwordFieldKey = new GlobalKey<FormFieldState<String>>();
void _handleSubmitted() {
final FormState form = _formKey.currentState;
if (!form.validate()) {
Expand All @@ -47,30 +47,30 @@ class TextFieldDemoState extends State<TextFieldDemo> {
}
}

String _validateName(InputValue value) {
String _validateName(String value) {
_formWasEdited = true;
if (value.text.isEmpty)
if (value.isEmpty)
return 'Name is required.';
final RegExp nameExp = new RegExp(r'^[A-za-z ]+$');
if (!nameExp.hasMatch(value.text))
if (!nameExp.hasMatch(value))
return 'Please enter only alphabetical characters.';
return null;
}

String _validatePhoneNumber(InputValue value) {
String _validatePhoneNumber(String value) {
_formWasEdited = true;
final RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$');
if (!phoneExp.hasMatch(value.text))
if (!phoneExp.hasMatch(value))
return '###-###-#### - Please enter a valid phone number.';
return null;
}

String _validatePassword(InputValue value) {
String _validatePassword(String value) {
_formWasEdited = true;
final FormFieldState<InputValue> passwordField = _passwordFieldKey.currentState;
if (passwordField.value == null || passwordField.value.text.isEmpty)
final FormFieldState<String> passwordField = _passwordFieldKey.currentState;
if (passwordField.value == null || passwordField.value.isEmpty)
return 'Please choose a password.';
if (passwordField.value.text != value.text)
if (passwordField.value != value)
return 'Passwords don\'t match';
return null;
}
Expand Down Expand Up @@ -104,7 +104,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text('Text fields')
title: new Text('Text fields'),
),
body: new Form(
key: _formKey,
Expand All @@ -113,48 +113,58 @@ class TextFieldDemoState extends State<TextFieldDemo> {
child: new ListView(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
children: <Widget>[
new TextField(
icon: new Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
onSaved: (InputValue val) { person.name = val.text; },
new TextFormField(
decoration: const InputDecoration(
icon: const Icon(Icons.person),
hintText: 'What do people call you?',
labelText: 'Name *',
),
onSaved: (String value) { person.name = value; },
validator: _validateName,
),
new TextField(
icon: new Icon(Icons.phone),
hintText: 'Where can we reach you?',
labelText: 'Phone Number *',
new TextFormField(
decoration: const InputDecoration(
icon: const Icon(Icons.phone),
hintText: 'Where can we reach you?',
labelText: 'Phone Number *',
),
keyboardType: TextInputType.phone,
onSaved: (InputValue val) { person.phoneNumber = val.text; },
onSaved: (String value) { person.phoneNumber = value; },
validator: _validatePhoneNumber,
),
new TextField(
hintText: 'Tell us about yourself',
labelText: 'Life story',
new TextFormField(
decoration: const InputDecoration(
hintText: 'Tell us about yourself',
labelText: 'Life story',
),
maxLines: 3,
),
new Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Expanded(
child: new TextField(
child: new TextFormField(
key: _passwordFieldKey,
hintText: 'How do you log in?',
labelText: 'New Password *',
decoration: const InputDecoration(
hintText: 'How do you log in?',
labelText: 'New Password *',
),
obscureText: true,
onSaved: (InputValue val) { person.password = val.text; }
)
onSaved: (String value) { person.password = value; },
),
),
const SizedBox(width: 16.0),
new Expanded(
child: new TextField(
hintText: 'How do you log in?',
labelText: 'Re-type Password *',
child: new TextFormField(
decoration: const InputDecoration(
hintText: 'How do you log in?',
labelText: 'Re-type Password *',
),
obscureText: true,
validator: _validatePassword,
)
)
]
),
),
],
),
new Container(
padding: const EdgeInsets.all(20.0),
Expand All @@ -168,9 +178,9 @@ class TextFieldDemoState extends State<TextFieldDemo> {
padding: const EdgeInsets.only(top: 20.0),
child: new Text('* indicates required field', style: Theme.of(context).textTheme.caption),
),
]
],
)
)
),
);
}
}
4 changes: 2 additions & 2 deletions examples/flutter_gallery/lib/gallery/item.dart
Expand Up @@ -261,8 +261,8 @@ final List<GalleryItem> kAllGalleryItems = <GalleryItem>[
title: 'Text fields',
subtitle: 'Single line of editable text and numbers',
category: 'Material Components',
routeName: TextFieldDemo.routeName,
buildRoute: (BuildContext context) => new TextFieldDemo(),
routeName: TextFormFieldDemo.routeName,
buildRoute: (BuildContext context) => new TextFormFieldDemo(),
),
new GalleryItem(
title: 'Tooltips',
Expand Down

0 comments on commit ae89948

Please sign in to comment.