CoUI PhoneInput#
ํ๋ง๋๋ก (๋น๊ฐ๋ฐ์์ฉ)#
์ด ์คํฌ์ ํ๋ฉด์ "์ ํ๋ฒํธ ์ ๋ ฅ์นธ"์ ๋ง๋ค ๋ ์ฐ๋ ์ฌ์ฉ๋ฒ ์ค๋ช ์์ ๋๋ค. ๊ตญ๊ฐ ๊น๋ฐ๊ณผ ๊ตญ๊ฐ๋ฒํธ(์: ํ๊ตญ +82)๋ฅผ ๊ณ ๋ฅด๋ ๋ฒํผ๊ณผ ๋ฒํธ ์ ๋ ฅ์นธ์ด ํ ์นธ ์์ ํฉ์ณ์ง ํํ์์. ๋ง์น ํด์ธ ๋ฐฐ์ก ์ฃผ๋ฌธ์์์ ๋๋ผ๋ฅผ ๋จผ์ ๊ณ ๋ฅด๋ฉด ์์๋ฆฌ ๋ฒํธ๊ฐ ์๋์ผ๋ก ์ฑ์์ง๋ ๊ฒ๊ณผ ๊ฐ์ต๋๋ค. ๊ฐ๋ฐ์๋ AI๊ฐ ์ด ์ ๋ ฅ์นธ์ ์ผ๊ด๋ ๋ชจ์์ผ๋ก ๋น ๋ฅด๊ฒ ๋ง๋ค ์ ์๊ฒ ๋์์ค๋๋ค.
๋ฌด์์ยท์ธ์ (๋น๊ฐ๋ฐ์์ฉ)#
-
๋ฌด์์ ํด์ฃผ๋์: ๊ตญ๊ฐ ์ ํ ๋ฒํผ๊ณผ ์ ํ๋ฒํธ ์
๋ ฅ์นธ์ ํ๋๋ก ๋ฌถ์ ์ปดํฌ๋ํธ(
PhoneInput)๋ฅผ ๋ง๋๋ ๋ฐฉ๋ฒ์ ์๋ ค์ค๋๋ค. ๋๋ผ๋ฅผ ๊ณ ๋ฅด๋ฉด ๊ตญ๊ฐ๋ฒํธ ์์๋ฆฌ๊ฐ ์๋์ผ๋ก ๋ฐ๋๊ณ , ๊ธฐ๋ณธ์ ์ผ๋ก ์ซ์๋ง ์ ๋ ฅ๋๋๋ก ๋ง์์ค๋๋ค. ๊ตญ๊ฐ ์ ํ์ฐฝ ์์๋ ๊ฒ์์นธ์ด ์์ด ๋๋ผ ์ด๋ฆยท๊ตญ๊ฐ๋ฒํธยท๊ตญ๊ฐ์ฝ๋๋ก ๋ฐ๋ก ์ฐพ์ ์ ์์ต๋๋ค. - ์ธ์ ์๋ํ๋์: ํ์๊ฐ์ , ๋ณธ์ธ ์ธ์ฆ, ์ฐ๋ฝ์ฒ ์ ๋ ฅ์ฒ๋ผ "์ ํ๋ฒํธ๋ฅผ ๋ฐ๋ ํ๋ฉด"์ ๋ง๋ค ๋ ์ด ์คํฌ์ด ์ฐธ๊ณ ๋ฉ๋๋ค.
- ์ด๋์์ ์ฐ๋์: ๋ชจ๋ฐ์ผ ์ฑ(Flutter)๊ณผ ์น์ฌ์ดํธ(Web) ๋ ๊ณณ ๋ชจ๋์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์ธ ์ ์์ต๋๋ค.
- ๋ฌด์์ ์กฐ์ ํ ์ ์๋์: ๋ณด์ฌ์ค ๋๋ผ ๋ชฉ๋ก ์ ํ, ์๋ด ๋ฌธ๊ตฌ(placeholder), ์ ๋ ฅ์นธ ๋ชจ์(๋ฅ๊ทผ ์ ๋ยท๋์ดยท๊ฐ๊ฒฉยท์์ ๋ฑ), ๋นํ์ฑํ ์ํ ๋ฑ์ ์์ ๋กญ๊ฒ ๋ฐ๊ฟ ์ ์์ต๋๋ค.
ํต์ฌ ์ฉ์ด (๋น๊ฐ๋ฐ์์ฉ)#
| ์ฉ์ด | ์ฌ์ด ์ค๋ช |
|---|---|
| dial code (๊ตญ๊ฐ๋ฒํธ) | ๋๋ผ๋ง๋ค ์ ํด์ง ์ ํ ์์๋ฆฌ ๋ฒํธ. ์: ํ๊ตญ +82, ๋ฏธ๊ตญ +1 |
| country / CoreCountry | ์ ํํ ๋๋ผ ์ ๋ณด(๊ตญ๊ฐ์ฝ๋ยท๊ตญ๊ฐ๋ฒํธยท์ด๋ฆ)๋ฅผ ๋ด๋ ๋จ์ |
| placeholder | ์ ๋ ฅ์นธ์ด ๋น์ด ์์ ๋ ํ๋ฆฌ๊ฒ ๋ณด์ด๋ ์๋ด ๋ฌธ๊ตฌ. ์: "์ ํ๋ฒํธ" |
| onChanged | ๋๋ผ๋ ๋ฒํธ๊ฐ ๋ฐ๋ ๋๋ง๋ค ์๋ ค์ฃผ๋ ์ ํธ(์ฐ๊ฒฐ ๋์) |
| fullNumber | ๊ตญ๊ฐ๋ฒํธ์ ๋ฒํธ๋ฅผ ํฉ์น ์ ์ฒด ๋ฒํธ. ์: +821012345678 |
| enabled / disabled | ์ ๋ ฅ์นธ์ ์ผ๊ณ (์ฌ์ฉ ๊ฐ๋ฅ) ๋๋(ํ๋ฆฌ๊ฒ ๋นํ์ฑํ) ์ํ |
| style / theme | ์ ๋ ฅ์นธ์ ๋ชจ์ยท์ยทํฌ๊ธฐ ๊ฐ์ ๋์์ธ ์ค์ |
| Flutter / Web(Jaspr) | ๊ฐ๊ฐ ๋ชจ๋ฐ์ผ ์ฑ๊ณผ ์น์ฌ์ดํธ๋ฅผ ๋ง๋๋ ๊ธฐ์ |
Overview#
PhoneInput is a form component that combines a country selector trigger and a phone number text field inside a single bordered container. Tapping the selector opens a country picker (search input + country list) that both platforms share as the same UX: search matches country name, dial code, or ISO code (case-insensitive). Selecting a country updates the dial code prefix; the number field accepts only digits by default. The Flutter and Web implementations share the same
CorePhoneInputContract interface and CorePhoneInputStyle design tokens.
Resolve priority: widget.phoneInputStyle > CorePhoneInputTheme.style (project theme) > design system defaults.
Variants & sizes#
PhoneInput has no variant or size enum (no CoreComponentSize
axis). It is a single-form component; every visual knob flows through the single style slot phoneInputStyle: CorePhoneInputStyle
(full field set below).
| Axis | Values |
|---|---|
| variant | โ (none) |
| size | โ (none; use CorePhoneInputStyle.height, default CoreSpace.space40) |
| style slot | phoneInputStyle: CorePhoneInputStyle |
Flutter (coui_flutter)#
Import#
import 'package:coui_flutter/coui_flutter.dart';
The Country enum (and Countries helper) is re-exported from the phonecodes
package through the same barrel โ no extra import needed.
Basic usage#
PhoneInput(
initialCountry: Country.unitedStates,
onChanged: (phoneNumber) {
// phoneNumber.fullNumber โ '+11234567890'
// phoneNumber.number โ '1234567890' (without dial code)
// phoneNumber.value โ null when empty, fullNumber otherwise
print(phoneNumber.fullNumber);
},
)
With initial value#
PhoneInput(
initialValue: PhoneNumber(Country.southKorea, '1012345678'),
onChanged: (phoneNumber) => print(phoneNumber.fullNumber),
)
PhoneNumberuses positional parameters:PhoneNumber(country, number). WheninitialValueis provided, itscountrytakes precedence overinitialCountryand itsnumberoverinitialNumber.
With placeholder and restricted country list#
PhoneInput(
initialCountry: Country.southKorea,
placeholder: 'Phone number',
countries: const [Country.southKorea, Country.unitedStates, Country.japan],
onChanged: (phoneNumber) => print(phoneNumber.fullNumber),
)
With external controller#
final _controller = TextEditingController();
PhoneInput(
controller: _controller,
initialCountry: Country.unitedStates,
onChanged: (phoneNumber) => print(phoneNumber.fullNumber),
)
An internal controller is created (and disposed) automatically when controller is null.
Disabled state#
PhoneInput(
initialCountry: Country.unitedStates,
enabled: false,
)
Disabled rendering wraps the component in IgnorePointer + Opacity (default opacity
0.5), matching Web's container-level opacity.
Style override via phoneInputStyle#
PhoneInput(
initialCountry: Country.unitedStates,
phoneInputStyle: const CorePhoneInputStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
height: CoreSpace.space48,
selectorPadding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16),
inputFieldPadding: CoreEdgeInsets.symmetric(horizontal: CoreSpace.space16),
flagSize: 20.0,
dropdownFlagSize: 20.0,
hoverColor: CoreColor.token(CoreColors.tertiary),
),
onChanged: (phoneNumber) => print(phoneNumber.fullNumber),
)
Project-level theme (CorePhoneInputTheme)#
// Inside your CoUI theme setup:
CoreComponentTheme(
phoneInput: CorePhoneInputTheme(
style: CorePhoneInputStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius4),
height: CoreSpace.space40,
),
),
)
Parameters โ PhoneInput (Flutter)#
| Parameter | Type | Default | Description |
|---|---|---|---|
initialCountry |
Country? |
null (falls back to Country.unitedStates) |
Country shown when no initialValue is provided |
initialNumber |
String? |
null |
Phone number text (without dial code) used as initial field content |
initialValue |
PhoneNumber? |
null |
Combined initial value;
country
overrides
initialCountry
,
number
overrides
initialNumber
|
onChanged |
ValueChanged<PhoneNumber>? |
null |
Called on every country or number change |
controller |
TextEditingController? |
null |
External text controller; an internal one is created when null |
placeholder |
String? |
null |
Hint text rendered inside the number field when empty |
countries |
List<Country>? |
null (โ Country.values) |
Allowed countries shown in the picker; null means all |
enabled |
bool |
true |
Disables interaction and applies disabled opacity when false |
onlyNumber |
bool |
true |
keyboardType: phone + FilteringTextInputFormatter.digitsOnly |
filterPlusCode |
bool |
true |
Strips leading dial code (e.g. +1) or bare + from typed input |
filterZeroCode |
bool |
true |
Strips a leading 0 from typed input |
filterCountryCode |
bool |
true |
Strips the numeric country-code prefix (without +) from typed input |
phoneInputStyle |
CorePhoneInputStyle? |
null |
Per-instance style override |
The three
filter*checks run as an if/else-if chain in this order โfilterPlusCode(dial code with+, then bare+) โfilterZeroCodeโfilterCountryCodeโ so at most one filter applies per value read.
Behavior notes (Flutter)#
-
Country picker popup: opens anchored below the selector trigger (popover, no vertical invert) with a
Search countryfield on top and the country list below. Search matchesname,dialCode, orcode(case-insensitive). An empty result shows a mutedNo resultshint. -
Form integration: the state mixes in
FormValueSupplier<PhoneNumber, PhoneInput>, soPhoneInputparticipates in the CoUI form value system automatically; a replaced form value is written back into the text controller. -
Autofill: the number field sets
autofillHints: [AutofillHints.telephoneNumber].
PhoneNumber (value object, Flutter)#
| Member | Type | Description |
|---|---|---|
country | Country | Selected country enum value |
number | String | Phone number without dial code |
fullNumber |
String |
dialCode + number (e.g. +821012345678) |
value |
String? |
null when number is empty, fullNumber otherwise |
toString() |
String |
'' when empty, fullNumber otherwise |
Country enum (from phonecodes, re-exported)#
Use Country.<camelCaseName> enum values โ e.g. Country.unitedStates,
Country.southKorea, Country.japan, Country.unitedKingdom. Use
Country.values for the full list. Each value exposes name, code,
dialCode, and flag (emoji string).
Note: Korea is
Country.southKoreaโ there is noCountry.koreamember.
Web / Jaspr (coui_web)#
Import#
import 'package:coui_web/coui_web.dart';
Basic usage#
PhoneInput(
initialCountry: const CoreCountry(code: 'US', dialCode: '+1', name: 'United States'),
placeholder: 'Phone number',
onChanged: (value) {
// value.fullNumber โ '+11234567890'
// value.number โ '1234567890'
// value.countryCode โ 'US'
// value.dialCode โ '+1'
print(value.fullNumber);
},
)
With predefined default countries#
PhoneInput.defaultCountries contains 10 common countries (US, KR, JP, GB, DE, FR, CN, IN, CA, AU). When
countries is null, this list is used.
PhoneInput(
initialCountry: const CoreCountry(code: 'KR', dialCode: '+82', name: 'South Korea'),
countries: PhoneInput.defaultCountries,
onChanged: (value) => print(value.fullNumber),
)
With custom country list#
PhoneInput(
initialCountry: const CoreCountry(code: 'KR', dialCode: '+82', name: 'South Korea'),
countries: const [
CoreCountry(code: 'KR', dialCode: '+82', name: 'South Korea'),
CoreCountry(code: 'US', dialCode: '+1', name: 'United States'),
CoreCountry(code: 'JP', dialCode: '+81', name: 'Japan'),
],
onChanged: (value) => print(value.fullNumber),
)
With HTML attributes and CSS#
PhoneInput(
id: 'signup-phone',
classes: 'my-phone-input',
initialCountry: const CoreCountry(code: 'US', dialCode: '+1', name: 'United States'),
attributes: const {'data-testid': 'phone-input'},
onChanged: (value) => print(value.fullNumber),
)
A caller-supplied
idbecomes the root element id and the outside-click scope for closing the dropdown; when omitted, a generatedco-phone-input-Nid is used.
Style override via phoneInputStyle#
PhoneInput(
initialCountry: const CoreCountry(code: 'US', dialCode: '+1', name: 'United States'),
phoneInputStyle: const CorePhoneInputStyle(
borderRadius: CoreBorderRadius.all(CoreRadius.radius8),
height: CoreSpace.space48,
popupMaxWidth: CoreSpace.space248,
popupMaxHeight: CoreSpace.space248,
),
onChanged: (value) => print(value.fullNumber),
)
Parameters โ PhoneInput (Web)#
| Parameter | Type | Default | Description |
|---|---|---|---|
initialCountry |
CoreCountry? |
null
(falls back to
CoreCountry(code: 'US', dialCode: '+1', name: 'United States')
)
|
Country shown initially |
initialNumber |
String? |
null |
Phone number text (without dial code) used as initial field value |
onChanged |
CoreValueChanged<PhoneInputValue>? |
null |
Called on every country or number change |
onlyNumber |
bool |
true |
Sets inputmode="numeric" (otherwise inputmode="tel") on the number field |
placeholder |
String? |
null |
HTML placeholder attribute on the number field |
countries |
List<CoreCountry>? |
null (โ PhoneInput.defaultCountries) |
Countries available in the picker |
enabled |
bool |
true |
Adds disabled attribute and applies disabled opacity on the root when false |
phoneInputStyle |
CorePhoneInputStyle? |
null |
Per-instance style override |
id |
String? |
null |
HTML id for the root <div> (also the outside-click scope) |
classes |
String? |
null |
Additional CSS classes for the root element |
css |
Styles? |
null |
Inline CSS styles merged onto the root element |
attributes |
Map<String, String>? |
null |
Extra HTML attributes for the root element |
eventHandlers |
Map<String, List<UiEventHandler>> |
{} |
Custom DOM event handlers for the root element |
onClick / onMouseEnter / onMouseLeave |
UiMouseEventHandler? |
null |
Root-level mouse event passthrough |
onKeyDown / onKeyUp |
UiKeyboardEventHandler? |
null |
Root-level keyboard event passthrough |
onInput / onChange |
UiInputEventHandler? |
null |
Root-level input event passthrough |
Web does not use
TextEditingControllerโ the contract'scontrollerfield is typedNulland always returnsnull.
Behavior notes (Web)#
- The number field renders as
<input type="tel" autocomplete="tel">. -
The dropdown (
role="listbox") renders only while open, with aSearch countryinput on top; clicking outside the component closes it. Each country row is a<button role="option">. -
Accessibility: the selector trigger carries
aria-label="Select country",aria-haspopup="listbox", andaria-expanded; the search field carriesaria-label="Search country". -
When the parent swaps
initialCountryto a new non-null value, the selected country follows (didUpdateComponent). -
PhoneInput.flagEmoji(code)(delegating toCoreCountry.flagEmoji) converts an ISO 2-letter code to its flag emoji.
PhoneInputValue (value object, Web)#
| Member | Type | Description |
|---|---|---|
countryCode |
String |
ISO 2-letter code (e.g. 'KR') |
dialCode |
String |
Dial code with prefix (e.g. '+82') |
number |
String |
Phone number without dial code (default '') |
fullNumber | String | dialCode + number |
value |
String? |
null when number is empty, fullNumber otherwise |
Constructor uses named parameters: PhoneInputValue(countryCode: 'KR', dialCode: '+82', number: '1012345678').
CoreCountry (value object, from coui_core)#
const CoreCountry(code: 'KR', dialCode: '+82', name: 'South Korea')
| Field | Type | Description |
|---|---|---|
code | String | ISO 2-letter country code |
dialCode |
String |
Dial code with + prefix |
name | String | Localized country name |
Use CoreCountry.flagEmoji(code) to convert a code string to its flag emoji (returns ''
when code is not exactly 2 characters).
CorePhoneInputStyle fields (single style slot)#
All fields are optional overrides merged over the chain defaults โ CorePhoneInputTheme.style โ phoneInputStyle. Defaults are static fields on
CorePhoneInputStyle (static const, except defaultDialCodeTextStyle
which is static final) โ the single source of truth shared by both platforms.
| Field | Type | Default |
|---|---|---|
borderRadius |
CoreBorderRadius? |
CoreBorderRadius.all(CoreRadius.radius4) |
flagGapStyle |
CoreGapStyle? |
CoreGapStyle(size: CoreSpace.space8, crossAxisExtent: 0) |
countryGapStyle |
CoreGapStyle? |
CoreGapStyle(size: CoreSpace.space16, crossAxisExtent: 0) |
maxWidth | double? | 200.0 |
padding |
CoreEdgeInsets? |
null (flush row layout) |
height | double? | CoreSpace.space40 |
borderWidth |
double? |
CoreStrokeWidth.stroke1 |
inputFieldPadding |
CoreEdgeInsets? |
CoreEdgeInsets.symmetric(horizontal: CoreSpace.space12) |
selectorPadding |
CoreEdgeInsets? |
CoreEdgeInsets.symmetric(horizontal: CoreSpace.space12) |
dropdownItemPadding |
CoreEdgeInsets? |
CoreEdgeInsets.symmetric(horizontal: CoreSpace.space12, vertical: CoreSpace.space8) |
dropdownItemGapStyle |
CoreGapStyle? |
CoreGapStyle(size: CoreSpace.space8, crossAxisExtent: 0) |
popupMaxWidth |
double? |
CoreSpace.space248 |
popupMaxHeight |
double? |
CoreSpace.space248 |
popoverTopOffset |
double? |
CoreSpace.space4 |
chevronIconStyle |
CoreIconStyle? |
CoreIconStyle(size: 12.0) |
flagSize | double? | 16.0 |
dropdownFlagSize | double? | 16.0 |
disabledOpacity | double? | 0.5 |
popupBorderRadius |
CoreBorderRadius? |
CoreBorderRadius.all(CoreRadius.radius8) |
dropdownDialCodeTextStyle |
CoreTextStyle? |
CoreTextStyle.token(CoreTextStyles.bodySmall, color: CoreColor.token(CoreColors.onSurfaceVariant))
|
backgroundColor |
CoreColor? |
CoreColor.token(CoreColors.surface) |
borderColor |
CoreColor? |
CoreColor.token(CoreColors.outlineVariant) |
hoverColor |
CoreColor? |
CoreColor.token(CoreColors.tertiary) |
separatorColor |
CoreColor? |
CoreColor.token(CoreColors.outlineVariant) |
placeholderColor |
CoreColor? |
CoreColor.token(CoreColors.onSurfaceVariant) |
chevronColor |
CoreColor? |
CoreColor.token(CoreColors.onSurface) |
disabledChevronColor |
CoreColor? |
CoreColor.token(CoreColors.onSurfaceVariant) |
dialCodeTextStyle |
CoreTextStyle? |
labelLarge role + medium weight + onSurface color |
disabledDialCodeColor |
CoreColor? |
CoreColor.token(CoreColors.onSurfaceVariant) |
flagTextStyle |
CoreTextStyle? |
CoreTextStyle.token(CoreTextStyles.labelLarge) |
bodyTextStyle |
CoreTextStyle? |
CoreTextStyle.token(CoreTextStyles.bodySmall) |
CorePhoneInputStyle also provides merge(other) and copyWith(...).
Flutter โ Web differences#
| Concept | Flutter | Web (Jaspr) |
|---|---|---|
| Widget class | PhoneInput (extends StatefulWidget) |
PhoneInput (extends UiStatefulComponent) |
| Contract | CorePhoneInputContract<TextEditingController?, PhoneNumber, Country> |
CorePhoneInputContract<Null, PhoneInputValue, CoreCountry> |
| Country type | Country (enum, from phonecodes) |
CoreCountry (value object, from coui_core) |
Value type in onChanged |
PhoneNumber (positional ctor) |
PhoneInputValue (named ctor) |
| Callback type | ValueChanged<PhoneNumber>? |
CoreValueChanged<PhoneInputValue>? |
| Controller | TextEditingController? |
Null (always null, not supported) |
| Default country fallback | Country.unitedStates |
CoreCountry(code: 'US', dialCode: '+1', name: 'United States') |
| Country list default | Country.values (all countries) |
PhoneInput.defaultCountries (10 common countries) |
| Input filters | filterPlusCode / filterZeroCode / filterCountryCode params |
โ (no filter params) |
initialValue (combined) |
supported (PhoneNumber?) |
โ (use initialCountry + initialNumber) |
onlyNumber effect |
phone keyboard + digits-only formatter | inputmode="numeric" vs "tel" |
| Disabled rendering | IgnorePointer + Opacity(disabledOpacity) |
disabled attribute + opacity inline CSS on root |
| Picker presentation | popover overlay below the trigger | absolutely-positioned <div role="listbox"> sibling |
| HTML extras | โ |
id
,
classes
,
css
,
attributes
,
eventHandlers
,
onClick
,
onInput
,
onChange
,
onKeyDown
,
onKeyUp
,
onMouseEnter
,
onMouseLeave
|
| Flag rendering | country.flag emoji string |
PhoneInput.flagEmoji(code) / CoreCountry.flagEmoji(code) |
| Form integration | FormValueSupplier<PhoneNumber, PhoneInput> |
โ |
Shared across both platforms: CorePhoneInputStyle, CorePhoneInputTheme, CorePhoneInputContract, search-driven country picker UX, and the resolve chain
defaults โ theme.style โ phoneInputStyle.
v0.127 Migration (Legacy)#
v0.127 removed the Co prefix from widget classes (hard break โ no aliases). Core*
style/theme/contract names and the value objects are unchanged.
| Legacy (โ removed) | v0.127 Canonical |
|---|---|
โ CoPhoneInput | PhoneInput |
โ CoPhoneInput.defaultCountries |
PhoneInput.defaultCountries |
โ CoPhoneInput.flagEmoji | PhoneInput.flagEmoji |
โ CoLucideIcons (internal chevron icon set) | LucideIcons |
โ per-component src/ imports |
single barrel:
package:coui_flutter/coui_flutter.dart
/
package:coui_web/coui_web.dart
|
Unchanged names: CorePhoneInputStyle, CorePhoneInputTheme, CorePhoneInputContract,
PhoneNumber, PhoneInputValue, CoreCountry, Country.