diff --git a/CHANGELOG.md b/CHANGELOG.md index d560c243..e1526441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [4.3.0] 18 / 10 / 2021 +- Added most of textfield params to the phone input. +- Added method to select the current national number from the controller +- Changed how controllers worked under the hood +- Fix an issue where a phone number could not start with its country code +- uses phone_numbers_parser v4.1.0 + ## [4.2.0] 16 / 10 / 2021 - [deprecated] PhoneValidator.invalid in favor of PhoneValidator.valid as the naming did not make sens and was backward. diff --git a/README.md b/README.md index 26a1f048..d58f52a4 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,6 @@ PhoneFormField( controller: null, // controller & initialValue value initialValue: null, // can't be supplied simultaneously shouldFormat: true // default - autofocus: false, // default - autofillHints: [AutofillHints.telephoneNumber], // default to null defaultCountry: 'US', // default decoration: InputDecoration( labelText: 'Phone', // default to null @@ -41,14 +39,15 @@ PhoneFormField( ), validator: PhoneValidator.validMobile(), // default PhoneValidator.valid() selectorNavigator: const BottomSheetNavigator(), // default to bottom sheet but you can customize how the selector is shown by extending CountrySelectorNavigator - enabled: true, // default showFlagInInput: true, // default flagSize: 16, // default + autofillHints: [AutofillHints.telephoneNumber], // default to null + enabled: true, // default + autofocus: false, // default autovalidateMode: AutovalidateMode.onUserInteraction, // default - cursorColor: Theme.of(context).colorScheme.primary, // default null onSaved: (PhoneNumber p) => print('saved $p'), // default null onChanged: (PhoneNumber p) => print('saved $p'), // default null - restorationId: 'phoneRestorationId' + // ... + other textfield params ) ``` diff --git a/example/android/build.gradle b/example/android/build.gradle index 5560710b..fa430533 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.0.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 571984ad..a359f099 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip diff --git a/example/lib/main.dart b/example/lib/main.dart index d49503c3..74fb6ca2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -71,6 +71,8 @@ class PhoneFieldView extends StatelessWidget { } class MyApp extends StatelessWidget { + MyApp(); + @override Widget build(BuildContext context) { return MaterialApp( @@ -225,22 +227,34 @@ class _PhoneFormFieldScreenState extends State { ), ), SizedBox( - height: 40, + height: 12, ), + Text(controller.value.toString()), + Text('is valid mobile number ' + '${controller.value?.validate(type: PhoneNumberType.mobile) ?? 'false'}'), + Text( + 'is valid fixed line number ${controller.value?.validate(type: PhoneNumberType.fixedLine) ?? 'false'}'), + SizedBox(height: 12), ElevatedButton( onPressed: controller.value == null ? null - : () => formKey.currentState?.reset(), + : () => controller.reset(), child: Text('reset'), ), - SizedBox( - height: 40, + SizedBox(height: 12), + ElevatedButton( + onPressed: () => controller.selectNationalNumber(), + child: Text('Select national number'), + ), + SizedBox(height: 12), + ElevatedButton( + onPressed: () => + controller.value = PhoneNumber.fromIsoCode( + 'fr', + '699999999', + ), + child: Text('Set +33 699 999 999'), ), - Text(controller.value.toString()), - Text('is valid mobile number ' - '${controller.value?.validate(type: PhoneNumberType.mobile) ?? 'false'}'), - Text( - 'is valid fixed line number ${controller.value?.validate(type: PhoneNumberType.fixedLine) ?? 'false'}'), ], ), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index aa430e02..e96ebd2f 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -134,7 +134,7 @@ packages: name: phone_numbers_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.1.0" sky_engine: dependency: transitive description: flutter diff --git a/lib/phone_form_field.dart b/lib/phone_form_field.dart index 0654ff6b..3bbe5543 100644 --- a/lib/phone_form_field.dart +++ b/lib/phone_form_field.dart @@ -10,7 +10,7 @@ export 'src/validator/phone_validator.dart'; export 'l10n/generated/phone_field_localization.dart'; export 'src/models/selector_config.dart'; -export 'src/models/phone_controller.dart'; +export 'src/models/phone_form_field_controller.dart'; export 'src/models/country.dart'; export 'src/models/all_countries.dart'; diff --git a/lib/src/models/country.dart b/lib/src/models/country.dart index e789e2ca..75d87e96 100644 --- a/lib/src/models/country.dart +++ b/lib/src/models/country.dart @@ -14,7 +14,8 @@ class Country { /// returns "+ [countryCode]" String get displayCountryCode => '+ $countryCode'; - Country(this.isoCode) : assert(isoCodes.contains(isoCode)); + Country(this.isoCode) + : assert(isoCodes.contains(isoCode), 'isocode $isoCode not found'); @override bool operator ==(Object other) => diff --git a/lib/src/models/phone_controller.dart b/lib/src/models/phone_controller.dart deleted file mode 100644 index c3696e78..00000000 --- a/lib/src/models/phone_controller.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:phone_form_field/phone_form_field.dart'; - -class PhoneController extends ValueNotifier { - final PhoneNumber? initialValue; - PhoneController(this.initialValue) : super(initialValue); -} diff --git a/lib/src/models/phone_field_controller.dart b/lib/src/models/phone_field_controller.dart new file mode 100644 index 00000000..2832e31a --- /dev/null +++ b/lib/src/models/phone_field_controller.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +class PhoneFieldController extends ChangeNotifier { + late final ValueNotifier isoCodeController; + late final TextEditingController nationalController; + final String defaultIsoCode; + + /// focus node of the national number + final FocusNode focusNode; + + String? get isoCode => isoCodeController.value; + String? get national => + nationalController.text.isEmpty ? null : nationalController.text; + set isoCode(String? isoCode) => isoCodeController.value = isoCode; + set national(String? national) => nationalController.value = TextEditingValue( + text: national ?? '', + selection: TextSelection.fromPosition( + TextPosition(offset: national?.length ?? 0), + ), + ); + + PhoneFieldController({ + required String? national, + required String? isoCode, + required this.defaultIsoCode, + required this.focusNode, + }) { + isoCodeController = ValueNotifier(isoCode); + nationalController = TextEditingController(text: national); + isoCodeController.addListener(notifyListeners); + nationalController.addListener(notifyListeners); + } + + selectNationalNumber() { + nationalController.selection = TextSelection( + baseOffset: 0, + extentOffset: nationalController.value.text.length, + ); + focusNode.requestFocus(); + } + + @override + void dispose() { + isoCodeController.dispose(); + nationalController.dispose(); + super.dispose(); + } +} diff --git a/lib/src/models/phone_form_field_controller.dart b/lib/src/models/phone_form_field_controller.dart new file mode 100644 index 00000000..838962db --- /dev/null +++ b/lib/src/models/phone_form_field_controller.dart @@ -0,0 +1,27 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:phone_form_field/phone_form_field.dart'; + +class PhoneController extends ValueNotifier { + final PhoneNumber? initialValue; + // when we want to select the national number + final StreamController _selectionRequest$ = StreamController(); + Stream get selectionRequest$ => _selectionRequest$.stream; + + PhoneController(this.initialValue) : super(initialValue); + + selectNationalNumber() { + _selectionRequest$.add(null); + } + + reset() { + value = null; + } + + @override + void dispose() { + _selectionRequest$.close(); + super.dispose(); + } +} diff --git a/lib/src/widgets/phone_field.dart b/lib/src/widgets/phone_field.dart index f7ba5595..437d4b0a 100644 --- a/lib/src/widgets/phone_field.dart +++ b/lib/src/widgets/phone_field.dart @@ -2,52 +2,113 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:phone_form_field/src/constants/constants.dart'; -import 'package:phone_form_field/src/models/simple_phone_number.dart'; +import 'package:phone_form_field/src/models/phone_field_controller.dart'; import 'package:phone_form_field/src/widgets/measure_initial_size.dart'; import '../../phone_form_field.dart'; import '../models/country.dart'; import 'country_picker/country_selector_navigator.dart'; import 'country_code_chip.dart'; +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; /// Phone field /// /// This deals with mostly UI and has no dependency on any phone parser library class PhoneField extends StatefulWidget { - final ValueNotifier controller; - final String defaultCountry; - final bool autofocus; + final PhoneFieldController controller; final bool showFlagInInput; - final bool? enabled; final String? errorText; final double flagSize; - final TextInputAction? textInputAction; - - /// input decoration applied to the input final InputDecoration decoration; - final Color? cursorColor; - final Iterable? autoFillHints; - final Function()? onEditingComplete; /// configures the way the country picker selector is shown final CountrySelectorNavigator selectorNavigator; + // textfield inputs + final TextInputType keyboardType; + final TextInputAction? textInputAction; + final TextStyle? style; + final StrutStyle? strutStyle; + final TextAlign textAlign; + final TextAlignVertical? textAlignVertical; + final TextDirection? textDirection; + final bool autofocus; + final String obscuringCharacter; + final bool obscureText; + final bool autocorrect; + final SmartDashesType? smartDashesType; + final SmartQuotesType? smartQuotesType; + final bool enableSuggestions; + final ToolbarOptions? toolbarOptions; + final bool? showCursor; + final VoidCallback? onEditingComplete; + final ValueChanged? onSubmitted; + final AppPrivateCommandCallback? onAppPrivateCommand; + final bool? enabled; + final double cursorWidth; + final double? cursorHeight; + final Radius? cursorRadius; + final Color? cursorColor; + final ui.BoxHeightStyle selectionHeightStyle; + final ui.BoxWidthStyle selectionWidthStyle; + final Brightness? keyboardAppearance; + final EdgeInsets scrollPadding; + final bool enableInteractiveSelection; + final TextSelectionControls? selectionControls; + bool get selectionEnabled => enableInteractiveSelection; + final MouseCursor? mouseCursor; + final ScrollPhysics? scrollPhysics; + final ScrollController? scrollController; + final Iterable? autofillHints; + final String? restorationId; + final bool enableIMEPersonalizedLearning; + PhoneField({ // form field params Key? key, required this.controller, - required this.autoFillHints, - required this.enabled, - required this.defaultCountry, - required this.autofocus, required this.showFlagInInput, - required this.onEditingComplete, - required this.errorText, - required this.decoration, - required this.cursorColor, required this.selectorNavigator, required this.flagSize, + required this.errorText, + required this.decoration, + // textfield inputs + required this.keyboardType, required this.textInputAction, + required this.style, + required this.strutStyle, + required this.textAlign, + required this.textAlignVertical, + required this.textDirection, + required this.autofocus, + required this.obscuringCharacter, + required this.obscureText, + required this.autocorrect, + required this.smartDashesType, + required this.smartQuotesType, + required this.enableSuggestions, + required this.toolbarOptions, + required this.showCursor, + required this.onEditingComplete, + required this.onSubmitted, + required this.onAppPrivateCommand, + required this.enabled, + required this.cursorWidth, + required this.cursorHeight, + required this.cursorRadius, + required this.cursorColor, + required this.selectionHeightStyle, + required this.selectionWidthStyle, + required this.keyboardAppearance, + required this.scrollPadding, + required this.enableInteractiveSelection, + required this.selectionControls, + required this.mouseCursor, + required this.scrollPhysics, + required this.scrollController, + required this.autofillHints, + required this.restorationId, + required this.enableIMEPersonalizedLearning, }); @override @@ -55,65 +116,34 @@ class PhoneField extends StatefulWidget { } class _PhoneFieldState extends State { - final FocusNode _focusNode = FocusNode(); Size? _size; - /// this is the controller for the national phone number - late TextEditingController _nationalNumberController; - bool get _isOutlineBorder => widget.decoration.border is OutlineInputBorder; - - SimplePhoneNumber? get value => widget.controller.value; - String get _isoCode => value?.isoCode ?? widget.defaultCountry; - + PhoneFieldController get controller => widget.controller; _PhoneFieldState(); @override void initState() { - _nationalNumberController = TextEditingController(text: value?.national); - widget.controller.addListener(() => _updateValue(widget.controller.value)); - _focusNode.addListener(() => setState(() {})); + controller.focusNode.addListener(onFocusChange); super.initState(); } - /// to update the current value of the input - void _updateValue(SimplePhoneNumber? phoneNumber) async { - final national = phoneNumber?.national ?? ''; - // if the national number has changed from outside we need to update - // the controller value - if (national != _nationalNumberController.text) { - // we need to use a future here because when resetting - // there is a race condition between the focus out event (clicking on reset) - // which updates the value to the current one without text selection - // and the actual reset - await Future.value(); - _nationalNumberController.value = TextEditingValue( - text: national, - selection: TextSelection.fromPosition( - TextPosition(offset: national.length), - ), - ); - } - // when updating from within - if (widget.controller.value != phoneNumber) { - widget.controller.value = phoneNumber; - } + void onFocusChange() { + setState(() {}); } @override void dispose() { - _focusNode.dispose(); - _nationalNumberController.dispose(); + controller.focusNode.removeListener(onFocusChange); super.dispose(); } void selectCountry() async { final selected = await widget.selectorNavigator.navigate(context); if (selected != null) { - _updateValue(SimplePhoneNumber( - isoCode: selected.isoCode, national: value?.national ?? '')); + controller.isoCode = selected.isoCode; } - _focusNode.requestFocus(); + controller.focusNode.requestFocus(); } Widget build(BuildContext context) { @@ -127,39 +157,17 @@ class _PhoneFieldState extends State { onSizeFound: (size) => setState(() => _size = size), child: _textField(), ), - if (_focusNode.hasFocus || _nationalNumberController.text.isNotEmpty) + if (controller.focusNode.hasFocus || controller.national != null) _inkWellOverlay(), ], ); } Widget _textField() { - // this is hacky but flutter does not provide a way to - // align the different prefix options with the text which might - // ultimately be fixed on flutter's side - // so all the padding options here are to align the country code - // with the the text - // double paddingBottom = 0; - // double paddingLeft = 0; - // double paddingTop = 0; - // if (_isOutlineBorder && !_hasLabel) paddingBottom = 3; - // if (!_isOutlineBorder && !_hasLabel) paddingBottom = 5; - - // final padding = - // EdgeInsets.fromLTRB(paddingLeft, paddingTop, 0, paddingBottom); return TextField( - focusNode: _focusNode, - controller: _nationalNumberController, - textInputAction: widget.textInputAction, - onChanged: (national) => _updateValue( - SimplePhoneNumber(isoCode: _isoCode, national: national)), - autofocus: widget.autofocus, - autofillHints: widget.autoFillHints, - onEditingComplete: widget.onEditingComplete, + focusNode: controller.focusNode, + controller: controller.nationalController, enabled: widget.enabled, - textDirection: TextDirection.ltr, - keyboardType: TextInputType.phone, - cursorColor: widget.cursorColor, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp( '[${Constants.PLUS}${Constants.DIGITS}${Constants.PUNCTUATION}]')), @@ -168,6 +176,41 @@ class _PhoneFieldState extends State { errorText: widget.errorText, prefix: _getDialCodeChip(), ), + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + style: widget.style, + strutStyle: widget.strutStyle, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + textDirection: widget.textDirection, + autofocus: widget.autofocus, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + toolbarOptions: widget.toolbarOptions, + showCursor: widget.showCursor, + onEditingComplete: widget.onEditingComplete, + onSubmitted: widget.onSubmitted, + onAppPrivateCommand: widget.onAppPrivateCommand, + cursorWidth: widget.cursorWidth, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorColor: widget.cursorColor, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + keyboardAppearance: widget.keyboardAppearance, + scrollPadding: widget.scrollPadding, + enableInteractiveSelection: widget.enableInteractiveSelection, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + restorationId: widget.restorationId, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, ); } @@ -204,7 +247,7 @@ class _PhoneFieldState extends State { visible: visible, child: CountryCodeChip( key: visible ? Key('country-code-chip') : null, - country: Country(_isoCode), + country: Country(controller.isoCode ?? controller.defaultIsoCode), showFlag: widget.showFlagInInput, textStyle: TextStyle( fontSize: 16, diff --git a/lib/src/widgets/phone_form_field.dart b/lib/src/widgets/phone_form_field.dart index 11bf79af..366c5968 100644 --- a/lib/src/widgets/phone_form_field.dart +++ b/lib/src/widgets/phone_form_field.dart @@ -1,14 +1,17 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:phone_form_field/src/constants/constants.dart'; import 'package:phone_form_field/src/helpers/validator_translator.dart'; -import 'package:phone_form_field/src/models/phone_controller.dart'; -import 'package:phone_form_field/src/models/simple_phone_number.dart'; +import 'package:phone_form_field/src/models/phone_field_controller.dart'; +import 'package:phone_form_field/src/models/phone_form_field_controller.dart'; import 'package:phone_form_field/src/validator/phone_validator.dart'; import 'package:phone_form_field/src/widgets/phone_field.dart'; import 'package:phone_numbers_parser/phone_numbers_parser.dart'; import 'country_picker/country_selector_navigator.dart'; +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; /// Phone input extending form field. /// @@ -84,35 +87,73 @@ class PhoneFormField extends FormField { /// callback called when the input value changes final ValueChanged? onChanged; + /// country that is displayed when there is no value + final String defaultCountry; + + /// the focusNode of the national number + final FocusNode? focusNode; + PhoneFormField({ Key? key, this.controller, this.shouldFormat = true, this.onChanged, - List autofillHints = const [], - bool autofocus = false, - bool enabled = true, + this.focusNode, bool showFlagInInput = true, CountrySelectorNavigator selectorNavigator = const BottomSheetNavigator(), Function(PhoneNumber?)? onSaved, - String defaultCountry = 'US', + this.defaultCountry = 'US', InputDecoration decoration = const InputDecoration(border: UnderlineInputBorder()), - Color? cursorColor, AutovalidateMode autovalidateMode = AutovalidateMode.onUserInteraction, PhoneNumber? initialValue, double flagSize = 16, PhoneNumberInputValidator? validator, - Function()? onEditingComplete, + // textfield inputs + TextInputType keyboardType = TextInputType.phone, TextInputAction? textInputAction, + TextStyle? style, + StrutStyle? strutStyle, + TextAlign textAlign = TextAlign.start, + TextAlignVertical? textAlignVertical, + TextDirection? textDirection, + bool autofocus = false, + String obscuringCharacter = '*', + bool obscureText = false, + bool autocorrect = true, + SmartDashesType? smartDashesType, + SmartQuotesType? smartQuotesType, + bool enableSuggestions = true, + ToolbarOptions? toolbarOptions, + bool? showCursor, + VoidCallback? onEditingComplete, + ValueChanged? onSubmitted, + AppPrivateCommandCallback? onAppPrivateCommand, + List? inputFormatters, + bool enabled = true, + double cursorWidth = 2.0, + double? cursorHeight, + Radius? cursorRadius, + Color? cursorColor, + ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight, + ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight, + Brightness? keyboardAppearance, + EdgeInsets scrollPadding = const EdgeInsets.all(20.0), + bool enableInteractiveSelection = true, + TextSelectionControls? selectionControls, + MouseCursor? mouseCursor, + ScrollPhysics? scrollPhysics, + ScrollController? scrollController, + Iterable? autofillHints, String? restorationId, + bool enableIMEPersonalizedLearning = true, }) : assert( initialValue == null || controller == null, 'One of initialValue or controller can be specified at a time', ), super( key: key, - autovalidateMode: AutovalidateMode.always, + autovalidateMode: autovalidateMode, enabled: enabled, initialValue: controller != null ? controller.initialValue : initialValue, @@ -123,18 +164,48 @@ class PhoneFormField extends FormField { final field = state as _PhoneFormFieldState; return PhoneField( controller: field._childController, - autoFillHints: autofillHints, - enabled: enabled, showFlagInInput: showFlagInInput, - decoration: decoration, - autofocus: autofocus, - defaultCountry: defaultCountry, selectorNavigator: selectorNavigator, - cursorColor: cursorColor, errorText: field.getErrorText(), flagSize: flagSize, - onEditingComplete: onEditingComplete, + decoration: decoration, + enabled: enabled, + // textfield params + autofillHints: autofillHints, + keyboardType: keyboardType, textInputAction: textInputAction, + style: style, + strutStyle: strutStyle, + textAlign: textAlign, + textAlignVertical: textAlignVertical, + textDirection: textDirection, + autofocus: autofocus, + obscuringCharacter: obscuringCharacter, + obscureText: obscureText, + autocorrect: autocorrect, + smartDashesType: smartDashesType, + smartQuotesType: smartQuotesType, + enableSuggestions: enableSuggestions, + toolbarOptions: toolbarOptions, + showCursor: showCursor, + onEditingComplete: onEditingComplete, + onSubmitted: onSubmitted, + onAppPrivateCommand: onAppPrivateCommand, + cursorWidth: cursorWidth, + cursorHeight: cursorHeight, + cursorRadius: cursorRadius, + cursorColor: cursorColor, + selectionHeightStyle: selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle, + keyboardAppearance: keyboardAppearance, + scrollPadding: scrollPadding, + enableInteractiveSelection: enableInteractiveSelection, + selectionControls: selectionControls, + mouseCursor: mouseCursor, + scrollController: scrollController, + scrollPhysics: scrollPhysics, + restorationId: restorationId, + enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, ); }, ); @@ -145,7 +216,8 @@ class PhoneFormField extends FormField { class _PhoneFormFieldState extends FormFieldState { late final PhoneController _controller; - late final ValueNotifier _childController; + late final PhoneFieldController _childController; + late final StreamSubscription _selectionSubscription; @override PhoneFormField get widget => super.widget as PhoneFormField; @@ -153,18 +225,25 @@ class _PhoneFormFieldState extends FormFieldState { @override void initState() { super.initState(); - final simplePhoneNumber = _convertPhoneNumberToFormattedSimplePhone(value); _controller = widget.controller ?? PhoneController(value); - _childController = ValueNotifier(simplePhoneNumber); + _childController = PhoneFieldController( + defaultIsoCode: widget.defaultCountry, + isoCode: _controller.value?.isoCode, + national: _getFormattedNsn(), + focusNode: widget.focusNode ?? FocusNode(), + ); _controller.addListener(_onControllerChange); - _childController - .addListener(() => _onChildControllerChange(_childController.value)); + _childController.addListener(() => _onChildControllerChange()); + // to expose text selection of national number + _selectionSubscription = _controller.selectionRequest$ + .listen((event) => _childController.selectNationalNumber()); } @override void dispose() { super.dispose(); _childController.dispose(); + _selectionSubscription.cancel(); // dispose the controller only when it's initialised in this instance // otherwise this should be done where instance is created if (widget.controller == null) { @@ -183,61 +262,56 @@ class _PhoneFormFieldState extends FormFieldState { /// deals with the UI can display the correct value. void _onControllerChange() { final phone = _controller.value; - final base = _childController.value; widget.onChanged?.call(phone); didChange(phone); - final formatted = _convertPhoneNumberToFormattedSimplePhone(phone); - if (base?.national != formatted?.national || - base?.isoCode != formatted?.isoCode) { - _childController.value = formatted; + final formatted = _getFormattedNsn(); + if (_childController.national != formatted) { + _childController.national = formatted; + } + if (_childController.isoCode != phone?.isoCode) { + _childController.isoCode = phone?.isoCode; } } /// when the base controller changes (when the user manually input something) /// then we need to update the local controller's value. - void _onChildControllerChange(SimplePhoneNumber? simplePhone) { - if (simplePhone?.national == _controller.value?.nsn && - simplePhone?.isoCode == _controller.value?.isoCode) { + void _onChildControllerChange() { + if (_childController.national == _controller.value?.nsn && + _childController.isoCode == _controller.value?.isoCode) { return; } - if (simplePhone == null) { + if (_childController.national == null && _childController.isoCode == null) { return _controller.value = null; } - // we convert the simple phone number to a full blown PhoneNumber - // to access validation, formatting etc. + // we convert the multiple controllers from the child controller + // to a full blown PhoneNumber to access validation, formatting etc. PhoneNumber phoneNumber; - // when the base input change we check if its not a whole number + // when the nsn input change we check if its not a whole number // to allow for copy pasting and auto fill. If it is one then - // we parse it accordingly - if (simplePhone.national.startsWith(RegExp('[${Constants.PLUS}]'))) { + // we parse it accordingly. + // we assume it's a whole phone number if it starts with + + final childNsn = _childController.national; + if (childNsn != null && + childNsn.startsWith(RegExp('[${Constants.PLUS}]'))) { // if starts with + then we parse the whole number // to figure out the country code - final international = simplePhone.national; + final international = childNsn; phoneNumber = PhoneNumber.fromRaw(international); } else { phoneNumber = PhoneNumber.fromNational( - simplePhone.isoCode, - simplePhone.national, + _childController.isoCode ?? _childController.defaultIsoCode, + childNsn ?? '', ); } _controller.value = phoneNumber; } - /// converts the phone number value to a formatted value - /// usable by the childController, The [PhoneField] - /// which deals with the UI, will display that value - SimplePhoneNumber? _convertPhoneNumberToFormattedSimplePhone( - PhoneNumber? phoneNumber) { - if (phoneNumber == null) return null; - var formattedNsn = phoneNumber.nsn; + String? _getFormattedNsn() { if (widget.shouldFormat) { - formattedNsn = phoneNumber.getFormattedNsn(); + return _controller.value?.getFormattedNsn(); } - return SimplePhoneNumber( - isoCode: phoneNumber.isoCode, - national: formattedNsn, - ); + return _controller.value?.nsn; } /// gets the localized error text if any diff --git a/pubspec.lock b/pubspec.lock index 93f9535e..ff4a8fec 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -120,7 +120,7 @@ packages: name: phone_numbers_parser url: "https://pub.dartlang.org" source: hosted - version: "4.0.1" + version: "4.1.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 2798b100..5e856aa7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: flutter: sdk: flutter circle_flags: ^0.0.2 - phone_numbers_parser: ^4.0.1 + phone_numbers_parser: ^4.1.0 dart_countries: ^2.1.0 intl: ^0.17.0 diff --git a/test/phone_form_field_test.dart b/test/phone_form_field_test.dart index de0ca90d..146ccf3a 100644 --- a/test/phone_form_field_test.dart +++ b/test/phone_form_field_test.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:phone_form_field/phone_form_field.dart'; -import 'package:phone_form_field/src/models/phone_controller.dart'; +import 'package:phone_form_field/src/models/phone_form_field_controller.dart'; import 'package:phone_form_field/src/validator/phone_validator.dart'; import 'package:phone_form_field/src/widgets/country_picker/country_selector.dart'; import 'package:phone_form_field/src/widgets/country_code_chip.dart';