From 653a9aa4f4e288431ce9cdb25843901975cff360 Mon Sep 17 00:00:00 2001 From: Bartek Date: Wed, 30 Sep 2026 11:10:04 +0200 Subject: [PATCH] Add PhoneInput component A type=tel input gets a country picker built on the Combobox, with flags, dial codes and search. It formats the number while typing, detects the country from a number with a dial code, and gives the full number in the international form. Raise the bundlewatch limits for tabler.js. --- .changeset/phone-input-component.md | 7 + core/js/src/phone-countries.ts | 100 +++++++ core/js/src/phone-input.ts | 379 +++++++++++++++++++++++++ core/js/tabler.ts | 2 + core/js/tests/unit/phone-input.spec.ts | 295 +++++++++++++++++++ core/package.json | 8 +- core/scss/_core.scss | 1 + core/scss/ui/forms/_phone-input.scss | 32 +++ docs/content/ui/forms/phone-input.mdx | 210 ++++++++++++++ preview/pages/phone-input.astro | 139 +++++++++ shared/data/docs.json | 4 + shared/data/menu.json | 5 + 12 files changed, 1178 insertions(+), 4 deletions(-) create mode 100644 .changeset/phone-input-component.md create mode 100644 core/js/src/phone-countries.ts create mode 100644 core/js/src/phone-input.ts create mode 100644 core/js/tests/unit/phone-input.spec.ts create mode 100644 core/scss/ui/forms/_phone-input.scss create mode 100644 docs/content/ui/forms/phone-input.mdx create mode 100644 preview/pages/phone-input.astro diff --git a/.changeset/phone-input-component.md b/.changeset/phone-input-component.md new file mode 100644 index 000000000..562211adc --- /dev/null +++ b/.changeset/phone-input-component.md @@ -0,0 +1,7 @@ +--- +"@tabler/core": minor +"@tabler/docs": minor +"@tabler/preview": minor +--- + +Added Phone input component: a `type="tel"` field with a country picker built on the Combobox, with flags and dial codes. diff --git a/core/js/src/phone-countries.ts b/core/js/src/phone-countries.ts new file mode 100644 index 000000000..c43407be9 --- /dev/null +++ b/core/js/src/phone-countries.ts @@ -0,0 +1,100 @@ +/** + * -------------------------------------------------------------------------- + * Tabler phone-countries.ts + * Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE) + * -------------------------------------------------------------------------- + */ + +// ISO 3166-1 alpha-2 code and international dial code of every country, as one +// string to keep the bundle small. Countries of the North American numbering +// plan share the dial code 1. +const DIAL_CODES = + 'AD376 AE971 AF93 AG1 AI1 AL355 AM374 AO244 AR54 AS1 AT43 AU61 AW297 AX358 AZ994 BA387 BB1 BD880 BE32 BF226 BG359 BH973 BI257 BJ229 BL590 BM1 BN673 BO591 BQ599 BR55 BS1 BT975 BW267 BY375 BZ501 ' + + 'CA1 CC61 CD243 CF236 CG242 CH41 CI225 CK682 CL56 CM237 CN86 CO57 CR506 CU53 CV238 CW599 CX61 CY357 CZ420 DE49 DJ253 DK45 DM1 DO1 DZ213 EC593 EE372 EG20 EH212 ER291 ES34 ET251 FI358 FJ679 FK500 FM691 FO298 FR33 ' + + 'GA241 GB44 GD1 GE995 GF594 GG44 GH233 GI350 GL299 GM220 GN224 GP590 GQ240 GR30 GT502 GU1 GW245 GY592 HK852 HN504 HR385 HT509 HU36 ID62 IE353 IL972 IM44 IN91 IO246 IQ964 IR98 IS354 IT39 JE44 JM1 JO962 JP81 ' + + 'KE254 KG996 KH855 KI686 KM269 KN1 KP850 KR82 KW965 KY1 KZ7 LA856 LB961 LC1 LI423 LK94 LR231 LS266 LT370 LU352 LV371 LY218 MA212 MC377 MD373 ME382 MF590 MG261 MH692 MK389 ML223 MM95 MN976 MO853 MP1 MQ596 MR222 MS1 ' + + 'MT356 MU230 MV960 MW265 MX52 MY60 MZ258 NA264 NC687 NE227 NF672 NG234 NI505 NL31 NO47 NP977 NR674 NU683 NZ64 OM968 PA507 PE51 PF689 PG675 PH63 PK92 PL48 PM508 PR1 PS970 PT351 PW680 PY595 QA974 RE262 RO40 RS381 RU7 ' + + 'RW250 SA966 SB677 SC248 SD249 SE46 SG65 SH290 SI386 SJ47 SK421 SL232 SM378 SN221 SO252 SR597 SS211 ST239 SV503 SX1 SY963 SZ268 TC1 TD235 TG228 TH66 TJ992 TK690 TL670 TM993 TN216 TO676 TR90 TT1 TV688 TW886 TZ255 ' + + 'UA380 UG256 US1 UY598 UZ998 VA39 VC1 VE58 VG1 VI1 VN84 VU678 WF681 WS685 XK383 YE967 YT262 ZA27 ZM260 ZW263' + +// The country picked when a number starts with a dial code that several countries share +const MAIN_COUNTRY_OF_DIAL: Record = { + '1': 'us', + '7': 'ru', + '39': 'it', + '44': 'gb', + '47': 'no', + '61': 'au', + '212': 'ma', + '262': 're', + '358': 'fi', + '590': 'gp', + '599': 'cw', +} + +// How the national number is written, per country: `#` is a digit. Countries +// that are not listed use the generic pattern. +const FORMATS: [string, string][] = [ + ['(###) ###-####', 'us ca pr'], + ['(###) ###-##-##', 'ru kz'], + ['(##) #####-####', 'br'], + ['##### ######', 'gb'], + ['### ########', 'de'], + ['## ## ## ## ##', 'fr'], + ['### ### ###', 'pl pt cz ec'], + ['#### ### ###', 'au ro sk'], + ['### ## ## ##', 'es'], + ['#### ## ## ##', 'be'], + ['### ### ####', 'it gr co ie fi ae sa za nz'], + ['## ########', 'nl'], + ['##-####-####', 'jp'], + ['###-####-####', 'kr'], + ['##### #####', 'in'], + ['## #### ####', 'mx'], + ['### #### ####', 'cn'], + ['### ### ## ##', 'tr ua ch'], + ['#### ######', 'at'], + ['### ## ###', 'no'], + ['## ## ## ##', 'dk'], + ['###-###-####', 'il'], + ['#### ####', 'sg hk'], + ['## ####-####', 'ar'], +] + +// The patterns above are the way the number is written inside the country, with the leading 0. +// A number in the international form has no 0, so it is put back for these countries. +const TRUNK_ZERO = new Set('gb fr de au nl jp kr be at ch tr ua ie fi ro sk il za nz ae sa'.split(' ')) + +const GENERIC_FORMAT = '### ### ### ###' + +const formatOfCountry = (iso: string): string => FORMATS.find(([, countries]) => countries.split(' ').includes(iso))?.[0] ?? GENERIC_FORMAT + +// Writes the digits into a pattern. A separator shows up only when a digit follows it, +// and digits that do not fit the pattern are grouped by three after it. +const formatNumber = (digits: string, pattern: string): string => { + let result = '' + let index = 0 + + for (const char of pattern) { + if (index >= digits.length) { + break + } + + result += char === '#' ? digits[index++] : char + } + + const rest = digits.slice(index) + return rest ? `${result} ${rest.replace(/(\d{3})(?=\d)/g, '$1 ')}` : result +} + +type PhoneCountry = { iso: string; dial: string } + +const PHONE_COUNTRIES: PhoneCountry[] = DIAL_CODES.split(' ').map((entry) => ({ + iso: entry.slice(0, 2).toLowerCase(), + dial: entry.slice(2), +})) + +const countryOfDial = (dial: string): string | undefined => MAIN_COUNTRY_OF_DIAL[dial] ?? PHONE_COUNTRIES.find((country) => country.dial === dial)?.iso + +export { countryOfDial, formatNumber, formatOfCountry, PHONE_COUNTRIES, TRUNK_ZERO } +export type { PhoneCountry } diff --git a/core/js/src/phone-input.ts b/core/js/src/phone-input.ts new file mode 100644 index 000000000..182e439e2 --- /dev/null +++ b/core/js/src/phone-input.ts @@ -0,0 +1,379 @@ +/** + * -------------------------------------------------------------------------- + * Tabler phone-input.ts + * Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE) + * -------------------------------------------------------------------------- + */ + +import BaseComponent from './bootstrap/base-component' +import EventHandler from './bootstrap/dom/event-handler' +import { initAll } from './bootstrap/util/component-functions' +import Combobox from './combobox' +import { countryOfDial, formatNumber, formatOfCountry, PHONE_COUNTRIES, TRUNK_ZERO } from './phone-countries' +import type { PhoneCountry } from './phone-countries' + +type ComponentConfig = { + autoDetect: boolean + container: string | HTMLElement | null + country: string + countryLabel: string + format: boolean + hiddenInput: string | null + locale: string | null + onlyCountries: string[] | string | null + preferredCountries: string[] | string + searchPlaceholder: string +} + +type ComponentConfigInput = Partial & Record + +/** + * Constants + */ + +const NAME = 'phoneInput' +const DATA_KEY = `bs.${NAME}` +const EVENT_KEY = `.${DATA_KEY}` + +const EVENT_COUNTRY_CHANGE = `countrychange${EVENT_KEY}` + +const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="phone-input"]' + +const MAX_DIAL_LENGTH = 4 +const MIN_DIGITS = 4 +const MAX_DIGITS = 15 + +const Default: ComponentConfig = { + autoDetect: true, + container: null, + country: 'us', + countryLabel: 'Country', + format: true, + hiddenInput: null, + locale: null, + onlyCountries: null, + preferredCountries: [], + searchPlaceholder: 'Search…', +} + +const DefaultType: Record = { + autoDetect: 'boolean', + container: '(string|element|null)', + country: 'string', + countryLabel: 'string', + format: 'boolean', + hiddenInput: '(string|null)', + locale: '(string|null)', + onlyCountries: '(array|string|null)', + preferredCountries: '(array|string)', + searchPlaceholder: 'string', +} + +// A list of country codes as an array or as a string like "de,at,ch" +const toCodes = (value: string[] | string): string[] => (Array.isArray(value) ? value : value.split(',')).map((code) => String(code).trim().toLowerCase()).filter(Boolean) + +/** + * Class definition + * + * A telephone `` gets a country picker in front of it. The picker is a + * Combobox built from a hidden `` + const input = fixtureEl.querySelector('#phone')! + return { input, phone: new PhoneInput(input, config) } + } + + const type = (input: HTMLInputElement, value: string): void => { + input.value = value + input.dispatchEvent(new Event('input', { bubbles: true })) + } + + const toggle = (): HTMLElement => fixtureEl.querySelector('.combobox-toggle')! + + describe('constructor', () => { + it('should put a country picker in front of the input', () => { + const { input } = setup('data-bs-country="pl"') + + expect(input.parentElement!.classList.contains('phone-input-group')).toBe(true) + expect(input.previousElementSibling!.classList.contains('dropdown-menu')).toBe(true) + expect(toggle().nextElementSibling!.classList.contains('phone-input-menu')).toBe(true) + expect(toggle().textContent).toContain('+48') + expect(toggle().querySelector('.flag-country-pl')).not.toBeNull() + expect(toggle().getAttribute('aria-label')).toBe('Country') + }) + + it('should fall back to the first country when the given one is unknown', () => { + const { phone } = setup('data-bs-country="zz"') + expect(phone.getCountry()).not.toBe('zz') + }) + + it('should list preferred countries first and only the allowed ones', () => { + const { phone } = setup('', { preferredCountries: ['pl', 'de'], onlyCountries: ['pl', 'de', 'fr', 'us'], country: 'pl' }) + const values = [...fixtureEl.querySelectorAll('.dropdown-item')].map((item) => item.dataset.bsValue) + + expect(values).toEqual(['pl', 'de', 'fr', 'us']) + expect(phone.getCountry()).toBe('pl') + }) + + it('should read the country lists from a comma separated string', () => { + const { phone } = setup('data-bs-only-countries="pl, de,fr" data-bs-preferred-countries="fr" data-bs-country="fr"') + const values = [...fixtureEl.querySelectorAll('.dropdown-item')].map((item) => item.dataset.bsValue) + + expect(values).toEqual(['fr', 'de', 'pl']) + expect(phone.getCountry()).toBe('fr') + }) + + it('should copy the input size to the picker', () => { + fixtureEl.innerHTML = '' + new PhoneInput(fixtureEl.querySelector('input')!) + expect(toggle().classList.contains('form-select-sm')).toBe(true) + }) + + it('should disable the picker with the input', () => { + setup('disabled') + expect(toggle().hasAttribute('disabled')).toBe(true) + }) + }) + + describe('numbers', () => { + it('should build the international number from the national digits', () => { + const { input, phone } = setup('data-bs-country="pl"') + + type(input, '600 100 200') + expect(phone.getNumber()).toBe('+48600100200') + }) + + it('should drop leading zeros of the national number', () => { + const { input, phone } = setup('data-bs-country="gb"') + + type(input, '07911 123456') + expect(phone.getNumber()).toBe('+447911123456') + }) + + it('should return an empty string for an empty input', () => { + expect(setup().phone.getNumber()).toBe('') + }) + + it('should remove characters that cannot be in a number', () => { + const { input } = setup('data-bs-format="false"') + + type(input, 'ab12c-3') + expect(input.value).toBe('12-3') + }) + + it('should validate the length', () => { + const { input, phone } = setup('data-bs-country="pl"') + + type(input, '12') + expect(phone.isValid()).toBe(false) + + type(input, '600100200') + expect(phone.isValid()).toBe(true) + + type(input, '1234567890123456') + expect(phone.isValid()).toBe(false) + }) + }) + + describe('formatting', () => { + it('should write the number the way it is written in the country', () => { + const { input, phone } = setup('data-bs-country="pl"') + + type(input, '600100200') + expect(input.value).toBe('600 100 200') + + phone.setCountry('us') + expect(input.value).toBe('(600) 100-200') + + type(input, '2025550100') + expect(input.value).toBe('(202) 555-0100') + expect(phone.getNumber()).toBe('+12025550100') + }) + + it('should show a separator only when a digit follows it', () => { + const { input } = setup('data-bs-country="us"') + + type(input, '20') + expect(input.value).toBe('(20') + + type(input, '2025') + expect(input.value).toBe('(202) 5') + }) + + it('should group the digits that do not fit the pattern', () => { + const { input } = setup('data-bs-country="pl"') + + type(input, '60010020012345') + expect(input.value).toBe('600 100 200 123 45') + }) + + it('should use a generic pattern for a country without one', () => { + const { input } = setup('data-bs-country="is"') + + type(input, '5551234') + expect(input.value).toBe('555 123 4') + }) + + it('should keep the cursor next to the same digit', () => { + const { input } = setup('data-bs-country="pl"') + + input.focus() + type(input, '600 100 200') + input.value = '600 1900 200' + input.setSelectionRange(6, 6) + input.dispatchEvent(new Event('input', { bubbles: true })) + + expect(input.value).toBe('600 190 020 0') + expect(input.selectionStart).toBe(6) + }) + + it('should put the leading zero back for a number in the international form', () => { + const { input, phone } = setup('data-bs-country="us"') + + type(input, '+44 7911 123456') + expect(phone.getCountry()).toBe('gb') + expect(input.value).toBe('07911 123456') + expect(phone.getNumber()).toBe('+447911123456') + + type(input, '+33 6 11 22 33 44') + expect(input.value).toBe('06 11 22 33 44') + expect(phone.getNumber()).toBe('+33611223344') + }) + + it('should not format when format is off', () => { + const { input } = setup('data-bs-country="pl" data-bs-format="false"') + + type(input, '600100200') + expect(input.value).toBe('600100200') + }) + }) + + describe('country detection', () => { + it('should switch the country when a number with a dial code is typed', () => { + const { input, phone } = setup('data-bs-country="us"') + const change = vi.fn() + input.addEventListener('countrychange.bs.phoneInput', change) + + type(input, '+48 600 100 200') + + expect(phone.getCountry()).toBe('pl') + expect(input.value).toBe('600 100 200') + expect(toggle().textContent).toContain('+48') + expect(change).toHaveBeenCalledOnce() + expect(phone.getNumber()).toBe('+48600100200') + }) + + it('should pick the main country of a shared dial code', () => { + const { input, phone } = setup('data-bs-country="pl"') + + type(input, '+1 202 555 0100') + expect(phone.getCountry()).toBe('us') + + type(input, '+44 7911 123456') + expect(phone.getCountry()).toBe('gb') + }) + + it('should keep the current country when it shares the dial code', () => { + const { input, phone } = setup('data-bs-country="ca"') + + type(input, '+1 613 555 0100') + expect(phone.getCountry()).toBe('ca') + }) + + it('should not detect the country when autoDetect is off', () => { + const { input, phone } = setup('data-bs-country="pl" data-bs-auto-detect="false"') + + type(input, '+1 202') + expect(phone.getCountry()).toBe('pl') + }) + + it('should wait for a complete dial code', () => { + const { input, phone } = setup('data-bs-country="us"') + + type(input, '+3') + expect(phone.getCountry()).toBe('us') + expect(input.value).toBe('+3') + }) + }) + + describe('country picker', () => { + it('should follow a choice made in the menu and focus the input', async () => { + const { input, phone } = setup('data-bs-country="us"') + const change = vi.fn() + input.addEventListener('countrychange.bs.phoneInput', change) + + toggle().click() + fixtureEl.querySelector('.dropdown-item[data-bs-value="de"]')!.click() + + expect(phone.getCountry()).toBe('de') + expect(phone.getDialCode()).toBe('49') + expect(toggle().textContent).toContain('+49') + expect(change).toHaveBeenCalledOnce() + await Promise.resolve() + expect(document.activeElement).toBe(input) + }) + + it('should set the country from code', () => { + const { phone } = setup() + + phone.setCountry('FR') + expect(phone.getCountry()).toBe('fr') + expect(toggle().textContent).toContain('+33') + + phone.setCountry('nope') + expect(phone.getCountry()).toBe('fr') + }) + + it('should set the whole number', () => { + const { input, phone } = setup() + + phone.setNumber('+49 30 1234567') + expect(phone.getCountry()).toBe('de') + expect(input.value).toBe('030 1234567') + expect(phone.getNumber()).toBe('+49301234567') + }) + }) + + describe('hidden input', () => { + it('should keep the full number in a hidden input', () => { + const { input } = setup('data-bs-country="pl" data-bs-hidden-input="phone_full"') + const hidden = fixtureEl.querySelector('input[name="phone_full"]')! + + type(input, '600 100 200') + expect(hidden.type).toBe('hidden') + expect(hidden.value).toBe('+48600100200') + }) + }) + + describe('dispose', () => { + it('should put the input back where it was', () => { + const { input, phone } = setup() + phone.dispose() + + expect(input.parentElement).toBe(fixtureEl) + expect(fixtureEl.querySelector('.phone-input')).toBeNull() + expect(fixtureEl.querySelector('.combobox-toggle')).toBeNull() + expect(PhoneInput.getInstance(input)).toBeNull() + }) + }) +}) diff --git a/core/package.json b/core/package.json index d5b4b3f01..29037b13b 100644 --- a/core/package.json +++ b/core/package.json @@ -212,19 +212,19 @@ }, { "path": "./dist/js/tabler.js", - "maxSize": "65 kB" + "maxSize": "69 kB" }, { "path": "./dist/js/tabler.min.js", - "maxSize": "43 kB" + "maxSize": "46 kB" }, { "path": "./dist/js/tabler.esm.js", - "maxSize": "65 kB" + "maxSize": "69 kB" }, { "path": "./dist/js/tabler.esm.min.js", - "maxSize": "43 kB" + "maxSize": "46 kB" }, { "path": "./dist/js/tabler-theme.js", diff --git a/core/scss/_core.scss b/core/scss/_core.scss index 34e77b25f..4585bc02e 100644 --- a/core/scss/_core.scss +++ b/core/scss/_core.scss @@ -69,6 +69,7 @@ @forward 'ui/forms/form-check'; @forward 'ui/forms/form-otp'; @forward 'ui/forms/combobox'; +@forward 'ui/forms/phone-input'; @forward 'ui/forms/validation'; @forward 'ui/forms/strength'; @forward 'ui/legend'; diff --git a/core/scss/ui/forms/_phone-input.scss b/core/scss/ui/forms/_phone-input.scss new file mode 100644 index 000000000..22fbc9fe5 --- /dev/null +++ b/core/scss/ui/forms/_phone-input.scss @@ -0,0 +1,32 @@ +@use '../../config' as *; + +.phone-input { + position: relative; +} + +.phone-input-group { + // A fixed width, so the field does not move when the dial code gets longer (+1 to +351) + > .combobox-toggle { + flex: 0 0 8.5rem; + width: 8.5rem; + + &.form-select-sm { + flex-basis: 8rem; + width: 8rem; + } + + &.form-select-lg { + flex-basis: 9rem; + width: 9rem; + } + } + + &:has(.is-valid) ~ .valid-feedback, + &:has(.is-invalid) ~ .invalid-feedback { + display: block; + } +} + +.phone-input-menu { + min-width: 18rem; +} diff --git a/docs/content/ui/forms/phone-input.mdx b/docs/content/ui/forms/phone-input.mdx new file mode 100644 index 000000000..51fd3ca1b --- /dev/null +++ b/docs/content/ui/forms/phone-input.mdx @@ -0,0 +1,210 @@ +--- +title: Phone input +seoTitle: Phone number input with country picker +order: 15 +summary: A telephone field with a country picker in front of it, showing flags and dial codes, with search, number formatting and international number output. +description: Add a phone number input with a searchable country picker, flags and dial codes. Formats the number as you type, detects the country from a pasted number and gives the full international number. +related: [/ui/forms/combobox, /ui/forms/elements, /ui/forms/validation] +source: core/scss/ui/forms/_phone-input.scss +css-plugins: [flags] +classnames: + component: + - class: phone-input + desc: Wrapper the script puts around the input; holds the hidden country select + - class: phone-input-group + desc: The input group that holds the country picker and the input + - class: phone-input-menu + desc: The country menu; wide enough for a country name and its dial code +--- + +import Example from '@components/Example.astro'; +import CodeDocs from '@components/CodeDocs.astro'; + +## Overview + +A phone input turns a normal `` into a field with a country picker in front of it. The picker is a [combobox](/ui/forms/combobox), so it has search, keyboard support and a flag next to every country. The input stays a plain text field. It holds the national number, and the script gives you the full number in the international format. + +The country flags come from the [flags](/ui/plugins/flags) plugin, so include `tabler-flags.css` on the page. Country names come from the browser, in the language of the page. + +Add `data-bs-toggle="phone-input"` to the input and pick the start country with `data-bs-country`. + + +
+ + +
+
+ +## Usage + +### Preferred countries + +List the countries your users come from first. The rest follow in alphabetical order. Pass two-letter country codes separated by commas. + + +
+ + +
+
+ +### Only some countries + +Limit the list with `data-bs-only-countries`. + + +
+ + +
+
+ +### Number with a dial code + +A number that starts with `+` selects its country and the dial code is removed from the input. This makes pasting an international number work. Set `data-bs-auto-detect="false"` to turn it off. + + +
+ + +
+
+ +### Formatting + +The number is written the way people write it in the chosen country, while the user types. A US number becomes `(202) 555-0100`, a Polish one `600 100 200`. The cursor stays next to the same digit when you edit the middle of the number, and a number that does not fit the pattern continues in groups of three. About 40 countries have their own pattern; the others use a generic one. + + +
+ + +
+
+ +Set `data-bs-format="false"` to keep what the user typed. Only digits, spaces, dashes, dots and brackets are allowed then. + + +
+ + +
+
+ +### Full number in a form + +The input holds only the national number. To submit the full number, set `data-bs-hidden-input` to a field name. The script adds a hidden input with that name and keeps the number in the international format, for example `+48600100200`, in it. + + +
+ + +
+
+ +### Sizes + +The picker follows `.form-control-sm` and `.form-control-lg` of the input. + + +
+ + +
+
+ + +
+
+ + +
+
+ +### Validation + +Add `.is-valid` or `.is-invalid` to the input. The feedback shows below the whole field. + + +
+
+ + +
Looks good.
+
+
+ + +
Enter a full phone number.
+
+
+
+ +### Disabled + +Add `disabled` to the input. The picker is disabled too. + + +
+ + +
+
+ +## JavaScript + +### Initialization + +The script starts on every `[data-bs-toggle="phone-input"]` on page load. + + + +### Options + +Pass options as `data-bs-*` attributes on the input, or as an object when creating the instance. + +| Name | Type | Default | Description | +| --- | --- | --- | --- | +| `country` | string | `'us'` | Two-letter code of the country shown first. | +| `preferredCountries` | array \| string | `[]` | Codes of the countries listed first. | +| `onlyCountries` | array \| string \| null | `null` | Limits the list to these countries. | +| `autoDetect` | boolean | `true` | Selects the country of a number that starts with `+`. | +| `format` | boolean | `true` | Writes the number in the format of the chosen country while typing. | +| `hiddenInput` | string \| null | `null` | Name of a hidden input that gets the full number. | +| `locale` | string \| null | `null` | Language of the country names. Uses the `lang` of the page when empty. | +| `countryLabel` | string | `'Country'` | Accessible name of the picker. | +| `searchPlaceholder` | string | `'Search…'` | Placeholder of the search field in the menu. | +| `container` | string \| element | `null` | Where to append the menu, for example `'body'`. | + +### Methods + +| Method | Description | +| --- | --- | +| `getNumber()` | The full number, for example `+48600100200`, or an empty string. Leading zeros of the national number are dropped. | +| `getCountry()` | The two-letter code of the chosen country, in lower case. | +| `getDialCode()` | The dial code of the chosen country, without `+`. | +| `setCountry(code)` | Selects a country. An unknown code is ignored. | +| `setNumber(number)` | Sets the number. A dial code at the start selects the country. | +| `isValid()` | A quick length check: 4 to 15 digits in total. It does not know the numbering plan of each country. | +| `dispose()` | Puts the input back where it was and removes the picker. | +| `getInstance(element)` | Static. Returns the instance bound to the element, or `null`. | +| `getOrCreateInstance(element, config)` | Static. Returns the existing instance, or creates one. | + +### Events + +Events fire on the input. + +| Event | Description | +| --- | --- | +| `countrychange.bs.phoneInput` | Fires when the country changes, from the menu, from a number with a dial code or from `setCountry()`. `event.country` and `event.dialCode` hold the new values. | + +```javascript +input.addEventListener('countrychange.bs.phoneInput', (event) => { + console.log(event.country, event.dialCode) +}) +``` + +## Accessibility + +- Give the input a visible `