svelte-tel-input 4.0.0-next.0 → 4.0.0-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/components/input/TelInput.svelte +58 -26
- package/dist/types/index.d.ts +39 -4
- package/dist/utils/helpers.js +1 -0
- package/dist/validators/index.js +2 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Parse, format, and validate international phone numbers — store in [E.164](htt
|
|
|
19
19
|
- **E.164 storage** — one canonical format, always searchable and SMS-ready
|
|
20
20
|
- **Auto-detect country** from dial code (`+44` → `GB`)
|
|
21
21
|
- **Smart formatting** — international display with cursor position preservation
|
|
22
|
-
- **Validation** — powered
|
|
22
|
+
- **Validation** — powered with granular error types
|
|
23
23
|
- **Auto placeholder** — country-specific example numbers
|
|
24
24
|
- **Allowed countries** — restrict to a subset of country codes
|
|
25
25
|
- **Lock country** — prevent country switching via dial codes
|
|
@@ -88,3 +88,7 @@ Full API reference, options, events, types, and examples are on the **[documenta
|
|
|
88
88
|
## License
|
|
89
89
|
|
|
90
90
|
[MIT](LICENSE.md)
|
|
91
|
+
|
|
92
|
+
## Acknowledgements
|
|
93
|
+
|
|
94
|
+
Phone number parsing and validation is powered by [libphonenumber-js](https://gitlab.com/catamphetamine/libphonenumber-js) by [catamphetamine](https://gitlab.com/catamphetamine) and its contributors. This project wouldn't be possible without their work. Thank you for their work.
|
|
@@ -103,11 +103,10 @@
|
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*/
|
|
106
|
+
// Compute the initial formatted display value synchronously.
|
|
107
|
+
// Runs on both server and client at initialization time, so SSR renders the
|
|
108
|
+
// formatted number and the client's first render matches — no hydration mismatch.
|
|
109
|
+
|
|
111
110
|
const computeInitialDisplayValue = (): string => {
|
|
112
111
|
if (!value) return '';
|
|
113
112
|
let effectiveCountryIso2: CountryCode | null = null;
|
|
@@ -139,18 +138,16 @@
|
|
|
139
138
|
if (prevCountry === undefined) prevCountry = country;
|
|
140
139
|
});
|
|
141
140
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
* Initialized to the incoming prop values so the first render never false-fires.
|
|
148
|
-
*/
|
|
141
|
+
//Shadow trackers — record every value this component writes internally so the
|
|
142
|
+
//external-change watchers below can distinguish "we set it" vs "parent set it".
|
|
143
|
+
//Plain let (not $state) because they are only ever read inside untrack(), never
|
|
144
|
+
//as reactive dependencies.
|
|
145
|
+
//Initialized to the incoming prop values so the first render never false-fires.
|
|
149
146
|
let _lastWrittenValue: string = value;
|
|
150
147
|
let _lastWrittenCountry: CountryCode | null | undefined = untrack(() => country);
|
|
151
148
|
// let isInitialized = $state(false);
|
|
152
149
|
|
|
153
|
-
|
|
150
|
+
// Merge options into default opts, to be able to set just one config option.
|
|
154
151
|
const combinedOptions = $derived({
|
|
155
152
|
...defaultOptions,
|
|
156
153
|
...options
|
|
@@ -168,9 +165,9 @@
|
|
|
168
165
|
): ValidationError => {
|
|
169
166
|
const allowed = combinedOptions.allowedCountries;
|
|
170
167
|
if (allowed?.length && resolvedCountry != null && !allowed.includes(resolvedCountry)) {
|
|
171
|
-
return '
|
|
168
|
+
return 'COUNTRY_NOT_ALLOWED';
|
|
172
169
|
}
|
|
173
|
-
if (isEmpty) return required ? '
|
|
170
|
+
if (isEmpty) return required ? 'REQUIRED' : null;
|
|
174
171
|
if (parseValid) return null;
|
|
175
172
|
// Use the granular error from detailedValue when available
|
|
176
173
|
return detailedValue?.validationError ?? 'INVALID';
|
|
@@ -287,6 +284,11 @@
|
|
|
287
284
|
numberHasCountry.iso2 !== prevCountry
|
|
288
285
|
) {
|
|
289
286
|
countryUpdater(numberHasCountry.iso2);
|
|
287
|
+
// Keep prevCountry in sync so that a subsequent external country change
|
|
288
|
+
// (e.g. parent sets country='DE' again) is correctly detected as a change
|
|
289
|
+
// and triggers a value reset. Without this, prevCountry would still hold
|
|
290
|
+
// the last externally-written value and the reset guard would short-circuit.
|
|
291
|
+
prevCountry = numberHasCountry.iso2;
|
|
290
292
|
// Fire the callback only here — when the country is inferred from the
|
|
291
293
|
// user's input (dial-code parsing). reset() and external prop changes
|
|
292
294
|
// do NOT fire onCountryChange.
|
|
@@ -306,13 +308,35 @@
|
|
|
306
308
|
detailedValue = parsePhoneInput(rawInput, normalizerCountry);
|
|
307
309
|
} catch (err) {
|
|
308
310
|
if (err instanceof ParseError) {
|
|
309
|
-
detailedValue = {
|
|
311
|
+
detailedValue = {
|
|
312
|
+
isPhoneValid: false,
|
|
313
|
+
isValid: false,
|
|
314
|
+
validationError: err.message as ValidationError
|
|
315
|
+
};
|
|
310
316
|
onError?.(err.message);
|
|
311
317
|
} else {
|
|
312
318
|
throw err;
|
|
313
319
|
}
|
|
314
320
|
}
|
|
315
321
|
|
|
322
|
+
// If the resolved country is not in allowedCountries, the phone number is
|
|
323
|
+
// intrinsically valid (libphonenumber-js says so) but the application rejects
|
|
324
|
+
// it. Patch detailedValue to reflect this so that detailedValue.isValid is
|
|
325
|
+
// consistent with the component's `valid` prop and `validationError`.
|
|
326
|
+
const _allowedCountries = combinedOptions.allowedCountries;
|
|
327
|
+
if (
|
|
328
|
+
detailedValue &&
|
|
329
|
+
_allowedCountries?.length &&
|
|
330
|
+
detailedValue.countryCode != null &&
|
|
331
|
+
!_allowedCountries.includes(detailedValue.countryCode)
|
|
332
|
+
) {
|
|
333
|
+
detailedValue = {
|
|
334
|
+
...detailedValue,
|
|
335
|
+
isValid: false,
|
|
336
|
+
validationError: 'COUNTRY_NOT_ALLOWED'
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
316
340
|
// `inputValue` is the displayed value (must stay in sync with the directive's formatting).
|
|
317
341
|
inputValue = combinedOptions.spaces
|
|
318
342
|
? (detailedValue?.formattedNumber ?? rawInput)
|
|
@@ -351,11 +375,9 @@
|
|
|
351
375
|
});
|
|
352
376
|
});
|
|
353
377
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
* handleParsePhoneNumber, so any difference here means the parent changed it.
|
|
358
|
-
*/
|
|
378
|
+
//Detect externally driven value changes (e.g. parent sets bind:value, or resets to null).
|
|
379
|
+
//The shadow `_lastWrittenValue` is stamped on every internal write inside
|
|
380
|
+
//handleParsePhoneNumber, so any difference here means the parent changed it.
|
|
359
381
|
$effect(() => {
|
|
360
382
|
const currentValue = value;
|
|
361
383
|
untrack(() => {
|
|
@@ -365,11 +387,9 @@
|
|
|
365
387
|
});
|
|
366
388
|
});
|
|
367
389
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
* handleParsePhoneNumber later writes country through countryUpdater.
|
|
372
|
-
*/
|
|
390
|
+
// Detect externally driven country changes (e.g. parent's <select bind:value={country}>).
|
|
391
|
+
// Stamps `_lastWrittenCountry` eagerly so the watcher doesn't re-fire when
|
|
392
|
+
// handleParsePhoneNumber later writes country through countryUpdater.
|
|
373
393
|
$effect(() => {
|
|
374
394
|
const currentCountry = country;
|
|
375
395
|
untrack(() => {
|
|
@@ -398,6 +418,13 @@
|
|
|
398
418
|
onLoad?.();
|
|
399
419
|
});
|
|
400
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Resets the input value and validation state.
|
|
423
|
+
*
|
|
424
|
+
* @param {Object} [options] - Optional settings.
|
|
425
|
+
* @param {boolean} [options.country=false] - If true, resets the country to null; otherwise, resets to defaultCountry.
|
|
426
|
+
* @returns {void}
|
|
427
|
+
*/
|
|
401
428
|
const reset = ({ country: resetCountry = false }: { country?: boolean } = {}) => {
|
|
402
429
|
const targetCountry = resetCountry ? null : (defaultCountry ?? null);
|
|
403
430
|
applyValidity(true, false, targetCountry);
|
|
@@ -411,6 +438,11 @@
|
|
|
411
438
|
onValueChange?.('', null);
|
|
412
439
|
};
|
|
413
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Checks the validity of the current input value and updates validation state.
|
|
443
|
+
*
|
|
444
|
+
* @returns {{ valid: boolean; error: ValidationError }} - The validity status and error type.
|
|
445
|
+
*/
|
|
414
446
|
const checkValidity = (): { valid: boolean; error: ValidationError } => {
|
|
415
447
|
if (inputValue === '') {
|
|
416
448
|
applyValidity(true, false, country);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,6 +16,17 @@ export interface Country {
|
|
|
16
16
|
export interface DetailedValue {
|
|
17
17
|
countryCode?: CountryCode | null;
|
|
18
18
|
isPossible: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether the phone number itself is valid according to libphonenumber-js,
|
|
21
|
+
* regardless of any application-level constraints (e.g. `allowedCountries`).
|
|
22
|
+
* Use `isValid` as the source of truth for overall form validity.
|
|
23
|
+
*/
|
|
24
|
+
isPhoneValid: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Overall validity: `true` only when the number is valid **and** all
|
|
27
|
+
* component-level constraints (e.g. `allowedCountries`) are satisfied.
|
|
28
|
+
* This is the field you should check in your application logic.
|
|
29
|
+
*/
|
|
19
30
|
isValid: boolean;
|
|
20
31
|
phoneNumber: string | null;
|
|
21
32
|
countryCallingCode: string | null;
|
|
@@ -32,8 +43,8 @@ export interface DetailedValue {
|
|
|
32
43
|
|
|
33
44
|
/**
|
|
34
45
|
* The reason the current phone number input is invalid.
|
|
35
|
-
* - `'
|
|
36
|
-
* - `'
|
|
46
|
+
* - `'REQUIRED'` — field is empty and `required` is `true`
|
|
47
|
+
* - `'COUNTRY_NOT_ALLOWED'` — the resolved country is not in `options.allowedCountries`
|
|
37
48
|
* - `'TOO_SHORT'` — number has too few digits
|
|
38
49
|
* - `'TOO_LONG'` — number has too many digits
|
|
39
50
|
* - `'NOT_A_NUMBER'` — input does not look like a phone number at all
|
|
@@ -43,8 +54,8 @@ export interface DetailedValue {
|
|
|
43
54
|
* - `null` — no error (input is valid)
|
|
44
55
|
*/
|
|
45
56
|
export type ValidationError =
|
|
46
|
-
| '
|
|
47
|
-
| '
|
|
57
|
+
| 'REQUIRED'
|
|
58
|
+
| 'COUNTRY_NOT_ALLOWED'
|
|
48
59
|
| 'TOO_SHORT'
|
|
49
60
|
| 'TOO_LONG'
|
|
50
61
|
| 'NOT_A_NUMBER'
|
|
@@ -88,6 +99,9 @@ export interface TelInputOptions {
|
|
|
88
99
|
}
|
|
89
100
|
|
|
90
101
|
export interface Props extends HTMLInputAttributes {
|
|
102
|
+
/**
|
|
103
|
+
* Autocomplete attribute for autofill hints (e.g. 'tel', 'tel-national').
|
|
104
|
+
*/
|
|
91
105
|
autocomplete?: AutoFill | null;
|
|
92
106
|
/** You can set the classes of the input field*/
|
|
93
107
|
class?: string;
|
|
@@ -124,10 +138,31 @@ export interface Props extends HTMLInputAttributes {
|
|
|
124
138
|
options?: TelInputOptions;
|
|
125
139
|
/** Binding to the underlying `<input>` element */
|
|
126
140
|
el?: HTMLInputElement | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* Callback fired when the country changes (auto-detected or user-selected).
|
|
143
|
+
* @param newCountry The new country code or null.
|
|
144
|
+
*/
|
|
127
145
|
onCountryChange?: (newCountry: CountryCode | null) => void;
|
|
146
|
+
/**
|
|
147
|
+
* Callback fired when validity changes.
|
|
148
|
+
* @param newValidity The new validity state.
|
|
149
|
+
* @param error The validation error type.
|
|
150
|
+
*/
|
|
128
151
|
onValidityChange?: (newValidity: boolean, error: ValidationError) => void;
|
|
152
|
+
/**
|
|
153
|
+
* Callback fired when the value or details change.
|
|
154
|
+
* @param newValue The new E164 value.
|
|
155
|
+
* @param newDetails The new detailed value object.
|
|
156
|
+
*/
|
|
129
157
|
onValueChange?: (newValue: string, newDetails: Readonly<Partial<DetailedValue> | null>) => void;
|
|
158
|
+
/**
|
|
159
|
+
* Callback fired on parse or validation errors.
|
|
160
|
+
* @param error The error message.
|
|
161
|
+
*/
|
|
130
162
|
onError?: (error: string) => void;
|
|
163
|
+
/**
|
|
164
|
+
* Callback fired after component initialization.
|
|
165
|
+
*/
|
|
131
166
|
onLoad?: () => void;
|
|
132
167
|
}
|
|
133
168
|
|
package/dist/utils/helpers.js
CHANGED
package/dist/validators/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { parse } from '../utils/helpers.js';
|
|
|
32
32
|
export function validateTelInput(value, options = {}) {
|
|
33
33
|
const { required = false, allowedCountries, country } = options;
|
|
34
34
|
if (!value)
|
|
35
|
-
return required ? '
|
|
35
|
+
return required ? 'REQUIRED' : null;
|
|
36
36
|
const result = parse(value, country ?? null);
|
|
37
37
|
if (!result.isValid)
|
|
38
38
|
return result.validationError ?? 'INVALID';
|
|
@@ -40,7 +40,7 @@ export function validateTelInput(value, options = {}) {
|
|
|
40
40
|
allowedCountries.length &&
|
|
41
41
|
result.countryCode != null &&
|
|
42
42
|
!allowedCountries.includes(result.countryCode)) {
|
|
43
|
-
return '
|
|
43
|
+
return 'COUNTRY_NOT_ALLOWED';
|
|
44
44
|
}
|
|
45
45
|
return null;
|
|
46
46
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-tel-input",
|
|
3
3
|
"description": "svelte-tel-input",
|
|
4
|
-
"version": "4.0.0-next.
|
|
4
|
+
"version": "4.0.0-next.2",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/gyurielf/svelte-tel-input.git"
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@sveltejs/adapter-auto": "7.0.1",
|
|
37
|
-
"@sveltejs/kit": "^2.
|
|
37
|
+
"@sveltejs/kit": "^2.55.0",
|
|
38
38
|
"@sveltejs/package": "^2.5.7",
|
|
39
39
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
40
40
|
"@tailwindcss/vite": "^4.2.1",
|
|
@@ -43,19 +43,19 @@
|
|
|
43
43
|
"@testing-library/user-event": "^14.6.1",
|
|
44
44
|
"@types/micromatch": "^4.0.10",
|
|
45
45
|
"dotenv": "^17.3.1",
|
|
46
|
-
"jsdom": "^
|
|
46
|
+
"jsdom": "^29.0.0",
|
|
47
47
|
"micromatch": "^4.0.8",
|
|
48
48
|
"postcss": "^8.5.8",
|
|
49
49
|
"publint": "^0.3.18",
|
|
50
|
-
"svelte": "^5.
|
|
50
|
+
"svelte": "^5.54.0",
|
|
51
51
|
"svelte-check": "^4.4.5",
|
|
52
52
|
"svelte2tsx": "^0.7.52",
|
|
53
53
|
"tailwindcss": "^4.2.1",
|
|
54
54
|
"tslib": "^2.8.1",
|
|
55
55
|
"typescript": "^5.9.3",
|
|
56
|
-
"valibot": "^1.
|
|
56
|
+
"valibot": "^1.3.0",
|
|
57
57
|
"vite": "^7.3.1",
|
|
58
|
-
"vitest": "^4.0
|
|
58
|
+
"vitest": "^4.1.0",
|
|
59
59
|
"zod": "^4.3.6"
|
|
60
60
|
},
|
|
61
61
|
"type": "module",
|