Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class TextFormFieldDemo extends StatefulWidget {
const TextFormFieldDemo({ Key key }) : super(key: key);
Expand Down Expand Up @@ -36,6 +37,7 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
bool _formWasEdited = false;
final GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
final GlobalKey<FormFieldState<String>> _passwordFieldKey = new GlobalKey<FormFieldState<String>>();
final _UsNumberTextInputFormatter _phoneNumberFormatter = new _UsNumberTextInputFormatter();
void _handleSubmitted() {
final FormState form = _formKey.currentState;
if (!form.validate()) {
Expand All @@ -59,9 +61,9 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {

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

Expand Down Expand Up @@ -131,6 +133,12 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
keyboardType: TextInputType.phone,
onSaved: (String value) { person.phoneNumber = value; },
validator: _validatePhoneNumber,
// TextInputFormatters are applied in sequence.
inputFormatters: <TextInputFormatter> [
WhitelistingTextInputFormatter.digitsOnly,
// Fit the validating format.
_phoneNumberFormatter,
],
),
new TextFormField(
decoration: const InputDecoration(
Expand Down Expand Up @@ -184,3 +192,40 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
);
}
}

/// Format incoming numeric text to fit the format of (###) ###-#### ##...
class _UsNumberTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue
) {
final int newTextLength = newValue.text.length;
int selectionIndex = newValue.selection.end;
int usedSubstringIndex = 0;
final StringBuffer newText = new StringBuffer();
if (newTextLength >= 1) {
newText.write('(');
if (newValue.selection.end >= 1) selectionIndex++;
}
if (newTextLength >= 4) {
newText.write(newValue.text.substring(0, usedSubstringIndex = 3) + ') ');
if (newValue.selection.end >= 3) selectionIndex += 2;
}
if (newTextLength >= 7) {
newText.write(newValue.text.substring(3, usedSubstringIndex = 6) + '-');
if (newValue.selection.end >= 6) selectionIndex++;
}
if (newTextLength >= 11) {
newText.write(newValue.text.substring(6, usedSubstringIndex = 10) + ' ');
if (newValue.selection.end >= 10) selectionIndex++;
}
// Dump the rest.
if (newTextLength >= usedSubstringIndex)
newText.write(newValue.text.substring(usedSubstringIndex));
return new TextEditingValue(
text: newText.toString(),
selection: new TextSelection.collapsed(offset: selectionIndex),
);
}
}
1 change: 1 addition & 0 deletions packages/flutter/lib/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ export 'src/services/system_chrome.dart';
export 'src/services/system_navigator.dart';
export 'src/services/system_sound.dart';
export 'src/services/text_editing.dart';
export 'src/services/text_formatter.dart';
export 'src/services/text_input.dart';
export 'src/services/url_launcher.dart';
6 changes: 6 additions & 0 deletions packages/flutter/lib/src/material/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class TextField extends StatefulWidget {
this.maxLines: 1,
this.onChanged,
this.onSubmitted,
this.inputFormatters,
}) : super(key: key);

/// Controls the text being edited.
Expand Down Expand Up @@ -141,6 +142,10 @@ class TextField extends StatefulWidget {
/// field.
final ValueChanged<String> onSubmitted;

/// Optional input validation and formatting overrides. Formatters are run
/// in the provided order when the text input changes.
final List<TextInputFormatter> inputFormatters;

@override
_TextFieldState createState() => new _TextFieldState();

Expand Down Expand Up @@ -223,6 +228,7 @@ class _TextFieldState extends State<TextField> {
selectionControls: materialTextSelectionControls,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
inputFormatters: widget.inputFormatters,
),
);

Expand Down
2 changes: 2 additions & 0 deletions packages/flutter/lib/src/material/text_form_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class TextFormField extends FormField<String> {
int maxLines: 1,
FormFieldSetter<String> onSaved,
FormFieldValidator<String> validator,
List<TextInputFormatter> inputFormatters,
}) : super(
key: key,
initialValue: controller != null ? controller.value.text : '',
Expand All @@ -59,6 +60,7 @@ class TextFormField extends FormField<String> {
obscureText: obscureText,
maxLines: maxLines,
onChanged: field.onChanged,
inputFormatters: inputFormatters,
);
},
);
Expand Down
16 changes: 16 additions & 0 deletions packages/flutter/lib/src/services/text_editing.dart
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,20 @@ class TextSelection extends TextRange {
affinity.hashCode,
isDirectional.hashCode
);

/// Creates a new [TextSelection] based on the current selection, with the
/// provided parameters overridden.
TextSelection copyWith({
int baseOffset,
int extentOffset,
TextAffinity affinity,
bool isDirectional,
}) {
return new TextSelection(
baseOffset: baseOffset ?? this.baseOffset,
extentOffset: extentOffset ?? this.extentOffset,
affinity: affinity ?? this.affinity,
isDirectional: isDirectional ?? this.isDirectional,
);
}
}
194 changes: 194 additions & 0 deletions packages/flutter/lib/src/services/text_formatter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Copyright 2017 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/services.dart';

