Added a Form widget to manage multiple Input widgets.
This commit is contained in:
parent
03830d5676
commit
a7b28a3ede
@ -11,14 +11,16 @@ class TextFieldDemo extends StatefulWidget {
|
|||||||
TextFieldDemoState createState() => new TextFieldDemoState();
|
TextFieldDemoState createState() => new TextFieldDemoState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PersonData {
|
||||||
|
String name;
|
||||||
|
String phoneNumber;
|
||||||
|
String password;
|
||||||
|
}
|
||||||
|
|
||||||
class TextFieldDemoState extends State<TextFieldDemo> {
|
class TextFieldDemoState extends State<TextFieldDemo> {
|
||||||
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
|
||||||
final List<InputValue> _inputs = <InputValue>[
|
|
||||||
InputValue.empty,
|
PersonData person = new PersonData();
|
||||||
InputValue.empty,
|
|
||||||
InputValue.empty,
|
|
||||||
InputValue.empty,
|
|
||||||
];
|
|
||||||
|
|
||||||
void showInSnackBar(String value) {
|
void showInSnackBar(String value) {
|
||||||
_scaffoldKey.currentState.showSnackBar(new SnackBar(
|
_scaffoldKey.currentState.showSnackBar(new SnackBar(
|
||||||
@ -26,36 +28,30 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleInputChanged(InputValue value, int which) {
|
void _handleSubmitted() {
|
||||||
setState(() {
|
showInSnackBar('${person.name}\'s phone number is ${person.phoneNumber}');
|
||||||
_inputs[which] = value;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleInputSubmitted(InputValue value) {
|
String _validateName(String value) {
|
||||||
showInSnackBar('${_inputs[0].text}\'s phone number is ${_inputs[1].text}');
|
if (value.isEmpty)
|
||||||
}
|
|
||||||
|
|
||||||
String _validateName(InputValue value) {
|
|
||||||
if (value.text.isEmpty)
|
|
||||||
return 'Name is required.';
|
return 'Name is required.';
|
||||||
RegExp nameExp = new RegExp(r'^[A-za-z ]+$');
|
RegExp nameExp = new RegExp(r'^[A-za-z ]+$');
|
||||||
if (!nameExp.hasMatch(value.text))
|
if (!nameExp.hasMatch(value))
|
||||||
return 'Please enter only alphabetical characters.';
|
return 'Please enter only alphabetical characters.';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _validatePhoneNumber(InputValue value) {
|
String _validatePhoneNumber(String value) {
|
||||||
RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$');
|
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 '###-###-#### - Please enter a valid phone number.';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _validatePassword(InputValue value1, InputValue value2) {
|
String _validatePassword(String value) {
|
||||||
if (value1.text.isEmpty)
|
if (person.password == null || person.password.isEmpty)
|
||||||
return 'Please choose a password.';
|
return 'Please choose a password.';
|
||||||
if (value1.text != value2.text)
|
if (person.password != value)
|
||||||
return 'Passwords don\'t match';
|
return 'Passwords don\'t match';
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -67,24 +63,27 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||||||
appBar: new AppBar(
|
appBar: new AppBar(
|
||||||
title: new Text('Text Fields')
|
title: new Text('Text Fields')
|
||||||
),
|
),
|
||||||
body: new Block(
|
body: new Form(
|
||||||
|
onSubmitted: _handleSubmitted,
|
||||||
|
child: new Block(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
new Input(
|
new Input(
|
||||||
hintText: 'What do people call you?',
|
hintText: 'What do people call you?',
|
||||||
labelText: 'Name',
|
labelText: 'Name',
|
||||||
errorText: _validateName(_inputs[0]),
|
formField: new FormField<String>(
|
||||||
value: _inputs[0],
|
// TODO(mpcomplete): replace with person#name=
|
||||||
onChanged: (InputValue value) { _handleInputChanged(value, 0); },
|
setter: (String val) { person.name = val; },
|
||||||
onSubmitted: _handleInputSubmitted
|
validator: _validateName
|
||||||
|
)
|
||||||
),
|
),
|
||||||
new Input(
|
new Input(
|
||||||
hintText: 'Where can we reach you?',
|
hintText: 'Where can we reach you?',
|
||||||
labelText: 'Phone Number',
|
labelText: 'Phone Number',
|
||||||
errorText: _validatePhoneNumber(_inputs[1]),
|
formField: new FormField<String>(
|
||||||
value: _inputs[1],
|
setter: (String val) { person.phoneNumber = val; },
|
||||||
onChanged: (InputValue value) { _handleInputChanged(value, 1); },
|
validator: _validatePhoneNumber
|
||||||
onSubmitted: _handleInputSubmitted
|
)
|
||||||
),
|
),
|
||||||
new Row(
|
new Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@ -94,26 +93,26 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||||||
hintText: 'How do you log in?',
|
hintText: 'How do you log in?',
|
||||||
labelText: 'New Password',
|
labelText: 'New Password',
|
||||||
hideText: true,
|
hideText: true,
|
||||||
value: _inputs[2],
|
formField: new FormField<String>(
|
||||||
onChanged: (InputValue value) { _handleInputChanged(value, 2); },
|
setter: (String val) { person.password = val; }
|
||||||
onSubmitted: _handleInputSubmitted
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
new Flexible(
|
new Flexible(
|
||||||
child: new Input(
|
child: new Input(
|
||||||
hintText: 'How do you log in?',
|
hintText: 'How do you log in?',
|
||||||
labelText: 'Re-type Password',
|
labelText: 'Re-type Password',
|
||||||
errorText: _validatePassword(_inputs[2], _inputs[3]),
|
|
||||||
hideText: true,
|
hideText: true,
|
||||||
value: _inputs[3],
|
formField: new FormField<String>(
|
||||||
onChanged: (InputValue value) { _handleInputChanged(value, 3); },
|
validator: _validatePassword
|
||||||
onSubmitted: _handleInputSubmitted
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ export 'package:sky_services/editing/editing.mojom.dart' show KeyboardType;
|
|||||||
class Input extends StatefulWidget {
|
class Input extends StatefulWidget {
|
||||||
Input({
|
Input({
|
||||||
Key key,
|
Key key,
|
||||||
this.value: InputValue.empty,
|
this.value,
|
||||||
this.keyboardType: KeyboardType.text,
|
this.keyboardType: KeyboardType.text,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.labelText,
|
this.labelText,
|
||||||
@ -27,6 +27,7 @@ class Input extends StatefulWidget {
|
|||||||
this.hideText: false,
|
this.hideText: false,
|
||||||
this.isDense: false,
|
this.isDense: false,
|
||||||
this.autofocus: false,
|
this.autofocus: false,
|
||||||
|
this.formField,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.onSubmitted
|
this.onSubmitted
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@ -61,6 +62,9 @@ class Input extends StatefulWidget {
|
|||||||
/// Whether this input field should focus itself is nothing else is already focused.
|
/// Whether this input field should focus itself is nothing else is already focused.
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
|
||||||
|
/// Form-specific data, required if this Input is part of a Form.
|
||||||
|
final FormField<String> formField;
|
||||||
|
|
||||||
/// Called when the text being edited changes.
|
/// Called when the text being edited changes.
|
||||||
final ValueChanged<InputValue> onChanged;
|
final ValueChanged<InputValue> onChanged;
|
||||||
|
|
||||||
@ -79,12 +83,24 @@ class _InputState extends State<Input> {
|
|||||||
|
|
||||||
GlobalKey get focusKey => config.key is GlobalKey ? config.key : _rawInputLineKey;
|
GlobalKey get focusKey => config.key is GlobalKey ? config.key : _rawInputLineKey;
|
||||||
|
|
||||||
|
// Optional state to retain if we are inside a Form widget.
|
||||||
|
_FormFieldData _formData;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(debugCheckHasMaterial(context));
|
assert(debugCheckHasMaterial(context));
|
||||||
ThemeData themeData = Theme.of(context);
|
ThemeData themeData = Theme.of(context);
|
||||||
BuildContext focusContext = focusKey.currentContext;
|
BuildContext focusContext = focusKey.currentContext;
|
||||||
bool focused = focusContext != null && Focus.at(focusContext, autofocus: config.autofocus);
|
bool focused = focusContext != null && Focus.at(focusContext, autofocus: config.autofocus);
|
||||||
|
if (_formData == null)
|
||||||
|
_formData = _FormFieldData.maybeCreate(context, this);
|
||||||
|
InputValue value = config.value ?? _formData?.value ?? InputValue.empty;
|
||||||
|
ValueChanged<InputValue> onChanged = config.onChanged ?? _formData?.onChanged;
|
||||||
|
ValueChanged<InputValue> onSubmitted = config.onSubmitted ?? _formData?.onSubmitted;
|
||||||
|
String errorText = config.errorText;
|
||||||
|
|
||||||
|
if (errorText == null && config.formField != null && config.formField.validator != null)
|
||||||
|
errorText = config.formField.validator(value.text);
|
||||||
|
|
||||||
TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
|
TextStyle textStyle = config.style ?? themeData.textTheme.subhead;
|
||||||
Color activeColor = themeData.hintColor;
|
Color activeColor = themeData.hintColor;
|
||||||
@ -102,7 +118,7 @@ class _InputState extends State<Input> {
|
|||||||
|
|
||||||
List<Widget> stackChildren = <Widget>[];
|
List<Widget> stackChildren = <Widget>[];
|
||||||
|
|
||||||
bool hasInlineLabel = config.labelText != null && !focused && !config.value.text.isNotEmpty;
|
bool hasInlineLabel = config.labelText != null && !focused && !value.text.isNotEmpty;
|
||||||
|
|
||||||
if (config.labelText != null) {
|
if (config.labelText != null) {
|
||||||
TextStyle labelStyle = hasInlineLabel ?
|
TextStyle labelStyle = hasInlineLabel ?
|
||||||
@ -125,7 +141,7 @@ class _InputState extends State<Input> {
|
|||||||
topPadding += topPaddingIncrement;
|
topPadding += topPaddingIncrement;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.hintText != null && config.value.text.isEmpty && !hasInlineLabel) {
|
if (config.hintText != null && value.text.isEmpty && !hasInlineLabel) {
|
||||||
TextStyle hintStyle = themeData.textTheme.subhead.copyWith(color: themeData.hintColor);
|
TextStyle hintStyle = themeData.textTheme.subhead.copyWith(color: themeData.hintColor);
|
||||||
stackChildren.add(new Positioned(
|
stackChildren.add(new Positioned(
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
@ -139,7 +155,7 @@ class _InputState extends State<Input> {
|
|||||||
Color borderColor = activeColor;
|
Color borderColor = activeColor;
|
||||||
double borderWidth = focused ? 2.0 : 1.0;
|
double borderWidth = focused ? 2.0 : 1.0;
|
||||||
|
|
||||||
if (config.errorText != null) {
|
if (errorText != null) {
|
||||||
borderColor = themeData.errorColor;
|
borderColor = themeData.errorColor;
|
||||||
borderWidth = 2.0;
|
borderWidth = 2.0;
|
||||||
if (!config.isDense) {
|
if (!config.isDense) {
|
||||||
@ -163,24 +179,24 @@ class _InputState extends State<Input> {
|
|||||||
),
|
),
|
||||||
child: new RawInputLine(
|
child: new RawInputLine(
|
||||||
key: _rawInputLineKey,
|
key: _rawInputLineKey,
|
||||||
value: config.value,
|
value: value,
|
||||||
focusKey: focusKey,
|
focusKey: focusKey,
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
hideText: config.hideText,
|
hideText: config.hideText,
|
||||||
cursorColor: themeData.selectionColor,
|
cursorColor: themeData.selectionColor,
|
||||||
selectionColor: themeData.selectionColor,
|
selectionColor: themeData.selectionColor,
|
||||||
keyboardType: config.keyboardType,
|
keyboardType: config.keyboardType,
|
||||||
onChanged: config.onChanged,
|
onChanged: onChanged,
|
||||||
onSubmitted: config.onSubmitted
|
onSubmitted: onSubmitted
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
|
|
||||||
if (config.errorText != null && !config.isDense) {
|
if (errorText != null && !config.isDense) {
|
||||||
TextStyle errorStyle = themeData.textTheme.caption.copyWith(color: themeData.errorColor);
|
TextStyle errorStyle = themeData.textTheme.caption.copyWith(color: themeData.errorColor);
|
||||||
stackChildren.add(new Positioned(
|
stackChildren.add(new Positioned(
|
||||||
left: 0.0,
|
left: 0.0,
|
||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
child: new Text(config.errorText, style: errorStyle)
|
child: new Text(errorText, style: errorStyle)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,3 +232,36 @@ class _InputState extends State<Input> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _FormFieldData {
|
||||||
|
_FormFieldData(this.inputState) {
|
||||||
|
assert(field != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
InputValue value = new InputValue();
|
||||||
|
final _InputState inputState;
|
||||||
|
FormField<String> get field => inputState.config.formField;
|
||||||
|
|
||||||
|
static _FormFieldData maybeCreate(BuildContext context, _InputState inputState) {
|
||||||
|
// Only create a _FormFieldData if this Input is a descendent of a Form.
|
||||||
|
if (FormScope.of(context) != null)
|
||||||
|
return new _FormFieldData(inputState);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onChanged(InputValue value) {
|
||||||
|
FormScope scope = FormScope.of(inputState.context);
|
||||||
|
assert(scope != null);
|
||||||
|
this.value = value;
|
||||||
|
if (field.setter != null)
|
||||||
|
field.setter(value.text);
|
||||||
|
scope.onFieldChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSubmitted(InputValue value) {
|
||||||
|
FormScope scope = FormScope.of(inputState.context);
|
||||||
|
assert(scope != null);
|
||||||
|
scope.form.onSubmitted();
|
||||||
|
scope.onFieldChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
100
packages/flutter/lib/src/widgets/form.dart
Normal file
100
packages/flutter/lib/src/widgets/form.dart
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'basic.dart';
|
||||||
|
import 'framework.dart';
|
||||||
|
|
||||||
|
/// A container for grouping together multiple form field widgets (e.g. Input).
|
||||||
|
class Form extends StatefulWidget {
|
||||||
|
Form({
|
||||||
|
Key key,
|
||||||
|
this.child,
|
||||||
|
this.onSubmitted
|
||||||
|
}) : super(key: key) {
|
||||||
|
assert(child != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called when the input is accepted anywhere on the form.
|
||||||
|
final VoidCallback onSubmitted;
|
||||||
|
|
||||||
|
/// Root of the widget hierarchy that contains this form.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_FormState createState() => new _FormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FormState extends State<Form> {
|
||||||
|
int generation = 0;
|
||||||
|
|
||||||
|
void onFieldChanged() {
|
||||||
|
setState(() {
|
||||||
|
++generation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return new FormScope(
|
||||||
|
state: this,
|
||||||
|
generation: generation,
|
||||||
|
child: config.child
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef String FormFieldValidator<T>(T value);
|
||||||
|
typedef void FormFieldSetter<T>(T newValue);
|
||||||
|
|
||||||
|
/// This contains identifying information for Input fields, required if the
|
||||||
|
/// Input is part of a Form.
|
||||||
|
class FormField<T> {
|
||||||
|
FormField({
|
||||||
|
this.setter,
|
||||||
|
this.validator
|
||||||
|
});
|
||||||
|
|
||||||
|
/// An optional method to call with the new value when the form field changes.
|
||||||
|
final FormFieldSetter<T> setter;
|
||||||
|
|
||||||
|
/// An optional method that validates an input. Returns an error string to
|
||||||
|
/// display if the input is invalid, or null otherwise.
|
||||||
|
final FormFieldValidator<T> validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The root of all Forms. Used by form field widgets (e.g. Input) to
|
||||||
|
/// communicate changes back to the client.
|
||||||
|
class FormScope extends InheritedWidget {
|
||||||
|
FormScope({
|
||||||
|
Key key,
|
||||||
|
Widget child,
|
||||||
|
_FormState state,
|
||||||
|
int generation
|
||||||
|
}) : _state = state,
|
||||||
|
_generation = generation,
|
||||||
|
super(key: key, child: child);
|
||||||
|
|
||||||
|
final _FormState _state;
|
||||||
|
|
||||||
|
/// Incremented every time a form field has changed. This lets us know when
|
||||||
|
/// to rebuild the form.
|
||||||
|
final int _generation;
|
||||||
|
|
||||||
|
/// The Form this widget belongs to.
|
||||||
|
Form get form => _state.config;
|
||||||
|
|
||||||
|
/// Finds the FormScope that encloses the widget being built from the given
|
||||||
|
/// context.
|
||||||
|
static FormScope of(BuildContext context) {
|
||||||
|
return context.inheritFromWidgetOfExactType(FormScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use this to notify the Form that a form field has changed. This will
|
||||||
|
/// cause all form fields to rebuild, useful if form fields have
|
||||||
|
/// interdependencies.
|
||||||
|
void onFieldChanged() => _state.onFieldChanged();
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(FormScope old) => _generation != old._generation;
|
||||||
|
}
|
@ -17,6 +17,7 @@ export 'src/widgets/dismissable.dart';
|
|||||||
export 'src/widgets/drag_target.dart';
|
export 'src/widgets/drag_target.dart';
|
||||||
export 'src/widgets/editable.dart';
|
export 'src/widgets/editable.dart';
|
||||||
export 'src/widgets/focus.dart';
|
export 'src/widgets/focus.dart';
|
||||||
|
export 'src/widgets/form.dart';
|
||||||
export 'src/widgets/framework.dart';
|
export 'src/widgets/framework.dart';
|
||||||
export 'src/widgets/gesture_detector.dart';
|
export 'src/widgets/gesture_detector.dart';
|
||||||
export 'src/widgets/gridpaper.dart';
|
export 'src/widgets/gridpaper.dart';
|
||||||
|
168
packages/flutter/test/widget/form_test.dart
Normal file
168
packages/flutter/test/widget/form_test.dart
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
// Copyright 2016 The Chromium Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sky_services/editing/editing.mojom.dart' as mojom;
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
class MockKeyboard implements mojom.Keyboard {
|
||||||
|
mojom.KeyboardClient client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setClient(mojom.KeyboardClientStub client, mojom.KeyboardConfiguration configuraiton) {
|
||||||
|
this.client = client.impl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void show() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void hide() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setEditingState(mojom.EditingState state) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
WidgetFlutterBinding.ensureInitialized(); // for serviceMocker
|
||||||
|
MockKeyboard mockKeyboard = new MockKeyboard();
|
||||||
|
serviceMocker.registerMockService(mojom.Keyboard.serviceName, mockKeyboard);
|
||||||
|
|
||||||
|
void enterText(String testValue) {
|
||||||
|
// Simulate entry of text through the keyboard.
|
||||||
|
expect(mockKeyboard.client, isNotNull);
|
||||||
|
mockKeyboard.client.updateEditingState(new mojom.EditingState()
|
||||||
|
..text = testValue
|
||||||
|
..composingBase = 0
|
||||||
|
..composingExtent = testValue.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Setter callback is called', () {
|
||||||
|
testWidgets((WidgetTester tester) {
|
||||||
|
GlobalKey inputKey = new GlobalKey();
|
||||||
|
String fieldValue;
|
||||||
|
|
||||||
|
Widget builder() {
|
||||||
|
return new Center(
|
||||||
|
child: new Material(
|
||||||
|
child: new Form(
|
||||||
|
child: new Input(
|
||||||
|
key: inputKey,
|
||||||
|
formField: new FormField<String>(
|
||||||
|
setter: (String val) { fieldValue = val; }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tester.pumpWidget(builder());
|
||||||
|
|
||||||
|
void checkText(String testValue) {
|
||||||
|
enterText(testValue);
|
||||||
|
|
||||||
|
// Check that the FormField's setter was called.
|
||||||
|
expect(fieldValue, equals(testValue));
|
||||||
|
tester.pumpWidget(builder());
|
||||||
|
}
|
||||||
|
|
||||||
|
checkText('Test');
|
||||||
|
checkText('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Validator sets the error text', () {
|
||||||
|
testWidgets((WidgetTester tester) {
|
||||||
|
GlobalKey inputKey = new GlobalKey();
|
||||||
|
String errorText(String input) => input + '/error';
|
||||||
|
|
||||||
|
Widget builder() {
|
||||||
|
return new Center(
|
||||||
|
child: new Material(
|
||||||
|
child: new Form(
|
||||||
|
child: new Input(
|
||||||
|
key: inputKey,
|
||||||
|
formField: new FormField<String>(
|
||||||
|
validator: errorText
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tester.pumpWidget(builder());
|
||||||
|
|
||||||
|
void checkErrorText(String testValue) {
|
||||||
|
enterText(testValue);
|
||||||
|
tester.pumpWidget(builder());
|
||||||
|
|
||||||
|
// Check for a new Text widget with our error text.
|
||||||
|
Element errorElement = tester.findText(errorText(testValue));
|
||||||
|
expect(errorElement, isNotNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkErrorText('Test');
|
||||||
|
checkErrorText('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple Inputs communicate', () {
|
||||||
|
testWidgets((WidgetTester tester) {
|
||||||
|
GlobalKey inputKey = new GlobalKey();
|
||||||
|
GlobalKey focusKey = new GlobalKey();
|
||||||
|
// Input 1's text value.
|
||||||
|
String fieldValue;
|
||||||
|
// Input 2's validator depends on a input 1's value.
|
||||||
|
String errorText(String input) => fieldValue.toString() + '/error';
|
||||||
|
|
||||||
|
Widget builder() {
|
||||||
|
return new Center(
|
||||||
|
child: new Material(
|
||||||
|
child: new Form(
|
||||||
|
child: new Focus(
|
||||||
|
key: focusKey,
|
||||||
|
child: new Block(
|
||||||
|
children: <Widget>[
|
||||||
|
new Input(
|
||||||
|
key: inputKey,
|
||||||
|
formField: new FormField<String>(
|
||||||
|
setter: (String val) { fieldValue = val; }
|
||||||
|
)
|
||||||
|
),
|
||||||
|
new Input(
|
||||||
|
formField: new FormField<String>(
|
||||||
|
validator: errorText
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tester.pumpWidget(builder());
|
||||||
|
Focus.moveTo(inputKey);
|
||||||
|
tester.pump();
|
||||||
|
|
||||||
|
void checkErrorText(String testValue) {
|
||||||
|
enterText(testValue);
|
||||||
|
tester.pumpWidget(builder());
|
||||||
|
|
||||||
|
expect(fieldValue, equals(testValue));
|
||||||
|
|
||||||
|
// Check for a new Text widget with our error text.
|
||||||
|
Element errorElement = tester.findText(errorText(testValue));
|
||||||
|
expect(errorElement, isNotNull);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkErrorText('Test');
|
||||||
|
checkErrorText('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user