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 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 by [libphonenumber-js](https://gitlab.com/catamphetamine/libphonenumber-js), with granular error types
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
- * Compute the initial formatted display value synchronously.
108
- * Runs on both server and client at initialization time, so SSR renders the
109
- * formatted number and the client's first render matches — no hydration mismatch.
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
- * Shadow trackers record every value this component writes internally so the
144
- * external-change watchers below can distinguish "we set it" vs "parent set it".
145
- * Plain let (not $state) because they are only ever read inside untrack(), never
146
- * as reactive dependencies.
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
- /** Merge options into default opts, to be able to set just one config option. */
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 'country_not_allowed';
168
+ return 'COUNTRY_NOT_ALLOWED';
172
169
  }
173
- if (isEmpty) return required ? 'required' : null;
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 = { isValid: false, validationError: err.message as ValidationError };
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
- * Detect externally driven value changes (e.g. parent sets bind:value, or resets to null).
356
- * The shadow `_lastWrittenValue` is stamped on every internal write inside
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
- * Detect externally driven country changes (e.g. parent's <select bind:value={country}>).
370
- * Stamps `_lastWrittenCountry` eagerly so the watcher doesn't re-fire when
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);
@@ -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
- * - `'required'` — field is empty and `required` is `true`
36
- * - `'country_not_allowed'` — the resolved country is not in `options.allowedCountries`
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
- | 'required'
47
- | 'country_not_allowed'
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
 
@@ -220,6 +220,7 @@ export const parsePhoneInput = (input, country) => {
220
220
  : null,
221
221
  formattedNumber,
222
222
  isPossible: asYouType.isPossible(),
223
+ isPhoneValid: isValid,
223
224
  isValid,
224
225
  nationalNumber,
225
226
  phoneNumber,
@@ -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 ? 'required' : null;
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 'country_not_allowed';
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.0",
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.53.4",
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": "^28.1.0",
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.53.11",
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.2.0",
56
+ "valibot": "^1.3.0",
57
57
  "vite": "^7.3.1",
58
- "vitest": "^4.0.18",
58
+ "vitest": "^4.1.0",
59
59
  "zod": "^4.3.6"
60
60
  },
61
61
  "type": "module",