Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to Add a Customizable Select Type #430

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/intl_phone_number_input.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
library intl_phone_number_input;

export 'src/models/country_model.dart';
export 'src/utils/phone_number.dart';
export 'src/widgets/input_widget.dart';
export 'src/utils/selector_config.dart';
export 'src/widgets/input_widget.dart';
4 changes: 4 additions & 0 deletions lib/src/utils/selector_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class SelectorConfig {
/// [selectorType], for selector button type
final PhoneInputSelectorType selectorType;

final Future<Country?> Function(BuildContext inheritedContext, List<Country> countries)?
showCustomSelectorDialog;

/// [showFlags], displays flag along side countries info on selector button
/// and list items within the selector
final bool showFlags;
Expand All @@ -38,6 +41,7 @@ class SelectorConfig {

const SelectorConfig({
this.selectorType = PhoneInputSelectorType.DROPDOWN,
this.showCustomSelectorDialog,
this.showFlags = true,
this.useEmoji = false,
this.countryComparator,
Expand Down
200 changes: 99 additions & 101 deletions lib/src/widgets/input_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import 'package:intl_phone_number_input/src/widgets/selector_button.dart';
/// * [PhoneInputSelectorType.DROPDOWN]
/// * [PhoneInputSelectorType.BOTTOM_SHEET]
/// * [PhoneInputSelectorType.DIALOG]
enum PhoneInputSelectorType { DROPDOWN, BOTTOM_SHEET, DIALOG }
/// * [PhoneInputSelectorType.CUSTOM]
enum PhoneInputSelectorType { DROPDOWN, BOTTOM_SHEET, DIALOG, CUSTOM }

/// A [TextFormField] for [InternationalPhoneNumberInput].
///
Expand Down Expand Up @@ -56,9 +57,14 @@ class InternationalPhoneNumberInput extends StatefulWidget {
final String? errorMessage;

final double selectorButtonOnErrorPadding;
final double selectorButtonPadding;

final Widget prefixIcon;

/// Ignored if [setSelectorButtonAsPrefixIcon = true]
final double spaceBetweenSelectorAndTextField;
final double selectorButtonBottomPadding;
final Decoration selectorDecoration;
final int maxLength;

final bool isEnabled;
Expand Down Expand Up @@ -103,7 +109,11 @@ class InternationalPhoneNumberInput extends StatefulWidget {
this.hintText = 'Phone number',
this.errorMessage = 'Invalid phone number',
this.selectorButtonOnErrorPadding = 24,
this.selectorButtonPadding = 0,
this.prefixIcon = const SizedBox(),
this.spaceBetweenSelectorAndTextField = 12,
this.selectorButtonBottomPadding = 0,
this.selectorDecoration = const BoxDecoration(),
this.maxLength = 15,
this.isEnabled = true,
this.formatInput = true,
Expand Down Expand Up @@ -133,15 +143,23 @@ class InternationalPhoneNumberInput extends StatefulWidget {

class _InputWidgetState extends State<InternationalPhoneNumberInput> {
TextEditingController? controller;
double selectorButtonBottomPadding = 0;

Country? country;
List<Country> countries = [];
bool isNotValid = true;

Size widgetSize = Size.zero;
final GlobalKey formFieldKey = GlobalKey();

@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
final RenderObject? renderBox = formFieldKey.currentContext!.findRenderObject();
setState(() {
widgetSize = renderBox!.semanticBounds.size;
});
});
loadCountries();
controller = widget.textFieldController ?? TextEditingController();
initialiseWidget();
Expand Down Expand Up @@ -181,12 +199,10 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
(await PhoneNumberUtil.isValidNumber(
phoneNumber: widget.initialValue!.phoneNumber!,
isoCode: widget.initialValue!.isoCode!))!) {
String phoneNumber =
await PhoneNumber.getParsableNumber(widget.initialValue!);
String phoneNumber = await PhoneNumber.getParsableNumber(widget.initialValue!);

controller!.text = widget.formatInput
? phoneNumber
: phoneNumber.replaceAll(RegExp(r'[^\d+]'), '');
controller!.text =
widget.formatInput ? phoneNumber : phoneNumber.replaceAll(RegExp(r'[^\d+]'), '');

phoneNumberControllerListener();
}
Expand All @@ -196,8 +212,7 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
/// loads countries from [Countries.countryList] and selected Country
void loadCountries({Country? previouslySelectedCountry}) {
if (this.mounted) {
List<Country> countries =
CountryProvider.getCountriesData(countries: widget.countries);
List<Country> countries = CountryProvider.getCountriesData(countries: widget.countries);

Country country = previouslySelectedCountry ??
Utils.getInitialSelectedCountry(
Expand All @@ -208,8 +223,7 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
// Remove potential duplicates
countries = countries.toSet().toList();

final CountryComparator? countryComparator =
widget.selectorConfig.countryComparator;
final CountryComparator? countryComparator = widget.selectorConfig.countryComparator;
if (countryComparator != null) {
countries.sort(countryComparator);
}
Expand All @@ -225,14 +239,11 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
/// the `ValueCallback` [widget.onInputValidated]
void phoneNumberControllerListener() {
if (this.mounted) {
String parsedPhoneNumberString =
controller!.text.replaceAll(RegExp(r'[^\d+]'), '');
String parsedPhoneNumberString = controller!.text.replaceAll(RegExp(r'[^\d+]'), '');

getParsedPhoneNumber(parsedPhoneNumberString, this.country?.alpha2Code)
.then((phoneNumber) {
getParsedPhoneNumber(parsedPhoneNumberString, this.country?.alpha2Code).then((phoneNumber) {
if (phoneNumber == null) {
String phoneNumber =
'${this.country?.dialCode}$parsedPhoneNumberString';
String phoneNumber = '${this.country?.dialCode}$parsedPhoneNumberString';

if (widget.onInputChanged != null) {
widget.onInputChanged!(PhoneNumber(
Expand Down Expand Up @@ -264,12 +275,11 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {

/// Returns a formatted String of [phoneNumber] with [isoCode], returns `null`
/// if [phoneNumber] is not valid or if an [Exception] is caught.
Future<String?> getParsedPhoneNumber(
String phoneNumber, String? isoCode) async {
Future<String?> getParsedPhoneNumber(String phoneNumber, String? isoCode) async {
if (phoneNumber.isNotEmpty && isoCode != null) {
try {
bool? isValidPhoneNumber = await PhoneNumberUtil.isValidNumber(
phoneNumber: phoneNumber, isoCode: isoCode);
bool? isValidPhoneNumber =
await PhoneNumberUtil.isValidNumber(phoneNumber: phoneNumber, isoCode: isoCode);

if (isValidPhoneNumber!) {
return await PhoneNumberUtil.normalizePhoneNumber(
Expand All @@ -288,6 +298,7 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
InputDecoration(
border: widget.inputBorder ?? UnderlineInputBorder(),
hintText: widget.hintText,
prefixIcon: widget.prefixIcon,
);

if (widget.selectorConfig.setSelectorButtonAsPrefixIcon) {
Expand All @@ -303,6 +314,7 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
isEnabled: widget.isEnabled,
autoFocusSearchField: widget.autoFocusSearch,
isScrollControlled: widget.countrySelectorScrollControlled,
prefixIcon: widget.prefixIcon,
));
}

Expand All @@ -318,21 +330,7 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
///
/// Also updates [selectorButtonBottomPadding]
String? validator(String? value) {
bool isValid =
this.isNotValid && (value!.isNotEmpty || widget.ignoreBlank == false);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (isValid && widget.errorMessage != null) {
setState(() {
this.selectorButtonBottomPadding =
widget.selectorButtonOnErrorPadding;
});
} else {
setState(() {
this.selectorButtonBottomPadding = 0;
});
}
});

bool isValid = this.isNotValid && (value!.isNotEmpty || widget.ignoreBlank == false);
return isValid ? widget.errorMessage : null;
}

Expand All @@ -346,11 +344,9 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {

void _phoneNumberSaved() {
if (this.mounted) {
String parsedPhoneNumberString =
controller!.text.replaceAll(RegExp(r'[^\d+]'), '');
String parsedPhoneNumberString = controller!.text.replaceAll(RegExp(r'[^\d+]'), '');

String phoneNumber =
'${this.country?.dialCode ?? ''}' + parsedPhoneNumberString;
String phoneNumber = '${this.country?.dialCode ?? ''}' + parsedPhoneNumberString;

widget.onSaved?.call(
PhoneNumber(
Expand All @@ -370,89 +366,91 @@ class _InputWidgetState extends State<InternationalPhoneNumberInput> {
String? get locale {
if (widget.locale == null) return null;

if (widget.locale!.toLowerCase() == 'nb' ||
widget.locale!.toLowerCase() == 'nn') {
if (widget.locale!.toLowerCase() == 'nb' || widget.locale!.toLowerCase() == 'nn') {
return 'no';
}
return widget.locale;
}
}

class _InputWidgetView
extends WidgetView<InternationalPhoneNumberInput, _InputWidgetState> {
class _InputWidgetView extends WidgetView<InternationalPhoneNumberInput, _InputWidgetState> {
final _InputWidgetState state;

_InputWidgetView({Key? key, required this.state})
: super(key: key, state: state);
_InputWidgetView({Key? key, required this.state}) : super(key: key, state: state);

@override
Widget build(BuildContext context) {
final countryCode = state.country?.alpha2Code ?? '';
final dialCode = state.country?.dialCode ?? '';

return Container(
return IntrinsicHeight(
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (!widget.selectorConfig.setSelectorButtonAsPrefixIcon) ...[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SelectorButton(
country: state.country,
countries: state.countries,
onCountryChanged: state.onCountryChanged,
selectorConfig: widget.selectorConfig,
selectorTextStyle: widget.selectorTextStyle,
searchBoxDecoration: widget.searchBoxDecoration,
locale: state.locale,
isEnabled: widget.isEnabled,
autoFocusSearchField: widget.autoFocusSearch,
isScrollControlled: widget.countrySelectorScrollControlled,
),
SizedBox(
height: state.selectorButtonBottomPadding,
),
],
Container(
decoration: widget.selectorDecoration,
height: state.widgetSize.height + widget.selectorButtonBottomPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SelectorButton(
width: widget.spaceBetweenSelectorAndTextField,
country: state.country,
countries: state.countries,
onCountryChanged: state.onCountryChanged,
selectorConfig: widget.selectorConfig,
selectorTextStyle: widget.selectorTextStyle,
searchBoxDecoration: widget.searchBoxDecoration,
locale: state.locale,
isEnabled: widget.isEnabled,
autoFocusSearchField: widget.autoFocusSearch,
isScrollControlled: widget.countrySelectorScrollControlled,
prefixIcon: widget.prefixIcon,
),
],
),
),
SizedBox(width: widget.spaceBetweenSelectorAndTextField),
],
Flexible(
child: TextFormField(
key: widget.fieldKey ?? Key(TestHelper.TextInputKeyValue),
textDirection: TextDirection.ltr,
controller: state.controller,
cursorColor: widget.cursorColor,
focusNode: widget.focusNode,
enabled: widget.isEnabled,
autofocus: widget.autoFocus,
keyboardType: widget.keyboardType,
textInputAction: widget.keyboardAction,
style: widget.textStyle,
decoration: state.getInputDecoration(widget.inputDecoration),
textAlign: widget.textAlign,
textAlignVertical: widget.textAlignVertical,
onEditingComplete: widget.onSubmit,
onFieldSubmitted: widget.onFieldSubmitted,
autovalidateMode: widget.autoValidateMode,
autofillHints: widget.autofillHints,
validator: widget.validator ?? state.validator,
onSaved: state.onSaved,
scrollPadding: widget.scrollPadding,
inputFormatters: [
LengthLimitingTextInputFormatter(widget.maxLength),
widget.formatInput
? AsYouTypeFormatter(
isoCode: countryCode,
dialCode: dialCode,
onInputFormatted: (TextEditingValue value) {
state.controller!.value = value;
},
)
: FilteringTextInputFormatter.digitsOnly,
],
onChanged: state.onChanged,
child: Container(
key: state.formFieldKey,
child: TextFormField(
key: widget.fieldKey ?? Key(TestHelper.TextInputKeyValue),
textDirection: TextDirection.ltr,
controller: state.controller,
cursorColor: widget.cursorColor,
focusNode: widget.focusNode,
enabled: widget.isEnabled,
autofocus: widget.autoFocus,
keyboardType: widget.keyboardType,
textInputAction: widget.keyboardAction,
style: widget.textStyle,
decoration: state.getInputDecoration(widget.inputDecoration),
textAlign: widget.textAlign,
textAlignVertical: widget.textAlignVertical,
onEditingComplete: widget.onSubmit,
onFieldSubmitted: widget.onFieldSubmitted,
autovalidateMode: widget.autoValidateMode,
autofillHints: widget.autofillHints,
validator: widget.validator ?? state.validator,
onSaved: state.onSaved,
scrollPadding: widget.scrollPadding,
inputFormatters: [
LengthLimitingTextInputFormatter(widget.maxLength),
widget.formatInput
? AsYouTypeFormatter(
isoCode: countryCode,
dialCode: dialCode,
onInputFormatted: (TextEditingValue value) {
state.controller!.value = value;
},
)
: FilteringTextInputFormatter.digitsOnly,
],
onChanged: state.onChanged,
),
),
)
],
Expand Down
Loading