1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/gestures.dart' show DragStartBehavior;
6import 'package:flutter/material.dart';
7import 'package:flutter/services.dart';
8
9import '../../gallery/demo.dart';
10
11class TextFormFieldDemo extends StatefulWidget {
12 const TextFormFieldDemo({super.key});
13
14 static const String routeName = '/material/text-form-field';
15
16 @override
17 TextFormFieldDemoState createState() => TextFormFieldDemoState();
18}
19
20class PersonData {
21 String? name = '';
22 String? phoneNumber = '';
23 String? email = '';
24 String password = '';
25}
26
27class PasswordField extends StatefulWidget {
28 const PasswordField({
29 super.key,
30 this.fieldKey,
31 this.hintText,
32 this.labelText,
33 this.helperText,
34 this.onSaved,
35 this.validator,
36 this.onFieldSubmitted,
37 });
38
39 final Key? fieldKey;
40 final String? hintText;
41 final String? labelText;
42 final String? helperText;
43 final FormFieldSetter<String>? onSaved;
44 final FormFieldValidator<String>? validator;
45 final ValueChanged<String>? onFieldSubmitted;
46
47 @override
48 State<PasswordField> createState() => _PasswordFieldState();
49}
50
51class _PasswordFieldState extends State<PasswordField> {
52 bool _obscureText = true;
53
54 @override
55 Widget build(BuildContext context) {
56 return TextFormField(
57 key: widget.fieldKey,
58 obscureText: _obscureText,
59 maxLength: 8,
60 onSaved: widget.onSaved,
61 validator: widget.validator,
62 onFieldSubmitted: widget.onFieldSubmitted,
63 decoration: InputDecoration(
64 border: const UnderlineInputBorder(),
65 filled: true,
66 hintText: widget.hintText,
67 labelText: widget.labelText,
68 helperText: widget.helperText,
69 suffixIcon: GestureDetector(
70 dragStartBehavior: DragStartBehavior.down,
71 onTap: () {
72 setState(() {
73 _obscureText = !_obscureText;
74 });
75 },
76 child: Icon(
77 _obscureText ? Icons.visibility : Icons.visibility_off,
78 semanticLabel: _obscureText ? 'show password' : 'hide password',
79 ),
80 ),
81 ),
82 );
83 }
84}
85
86class TextFormFieldDemoState extends State<TextFormFieldDemo> {
87 PersonData person = PersonData();
88
89 void showInSnackBar(String value) {
90 ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(value)));
91 }
92
93 AutovalidateMode _autovalidateMode = AutovalidateMode.disabled;
94 bool _formWasEdited = false;
95
96 final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
97 final GlobalKey<FormFieldState<String>> _passwordFieldKey = GlobalKey<FormFieldState<String>>();
98 final _UsNumberTextInputFormatter _phoneNumberFormatter = _UsNumberTextInputFormatter();
99 void _handleSubmitted() {
100 final FormState form = _formKey.currentState!;
101 if (!form.validate()) {
102 _autovalidateMode = AutovalidateMode.always; // Start validating on every change.
103 showInSnackBar('Please fix the errors in red before submitting.');
104 } else {
105 form.save();
106 showInSnackBar("${person.name}'s phone number is ${person.phoneNumber}");
107 }
108 }
109
110 String? _validateName(String? value) {
111 _formWasEdited = true;
112 if (value!.isEmpty) {
113 return 'Name is required.';
114 }
115 final RegExp nameExp = RegExp(r'^[A-Za-z ]+$');
116 if (!nameExp.hasMatch(value)) {
117 return 'Please enter only alphabetical characters.';
118 }
119 return null;
120 }
121
122 String? _validatePhoneNumber(String? value) {
123 _formWasEdited = true;
124 final RegExp phoneExp = RegExp(r'^\(\d\d\d\) \d\d\d\-\d\d\d\d$');
125 if (!phoneExp.hasMatch(value!)) {
126 return '(###) ###-#### - Enter a US phone number.';
127 }
128 return null;
129 }
130
131 String? _validatePassword(String? value) {
132 _formWasEdited = true;
133 final FormFieldState<String> passwordField = _passwordFieldKey.currentState!;
134 if (passwordField.value == null || passwordField.value!.isEmpty) {
135 return 'Please enter a password.';
136 }
137 if (passwordField.value != value) {
138 return "The passwords don't match";
139 }
140 return null;
141 }
142
143 Future<void> _handlePopInvoked(bool didPop, Object? result) async {
144 if (didPop) {
145 return;
146 }
147
148 final bool? result = await showDialog<bool>(
149 context: context,
150 builder: (BuildContext context) {
151 return AlertDialog(
152 title: const Text('This form has errors'),
153 content: const Text('Really leave this form?'),
154 actions: <Widget>[
155 TextButton(
156 child: const Text('YES'),
157 onPressed: () {
158 Navigator.of(context).pop(true);
159 },
160 ),
161 TextButton(
162 child: const Text('NO'),
163 onPressed: () {
164 Navigator.of(context).pop(false);
165 },
166 ),
167 ],
168 );
169 },
170 );
171
172 if (result ?? false) {
173 // Since this is the root route, quit the app where possible by invoking
174 // the SystemNavigator. If this wasn't the root route, then
175 // Navigator.maybePop could be used instead.
176 // See https://github.com/flutter/flutter/issues/11490
177 SystemNavigator.pop();
178 }
179 }
180
181 @override
182 Widget build(BuildContext context) {
183 return Scaffold(
184 drawerDragStartBehavior: DragStartBehavior.down,
185 appBar: AppBar(
186 title: const Text('Text fields'),
187 actions: <Widget>[MaterialDemoDocumentationButton(TextFormFieldDemo.routeName)],
188 ),
189 body: SafeArea(
190 top: false,
191 bottom: false,
192 child: Form(
193 key: _formKey,
194 autovalidateMode: _autovalidateMode,
195 canPop:
196 _formKey.currentState == null || !_formWasEdited || _formKey.currentState!.validate(),
197 onPopInvokedWithResult: _handlePopInvoked,
198 child: Scrollbar(
199 child: SingleChildScrollView(
200 primary: true,
201 dragStartBehavior: DragStartBehavior.down,
202 padding: const EdgeInsets.symmetric(horizontal: 16.0),
203 child: Column(
204 crossAxisAlignment: CrossAxisAlignment.stretch,
205 children: <Widget>[
206 const SizedBox(height: 24.0),
207 TextFormField(
208 textCapitalization: TextCapitalization.words,
209 decoration: const InputDecoration(
210 border: UnderlineInputBorder(),
211 filled: true,
212 icon: Icon(Icons.person),
213 hintText: 'What do people call you?',
214 labelText: 'Name *',
215 ),
216 onSaved: (String? value) {
217 person.name = value;
218 },
219 validator: _validateName,
220 ),
221 const SizedBox(height: 24.0),
222 TextFormField(
223 decoration: const InputDecoration(
224 border: UnderlineInputBorder(),
225 filled: true,
226 icon: Icon(Icons.phone),
227 hintText: 'Where can we reach you?',
228 labelText: 'Phone Number *',
229 prefixText: '+1',
230 ),
231 keyboardType: TextInputType.phone,
232 onSaved: (String? value) {
233 person.phoneNumber = value;
234 },
235 validator: _validatePhoneNumber,
236 // TextInputFormatters are applied in sequence.
237 inputFormatters: <TextInputFormatter>[
238 FilteringTextInputFormatter.digitsOnly,
239 // Fit the validating format.
240 _phoneNumberFormatter,
241 ],
242 ),
243 const SizedBox(height: 24.0),
244 TextFormField(
245 decoration: const InputDecoration(
246 border: UnderlineInputBorder(),
247 filled: true,
248 icon: Icon(Icons.email),
249 hintText: 'Your email address',
250 labelText: 'E-mail',
251 ),
252 keyboardType: TextInputType.emailAddress,
253 onSaved: (String? value) {
254 person.email = value;
255 },
256 ),
257 const SizedBox(height: 24.0),
258 TextFormField(
259 decoration: const InputDecoration(
260 border: OutlineInputBorder(),
261 hintText:
262 'Tell us about yourself (e.g., write down what you do or what hobbies you have)',
263 helperText: 'Keep it short, this is just a demo.',
264 labelText: 'Life story',
265 ),
266 maxLines: 3,
267 ),
268 const SizedBox(height: 24.0),
269 TextFormField(
270 keyboardType: TextInputType.number,
271 decoration: const InputDecoration(
272 border: OutlineInputBorder(),
273 labelText: 'Salary',
274 prefixText: r'$',
275 suffixText: 'USD',
276 suffixStyle: TextStyle(color: Colors.green),
277 ),
278 ),
279 const SizedBox(height: 24.0),
280 PasswordField(
281 fieldKey: _passwordFieldKey,
282 helperText: 'No more than 8 characters.',
283 labelText: 'Password *',
284 onFieldSubmitted: (String value) {
285 setState(() {
286 person.password = value;
287 });
288 },
289 ),
290 const SizedBox(height: 24.0),
291 TextFormField(
292 enabled: person.password.isNotEmpty,
293 decoration: const InputDecoration(
294 border: UnderlineInputBorder(),
295 filled: true,
296 labelText: 'Re-type password',
297 ),
298 maxLength: 8,
299 obscureText: true,
300 validator: _validatePassword,
301 ),
302 const SizedBox(height: 24.0),
303 Center(
304 child: ElevatedButton(onPressed: _handleSubmitted, child: const Text('SUBMIT')),
305 ),
306 const SizedBox(height: 24.0),
307 Text('* indicates required field', style: Theme.of(context).textTheme.bodySmall),
308 const SizedBox(height: 24.0),
309 ],
310 ),
311 ),
312 ),
313 ),
314 ),
315 );
316 }
317}
318
319/// Format incoming numeric text to fit the format of (###) ###-#### ##...
320class _UsNumberTextInputFormatter extends TextInputFormatter {
321 @override
322 TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
323 final int newTextLength = newValue.text.length;
324 int selectionIndex = newValue.selection.end;
325 int usedSubstringIndex = 0;
326 final StringBuffer newText = StringBuffer();
327 if (newTextLength >= 1) {
328 newText.write('(');
329 if (newValue.selection.end >= 1) {
330 selectionIndex++;
331 }
332 }
333 if (newTextLength >= 4) {
334 final String value = newValue.text.substring(0, usedSubstringIndex = 3);
335 newText.write('$value) ');
336 if (newValue.selection.end >= 3) {
337 selectionIndex += 2;
338 }
339 }
340 if (newTextLength >= 7) {
341 final String value = newValue.text.substring(3, usedSubstringIndex = 6);
342 newText.write('$value-');
343 if (newValue.selection.end >= 6) {
344 selectionIndex++;
345 }
346 }
347 if (newTextLength >= 11) {
348 final String value = newValue.text.substring(6, usedSubstringIndex = 10);
349 newText.write('$value ');
350 if (newValue.selection.end >= 10) {
351 selectionIndex++;
352 }
353 }
354 // Dump the rest.
355 if (newTextLength >= usedSubstringIndex) {
356 newText.write(newValue.text.substring(usedSubstringIndex));
357 }
358 return TextEditingValue(
359 text: newText.toString(),
360 selection: TextSelection.collapsed(offset: selectionIndex),
361 );
362 }
363}
364

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com