/// A [TextInputFormatter] can be optionally injected into an [EditableText]
/// to provide as-you-type validation and formatting of the text being edited.
///
/// Text modification should only be applied when text is being committed by the
/// IME and not on text under composition (i.e. when
/// [TextEditingValue.composing] is collapsed).
///
/// Concrete implementations [BlacklistingTextInputFormatter], which removes
/// blacklisted characters upon edit commit, and
/// [WhitelistingTextInputFormatter], which only allows entries of whitelisted
/// characters, are provided.
///
/// To create custom formatters, extend the [TextInputFormatter] class and
/// implement the [formatEditUpdate] method.
///
/// See also:
///
/// * [EditableText] on which the formatting apply.
/// * [BlacklistingTextInputFormatter], a provided formatter for blacklisting
/// characters.
/// * [WhitelistingTextInputFormatter], a provided formatter for whitelisting
/// characters.
abstract class TextInputFormatter {
/// Called when text is being typed or cut/copy/pasted in the [EditableText].
///
/// You can override the resulting text based on the previous text value and
/// the incoming new text value.
///
/// When formatters are chained, `oldValue` reflects the initial value of
/// [TextEditingValue] at the beginning of the chain.
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue
);

/// A shorthand to creating a custom [TextInputFormatter] which formats
/// incoming text input changes with the given function.
static TextInputFormatter withFunction(
TextInputFormatFunction formatFunction
) {
return new _SimpleTextInputFormatter(formatFunction);
}
}

/// Function signature expected for creating custom [TextInputFormatter]
/// shorthands via [TextInputFormatter.withFunction];
typedef TextEditingValue TextInputFormatFunction(
TextEditingValue oldValue,
TextEditingValue newValue,
);

/// Wiring for [TextInputFormatter.withFunction].
class _SimpleTextInputFormatter extends TextInputFormatter {
_SimpleTextInputFormatter(this.formatFunction) :
assert(formatFunction != null);

final TextInputFormatFunction formatFunction;

@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue
) {
return formatFunction(oldValue, newValue);
}
}

/// A [TextInputFormatter] that prevents the insertion of blacklisted
/// characters patterns.
///
/// Instances of blacklisted characters found in the new [TextEditingValue]s
/// will be replaced with the [replacementString] which defaults to ``.
///
/// Since this formatter only removes characters from the text, it attempts to
/// preserve the existing [TextEditingValue.selection] to values it would now
/// fall at with the removed characters.
///
/// See also:
///
/// * [TextInputFormatter].
/// * [WhitelistingTextInputFormatter].
class BlacklistingTextInputFormatter extends TextInputFormatter {
BlacklistingTextInputFormatter(
this.blacklistedPattern,
{
this.replacementString: '',
}
) : assert(blacklistedPattern != null);

/// A [Pattern] to match and replace incoming [TextEditingValue]s.
final Pattern blacklistedPattern;

/// String used to replace found patterns.
final String replacementString;

@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, // unused.
TextEditingValue newValue,
) {
return _selectionAwareTextManipulation(
newValue,
(String substring) {
return substring.replaceAll(blacklistedPattern, replacementString);
},
);
}

/// A [BlacklistingTextInputFormatter] that forces input to be a single line.
static final BlacklistingTextInputFormatter singleLineFormatter
= new BlacklistingTextInputFormatter(new RegExp(r'\n'));
}

/// A [TextInputFormatter] that allows only the insertion of whitelisted
/// characters patterns.
///
/// Since this formatter only removes characters from the text, it attempts to
/// preserve the existing [TextEditingValue.selection] to values it would now
/// fall at with the removed characters.
///
/// See also:
///
/// * [TextInputFormatter].
/// * [BlacklistingTextInputFormatter].
class WhitelistingTextInputFormatter extends TextInputFormatter {
WhitelistingTextInputFormatter(this.whitelistedPattern) :
assert(whitelistedPattern != null);

/// A [Pattern] to extract all instances of allowed characters.
///
/// [RegExp] with multiple groups is not supported.
final Pattern whitelistedPattern;

@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue, // unused.
TextEditingValue newValue,
) {
return _selectionAwareTextManipulation(
newValue,
(String substring) {
return whitelistedPattern
.allMatches(substring)
.map((Match match) => match.group(0))
.join();
} ,
);
}

/// A [WhitelistingTextInputFormatter] that takes in digits `[0-9]` only.
static final WhitelistingTextInputFormatter digitsOnly
= new WhitelistingTextInputFormatter(new RegExp(r'\d+'));
}

TextEditingValue _selectionAwareTextManipulation(
TextEditingValue value,
String substringManipulation(String substring),
) {
final int selectionStartIndex = value.selection.start;
final int selectionEndIndex = value.selection.end;
String manipulatedText;
TextSelection manipulatedSelection;
if (selectionStartIndex < 0 || selectionEndIndex < 0) {
manipulatedText = substringManipulation(value.text);
} else {
final String beforeSelection = substringManipulation(
value.text.substring(0, selectionStartIndex)
);
final String inSelection = substringManipulation(
value.text.substring(selectionStartIndex, selectionEndIndex)
);
final String afterSelection = substringManipulation(
value.text.substring(selectionEndIndex)
);
manipulatedText = beforeSelection + inSelection + afterSelection;
manipulatedSelection = value.selection.copyWith(
baseOffset: beforeSelection.length,
extentOffset: beforeSelection.length + inSelection.length,
);
}
return new TextEditingValue(
text: manipulatedText,
selection: manipulatedSelection ?? const TextSelection.collapsed(offset: -1),
composing: manipulatedText == value.text
? value.composing
: TextRange.empty,
);
}
Loading