From 70d64aeb9b3bc8825f33d04a0ce5bce86815a86e Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 18 Oct 2021 13:49:11 +0200 Subject: [PATCH 1/9] passthrough --- example/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/lib/main.dart | 4 + lib/src/widgets/phone_field.dart | 155 +++++++++++++----- lib/src/widgets/phone_form_field.dart | 87 ++++++++-- 5 files changed, 200 insertions(+), 50 deletions(-) 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..6a6fb59e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -71,6 +71,10 @@ class PhoneFieldView extends StatelessWidget { } class MyApp extends StatelessWidget { + MyApp() { + print('My App'); + } + @override Widget build(BuildContext context) { return MaterialApp( diff --git a/lib/src/widgets/phone_field.dart b/lib/src/widgets/phone_field.dart index f7ba5595..f667b60b 100644 --- a/lib/src/widgets/phone_field.dart +++ b/lib/src/widgets/phone_field.dart @@ -9,45 +9,110 @@ 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 bool showFlagInInput; - final bool? enabled; + final String defaultCountry; final String? errorText; final double flagSize; - final TextInputAction? textInputAction; - - /// input decoration applied to the input + final FocusNode? focusNode; 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.focusNode, + 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,7 +120,7 @@ class PhoneField extends StatefulWidget { } class _PhoneFieldState extends State { - final FocusNode _focusNode = FocusNode(); + late final FocusNode _focusNode; Size? _size; /// this is the controller for the national phone number @@ -70,6 +135,7 @@ class _PhoneFieldState extends State { @override void initState() { + _focusNode = widget.focusNode ?? FocusNode(); _nationalNumberController = TextEditingController(text: value?.national); widget.controller.addListener(() => _updateValue(widget.controller.value)); _focusNode.addListener(() => setState(() {})); @@ -134,32 +200,12 @@ class _PhoneFieldState extends State { } 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, enabled: widget.enabled, - textDirection: TextDirection.ltr, - keyboardType: TextInputType.phone, - cursorColor: widget.cursorColor, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp( '[${Constants.PLUS}${Constants.DIGITS}${Constants.PUNCTUATION}]')), @@ -168,6 +214,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, ); } diff --git a/lib/src/widgets/phone_form_field.dart b/lib/src/widgets/phone_form_field.dart index 11bf79af..3373b1dc 100644 --- a/lib/src/widgets/phone_form_field.dart +++ b/lib/src/widgets/phone_form_field.dart @@ -9,6 +9,7 @@ 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. /// @@ -89,23 +90,55 @@ class PhoneFormField extends FormField { this.controller, this.shouldFormat = true, this.onChanged, - List autofillHints = const [], - bool autofocus = false, - bool enabled = true, bool showFlagInInput = true, CountrySelectorNavigator selectorNavigator = const BottomSheetNavigator(), Function(PhoneNumber?)? onSaved, String 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 + FocusNode? focusNode, + 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', @@ -122,19 +155,51 @@ class PhoneFormField extends FormField { builder: (state) { final field = state as _PhoneFormFieldState; return PhoneField( + focusNode: focusNode, 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, ); }, ); From dac6f1dd0b75b11b0257be1e6d8b7d93535d7b17 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 18 Oct 2021 13:52:34 +0200 Subject: [PATCH 2/9] changelog --- CHANGELOG.md | 3 +++ README.md | 9 ++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d560c243..73790708 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## [4.3.0] 16 / 10 / 2021 +- Added most of textfield params to the phone input. + ## [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 ) ``` From e35784f8560cf013eab5f267dcfafd6086faf4ed Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 18 Oct 2021 16:03:35 +0200 Subject: [PATCH 3/9] autovalidate mode --- lib/src/widgets/phone_form_field.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/phone_form_field.dart b/lib/src/widgets/phone_form_field.dart index 3373b1dc..09f0a92d 100644 --- a/lib/src/widgets/phone_form_field.dart +++ b/lib/src/widgets/phone_form_field.dart @@ -145,7 +145,7 @@ class PhoneFormField extends FormField { ), super( key: key, - autovalidateMode: AutovalidateMode.always, + autovalidateMode: autovalidateMode, enabled: enabled, initialValue: controller != null ? controller.initialValue : initialValue, From 823cda61512ad9620f972d6b3cee91669f022d77 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 18 Oct 2021 16:07:19 +0200 Subject: [PATCH 4/9] remove unnecessary print statement --- example/lib/main.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6a6fb59e..46160459 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -71,9 +71,7 @@ class PhoneFieldView extends StatelessWidget { } class MyApp extends StatelessWidget { - MyApp() { - print('My App'); - } + MyApp(); @override Widget build(BuildContext context) { From a04a5aa2e47b47512d96ad66ebe75b629c0f3895 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 18 Oct 2021 17:50:02 +0200 Subject: [PATCH 5/9] ff --- lib/phone_form_field.dart | 2 +- lib/src/models/phone_controller.dart | 7 ------ lib/src/models/phone_field_controller.dart | 24 +++++++++++++++++++ .../models/phone_form_field_controller.dart | 23 ++++++++++++++++++ lib/src/widgets/phone_form_field.dart | 14 ++++++++--- test/phone_form_field_test.dart | 2 +- 6 files changed, 60 insertions(+), 12 deletions(-) delete mode 100644 lib/src/models/phone_controller.dart create mode 100644 lib/src/models/phone_field_controller.dart create mode 100644 lib/src/models/phone_form_field_controller.dart 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/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..be925d5e --- /dev/null +++ b/lib/src/models/phone_field_controller.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:phone_form_field/src/models/simple_phone_number.dart'; + +class PhoneFieldController extends ChangeNotifier { + ValueNotifier _isoCodeController = ValueNotifier(''); + TextEditingController _nationalController = TextEditingController(); + // when we want to select the national number + final StreamController _selectionRequest$ = StreamController(); + Stream get selectionRequest$ => _selectionRequest$.stream; + + PhoneFieldController({required Strnational, required this.isoCode}); + + selectNationalNumber() { + _selectionRequest$.add(null); + } + + @override + void dispose() { + _selectionRequest$.close(); + 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..1808e409 --- /dev/null +++ b/lib/src/models/phone_form_field_controller.dart @@ -0,0 +1,23 @@ +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); + } + + @override + void dispose() { + _selectionRequest$.close(); + super.dispose(); + } +} diff --git a/lib/src/widgets/phone_form_field.dart b/lib/src/widgets/phone_form_field.dart index 09f0a92d..e47f8ca7 100644 --- a/lib/src/widgets/phone_form_field.dart +++ b/lib/src/widgets/phone_form_field.dart @@ -1,8 +1,11 @@ +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/phone_field_controller.dart'; +import 'package:phone_form_field/src/models/phone_form_field_controller.dart'; import 'package:phone_form_field/src/models/simple_phone_number.dart'; import 'package:phone_form_field/src/validator/phone_validator.dart'; import 'package:phone_form_field/src/widgets/phone_field.dart'; @@ -210,7 +213,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; @@ -220,16 +224,20 @@ class _PhoneFormFieldState extends FormFieldState { super.initState(); final simplePhoneNumber = _convertPhoneNumberToFormattedSimplePhone(value); _controller = widget.controller ?? PhoneController(value); - _childController = ValueNotifier(simplePhoneNumber); + _childController = PhoneFieldController(simplePhoneNumber); _controller.addListener(_onControllerChange); _childController .addListener(() => _onChildControllerChange(_childController.value)); + // 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) { 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'; From bc89ab2cd98a1ff1bde9269a1de2dbb3370ac9d9 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 18 Oct 2021 20:07:29 +0200 Subject: [PATCH 6/9] controllers refactor --- example/lib/main.dart | 30 +++++--- lib/src/models/country.dart | 3 +- lib/src/models/phone_field_controller.dart | 37 +++++++--- .../models/phone_form_field_controller.dart | 4 ++ lib/src/widgets/phone_field.dart | 52 ++------------ lib/src/widgets/phone_form_field.dart | 70 +++++++++---------- 6 files changed, 96 insertions(+), 100 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 46160459..74fb6ca2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -227,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/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_field_controller.dart b/lib/src/models/phone_field_controller.dart index be925d5e..a54e7570 100644 --- a/lib/src/models/phone_field_controller.dart +++ b/lib/src/models/phone_field_controller.dart @@ -1,24 +1,43 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:phone_form_field/src/models/simple_phone_number.dart'; class PhoneFieldController extends ChangeNotifier { - ValueNotifier _isoCodeController = ValueNotifier(''); - TextEditingController _nationalController = TextEditingController(); - // when we want to select the national number - final StreamController _selectionRequest$ = StreamController(); - Stream get selectionRequest$ => _selectionRequest$.stream; + late final ValueNotifier isoCodeController; + late final TextEditingController nationalController; + final String defaultIsoCode; - PhoneFieldController({required Strnational, required this.isoCode}); + 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, + }) { + isoCodeController = ValueNotifier(isoCode); + nationalController = TextEditingController(text: national); + isoCodeController.addListener(notifyListeners); + nationalController.addListener(notifyListeners); + } selectNationalNumber() { - _selectionRequest$.add(null); + nationalController.selection = TextSelection( + baseOffset: 0, extentOffset: nationalController.value.text.length); } @override void dispose() { - _selectionRequest$.close(); + 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 index 1808e409..838962db 100644 --- a/lib/src/models/phone_form_field_controller.dart +++ b/lib/src/models/phone_form_field_controller.dart @@ -15,6 +15,10 @@ class PhoneController extends ValueNotifier { _selectionRequest$.add(null); } + reset() { + value = null; + } + @override void dispose() { _selectionRequest$.close(); diff --git a/lib/src/widgets/phone_field.dart b/lib/src/widgets/phone_field.dart index f667b60b..cb9c2432 100644 --- a/lib/src/widgets/phone_field.dart +++ b/lib/src/widgets/phone_field.dart @@ -2,7 +2,7 @@ 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'; @@ -15,9 +15,8 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; /// /// This deals with mostly UI and has no dependency on any phone parser library class PhoneField extends StatefulWidget { - final ValueNotifier controller; + final PhoneFieldController controller; final bool showFlagInInput; - final String defaultCountry; final String? errorText; final double flagSize; final FocusNode? focusNode; @@ -69,7 +68,6 @@ class PhoneField extends StatefulWidget { // form field params Key? key, required this.controller, - required this.defaultCountry, required this.showFlagInInput, required this.selectorNavigator, required this.flagSize, @@ -123,61 +121,27 @@ class _PhoneFieldState extends State { late final 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() { _focusNode = widget.focusNode ?? FocusNode(); - _nationalNumberController = TextEditingController(text: value?.national); - widget.controller.addListener(() => _updateValue(widget.controller.value)); _focusNode.addListener(() => setState(() {})); 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; - } - } - @override void dispose() { _focusNode.dispose(); - _nationalNumberController.dispose(); 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(); } @@ -193,7 +157,7 @@ class _PhoneFieldState extends State { onSizeFound: (size) => setState(() => _size = size), child: _textField(), ), - if (_focusNode.hasFocus || _nationalNumberController.text.isNotEmpty) + if (_focusNode.hasFocus || controller.national != null) _inkWellOverlay(), ], ); @@ -202,9 +166,7 @@ class _PhoneFieldState extends State { Widget _textField() { return TextField( focusNode: _focusNode, - controller: _nationalNumberController, - onChanged: (national) => _updateValue( - SimplePhoneNumber(isoCode: _isoCode, national: national)), + controller: controller.nationalController, enabled: widget.enabled, inputFormatters: [ FilteringTextInputFormatter.allow(RegExp( @@ -285,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 e47f8ca7..8574844d 100644 --- a/lib/src/widgets/phone_form_field.dart +++ b/lib/src/widgets/phone_form_field.dart @@ -6,7 +6,6 @@ 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_field_controller.dart'; import 'package:phone_form_field/src/models/phone_form_field_controller.dart'; -import 'package:phone_form_field/src/models/simple_phone_number.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'; @@ -88,6 +87,9 @@ 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; + PhoneFormField({ Key? key, this.controller, @@ -96,7 +98,7 @@ class PhoneFormField extends FormField { bool showFlagInInput = true, CountrySelectorNavigator selectorNavigator = const BottomSheetNavigator(), Function(PhoneNumber?)? onSaved, - String defaultCountry = 'US', + this.defaultCountry = 'US', InputDecoration decoration = const InputDecoration(border: UnderlineInputBorder()), AutovalidateMode autovalidateMode = AutovalidateMode.onUserInteraction, @@ -161,7 +163,6 @@ class PhoneFormField extends FormField { focusNode: focusNode, controller: field._childController, showFlagInInput: showFlagInInput, - defaultCountry: defaultCountry, selectorNavigator: selectorNavigator, errorText: field.getErrorText(), flagSize: flagSize, @@ -222,12 +223,14 @@ class _PhoneFormFieldState extends FormFieldState { @override void initState() { super.initState(); - final simplePhoneNumber = _convertPhoneNumberToFormattedSimplePhone(value); _controller = widget.controller ?? PhoneController(value); - _childController = PhoneFieldController(simplePhoneNumber); + _childController = PhoneFieldController( + defaultIsoCode: widget.defaultCountry, + isoCode: _controller.value?.isoCode, + national: _getFormattedNsn(), + ); _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()); @@ -256,61 +259,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 From 05ecde443753fd4196aac4441fad234ab71d667f Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 18 Oct 2021 20:22:00 +0200 Subject: [PATCH 7/9] controller --- lib/src/models/phone_field_controller.dart | 11 ++++++++--- lib/src/widgets/phone_field.dart | 18 +++++++++--------- lib/src/widgets/phone_form_field.dart | 7 +++++-- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/lib/src/models/phone_field_controller.dart b/lib/src/models/phone_field_controller.dart index a54e7570..2832e31a 100644 --- a/lib/src/models/phone_field_controller.dart +++ b/lib/src/models/phone_field_controller.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; class PhoneFieldController extends ChangeNotifier { @@ -7,6 +5,9 @@ class PhoneFieldController extends ChangeNotifier { 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; @@ -22,6 +23,7 @@ class PhoneFieldController extends ChangeNotifier { required String? national, required String? isoCode, required this.defaultIsoCode, + required this.focusNode, }) { isoCodeController = ValueNotifier(isoCode); nationalController = TextEditingController(text: national); @@ -31,7 +33,10 @@ class PhoneFieldController extends ChangeNotifier { selectNationalNumber() { nationalController.selection = TextSelection( - baseOffset: 0, extentOffset: nationalController.value.text.length); + baseOffset: 0, + extentOffset: nationalController.value.text.length, + ); + focusNode.requestFocus(); } @override diff --git a/lib/src/widgets/phone_field.dart b/lib/src/widgets/phone_field.dart index cb9c2432..437d4b0a 100644 --- a/lib/src/widgets/phone_field.dart +++ b/lib/src/widgets/phone_field.dart @@ -19,7 +19,6 @@ class PhoneField extends StatefulWidget { final bool showFlagInInput; final String? errorText; final double flagSize; - final FocusNode? focusNode; final InputDecoration decoration; /// configures the way the country picker selector is shown @@ -72,7 +71,6 @@ class PhoneField extends StatefulWidget { required this.selectorNavigator, required this.flagSize, required this.errorText, - required this.focusNode, required this.decoration, // textfield inputs required this.keyboardType, @@ -118,7 +116,6 @@ class PhoneField extends StatefulWidget { } class _PhoneFieldState extends State { - late final FocusNode _focusNode; Size? _size; bool get _isOutlineBorder => widget.decoration.border is OutlineInputBorder; @@ -127,14 +124,17 @@ class _PhoneFieldState extends State { @override void initState() { - _focusNode = widget.focusNode ?? FocusNode(); - _focusNode.addListener(() => setState(() {})); + controller.focusNode.addListener(onFocusChange); super.initState(); } + void onFocusChange() { + setState(() {}); + } + @override void dispose() { - _focusNode.dispose(); + controller.focusNode.removeListener(onFocusChange); super.dispose(); } @@ -143,7 +143,7 @@ class _PhoneFieldState extends State { if (selected != null) { controller.isoCode = selected.isoCode; } - _focusNode.requestFocus(); + controller.focusNode.requestFocus(); } Widget build(BuildContext context) { @@ -157,7 +157,7 @@ class _PhoneFieldState extends State { onSizeFound: (size) => setState(() => _size = size), child: _textField(), ), - if (_focusNode.hasFocus || controller.national != null) + if (controller.focusNode.hasFocus || controller.national != null) _inkWellOverlay(), ], ); @@ -165,7 +165,7 @@ class _PhoneFieldState extends State { Widget _textField() { return TextField( - focusNode: _focusNode, + focusNode: controller.focusNode, controller: controller.nationalController, enabled: widget.enabled, inputFormatters: [ diff --git a/lib/src/widgets/phone_form_field.dart b/lib/src/widgets/phone_form_field.dart index 8574844d..366c5968 100644 --- a/lib/src/widgets/phone_form_field.dart +++ b/lib/src/widgets/phone_form_field.dart @@ -90,11 +90,15 @@ class PhoneFormField extends FormField { /// 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, + this.focusNode, bool showFlagInInput = true, CountrySelectorNavigator selectorNavigator = const BottomSheetNavigator(), Function(PhoneNumber?)? onSaved, @@ -106,7 +110,6 @@ class PhoneFormField extends FormField { double flagSize = 16, PhoneNumberInputValidator? validator, // textfield inputs - FocusNode? focusNode, TextInputType keyboardType = TextInputType.phone, TextInputAction? textInputAction, TextStyle? style, @@ -160,7 +163,6 @@ class PhoneFormField extends FormField { builder: (state) { final field = state as _PhoneFormFieldState; return PhoneField( - focusNode: focusNode, controller: field._childController, showFlagInInput: showFlagInInput, selectorNavigator: selectorNavigator, @@ -228,6 +230,7 @@ class _PhoneFormFieldState extends FormFieldState { defaultIsoCode: widget.defaultCountry, isoCode: _controller.value?.isoCode, national: _getFormattedNsn(), + focusNode: widget.focusNode ?? FocusNode(), ); _controller.addListener(_onControllerChange); _childController.addListener(() => _onChildControllerChange()); From d1844da8ebe366e6f3e540a81b0234279efbc75c Mon Sep 17 00:00:00 2001 From: cedvdb Date: Mon, 18 Oct 2021 20:25:25 +0200 Subject: [PATCH 8/9] changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73790708..ee9cddcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## [4.3.0] 16 / 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 + ## [4.2.0] 16 / 10 / 2021 - [deprecated] PhoneValidator.invalid in favor of PhoneValidator.valid as the naming did not make sens and was backward. From 3f48195ca6aab6d44196b48163e11c00848589b0 Mon Sep 17 00:00:00 2001 From: cedvdb Date: Tue, 19 Oct 2021 00:02:23 +0200 Subject: [PATCH 9/9] changelog --- CHANGELOG.md | 4 +++- example/pubspec.lock | 2 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee9cddcf..650e0f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ -## [4.3.0] 16 / 10 / 2021 +## [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 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/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