svelte-tel-input 4.0.1 → 4.1.0

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
@@ -9,7 +9,7 @@ A **headless**, fully customizable **Svelte 5** phone input toolkit.
9
9
  All the ingredients to parse, format, and validate international phone numbers — you build the UI.
10
10
  Store in [E.164](https://en.wikipedia.org/wiki/E.164). Ship with any CSS framework, design system, or custom markup.
11
11
 
12
- [Documentation](https://svelte-tel-input.vercel.app/) · [Playground](https://svelte-tel-input.vercel.app/playground/) · [Changelog](CHANGELOG.md)
12
+ [Documentation](https://svelte-tel-input.vercel.app/) · [Playground](https://svelte-tel-input.vercel.app/playground/) · [Changelog](https://github.com/gyurielf/svelte-tel-input/blob/main/packages/svelte-tel-input/CHANGELOG.md)
13
13
 
14
14
  </div>
15
15
 
@@ -80,7 +80,10 @@ Full API reference, options, events, types, and examples are on the **[documenta
80
80
  - [Options](https://svelte-tel-input.vercel.app/reference/options/)
81
81
  - [Events / Callbacks](https://svelte-tel-input.vercel.app/reference/events/)
82
82
  - [API Methods](https://svelte-tel-input.vercel.app/reference/api/)
83
+ - [Validators](https://svelte-tel-input.vercel.app/reference/validators/)
84
+ - [Utilities](https://svelte-tel-input.vercel.app/reference/utilities/)
83
85
  - [Types](https://svelte-tel-input.vercel.app/reference/types/)
86
+ - [Assets](https://svelte-tel-input.vercel.app/reference/assets/)
84
87
  - [Playground](https://svelte-tel-input.vercel.app/playground/)
85
88
 
86
89
  ## Support
@@ -1,9 +1,11 @@
1
1
  <script lang="ts">
2
2
  import { onMount, untrack } from 'svelte';
3
3
  import { ParseError } from 'libphonenumber-js/max';
4
- import { generatePlaceholder, parsePhoneInput, telInputAction } from '../../utils/index.js';
4
+ import { generatePlaceholder } from '../../utils/index.js';
5
+ import { parsePhoneInput } from '../../utils/helpers.js';
6
+ import { telInputAction } from '../../utils/directives/telInputAction.js';
7
+ import { getCountry, guessCountryByPartialNumber } from '../../utils/countryHelpers.js';
5
8
  import type { CountryCode, TelInputOptions, Props, ValidationError } from '../../types';
6
- import { getCountry, guessCountryByPartialNumber } from '../../utils/directives/countryHelpers';
7
9
 
8
10
  const defaultOptions = {
9
11
  autoPlaceholder: true,
@@ -26,12 +28,13 @@
26
28
  onValidityChange,
27
29
  onValueChange,
28
30
  onError,
31
+ initialFormat = 'international',
29
32
  value = $bindable(''),
30
33
  country = $bindable(null),
31
34
  defaultCountry = null,
32
35
  detailedValue = $bindable(null),
33
36
  valid = $bindable(true),
34
- validationError = $bindable<ValidationError>(null),
37
+ validationError = $bindable<Readonly<ValidationError>>(null),
35
38
  options = defaultOptions,
36
39
  el = $bindable(undefined),
37
40
  'aria-invalid': ariaInvalidProp = undefined,
@@ -125,6 +128,17 @@
125
128
  try {
126
129
  const details = parsePhoneInput(value, countryObj);
127
130
  const spaces = options?.spaces ?? defaultOptions.spaces;
131
+ if (
132
+ initialFormat === 'national' &&
133
+ details.formattedNumber &&
134
+ details.countryCallingCode
135
+ ) {
136
+ const prefix = `+${details.countryCallingCode} `;
137
+ if (details.formattedNumber.startsWith(prefix)) {
138
+ const national = details.formattedNumber.slice(prefix.length);
139
+ return spaces ? national : national.replace(/\s/g, '');
140
+ }
141
+ }
128
142
  return spaces ? (details.formattedNumber ?? value) : value;
129
143
  } catch {
130
144
  return value;
@@ -147,6 +161,11 @@
147
161
  let _lastWrittenCountry: CountryCode | null | undefined = untrack(() => country);
148
162
  // let isInitialized = $state(false);
149
163
 
164
+ // When true, `handleParsePhoneNumber` and the spaces-effect will display the
165
+ // national (dial-code-stripped) format. Starts as `true` when
166
+ // `initialFormat='national'` and is cleared the first time the user types.
167
+ let _showNationalFormat = untrack(() => initialFormat === 'national');
168
+
150
169
  // Merge options into default opts, to be able to set just one config option.
151
170
  const combinedOptions = $derived({
152
171
  ...defaultOptions,
@@ -155,6 +174,8 @@
155
174
 
156
175
  const handleInputAction = (value: string) => {
157
176
  if (disabled || readonly) return;
177
+ // First user keystroke exits the "initial national format" display mode.
178
+ _showNationalFormat = false;
158
179
  handleParsePhoneNumber(value, country, combinedOptions.validateOn !== 'blur');
159
180
  };
160
181
 
@@ -355,9 +376,27 @@
355
376
  }
356
377
 
357
378
  // `inputValue` is the displayed value (must stay in sync with the directive's formatting).
358
- inputValue = combinedOptions.spaces
359
- ? (detailedValue?.formattedNumber ?? rawInput)
360
- : rawInput;
379
+ // When `_showNationalFormat` is true (initial render, user hasn't typed yet),
380
+ // strip the dial-code prefix so the value appears in national format.
381
+ if (
382
+ _showNationalFormat &&
383
+ detailedValue?.formattedNumber &&
384
+ detailedValue?.countryCallingCode
385
+ ) {
386
+ const _prefix = `+${detailedValue.countryCallingCode} `;
387
+ if (detailedValue.formattedNumber.startsWith(_prefix)) {
388
+ const _national = detailedValue.formattedNumber.slice(_prefix.length);
389
+ inputValue = combinedOptions.spaces ? _national : _national.replace(/\s/g, '');
390
+ } else {
391
+ inputValue = combinedOptions.spaces
392
+ ? (detailedValue.formattedNumber ?? rawInput)
393
+ : rawInput;
394
+ }
395
+ } else {
396
+ inputValue = combinedOptions.spaces
397
+ ? (detailedValue?.formattedNumber ?? rawInput)
398
+ : rawInput;
399
+ }
361
400
 
362
401
  // `value` is the stored value (E.164 when possible).
363
402
  value = detailedValue?.e164 ?? rawInput;
@@ -385,11 +424,24 @@
385
424
  : placeholder
386
425
  );
387
426
 
388
- // Re-format displayed value when spaces option changes
427
+ // Re-format displayed value when spaces option changes.
428
+ // Also respects _showNationalFormat so the initial national rendering is preserved.
389
429
  $effect(() => {
390
430
  const spaces = combinedOptions.spaces;
391
431
  untrack(() => {
392
432
  if (inputValue !== '' && detailedValue) {
433
+ if (
434
+ _showNationalFormat &&
435
+ detailedValue.formattedNumber &&
436
+ detailedValue.countryCallingCode
437
+ ) {
438
+ const prefix = `+${detailedValue.countryCallingCode} `;
439
+ if (detailedValue.formattedNumber.startsWith(prefix)) {
440
+ const national = detailedValue.formattedNumber.slice(prefix.length);
441
+ inputValue = spaces ? national : national.replace(/\s/g, '');
442
+ return;
443
+ }
444
+ }
393
445
  inputValue = spaces
394
446
  ? (detailedValue.formattedNumber ?? inputValue)
395
447
  : (detailedValue.e164 ?? inputValue);
@@ -397,13 +449,16 @@
397
449
  });
398
450
  });
399
451
 
400
- //Detect externally driven value changes (e.g. parent sets bind:value, or resets to null).
452
+ //Detect externally driven value changes (e.g. parent sets bind:value, or resets to '').
401
453
  //The shadow `_lastWrittenValue` is stamped on every internal write inside
402
454
  //handleParsePhoneNumber, so any difference here means the parent changed it.
403
455
  $effect(() => {
404
456
  const currentValue = value;
405
457
  untrack(() => {
406
458
  if (currentValue !== _lastWrittenValue) {
459
+ // External change (parent set value): re-apply initialFormat so the
460
+ // display respects it, just like on initial load.
461
+ _showNationalFormat = initialFormat === 'national';
407
462
  handleParsePhoneNumber(currentValue, country ?? null);
408
463
  }
409
464
  });
@@ -434,6 +489,8 @@
434
489
  if (value) {
435
490
  // If country is specified, use it; otherwise, use the initial country (which is figured out from value)
436
491
  const currentCountry = country || initialCountry;
492
+ // handleParsePhoneNumber respects _showNationalFormat internally,
493
+ // so national format is applied automatically when initialFormat='national'. Later maybe this could be optional.
437
494
  handleParsePhoneNumber(value, currentCountry);
438
495
  }
439
496
  // isInitialized = true;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export { default as TelInput } from './components/input/TelInput.svelte';
2
- export { inputParser, clickOutsideAction, isSelected, parsePhoneInput, parse, normalizeToE164, pickCountries } from './utils/index.js';
3
- export { parsePhoneNumberWithError, ParseError } from 'libphonenumber-js/max';
2
+ export { parse, normalizeToE164, pickCountries } from './utils/index.js';
4
3
  export { countries } from './assets/index.js';
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  export { default as TelInput } from './components/input/TelInput.svelte';
2
- export { inputParser, clickOutsideAction, isSelected, parsePhoneInput, parse, normalizeToE164, pickCountries } from './utils/index.js';
3
- export { parsePhoneNumberWithError, ParseError } from 'libphonenumber-js/max';
2
+ export { parse, normalizeToE164, pickCountries } from './utils/index.js';
4
3
  export { countries } from './assets/index.js';
@@ -1,4 +1,4 @@
1
- import type { CountryCallingCode, CountryCode, MetadataJson, PhoneNumber } from 'libphonenumber-js';
1
+ import type { CountryCode } from 'libphonenumber-js';
2
2
  import type { HTMLInputAttributes } from 'svelte/elements';
3
3
 
4
4
  export interface Country {
@@ -38,7 +38,7 @@ export interface DetailedValue {
38
38
  uri: string | null;
39
39
  e164: string | null;
40
40
  /** Granular validation error when `isValid` is `false`. */
41
- validationError?: ValidationError;
41
+ validationError: ValidationError;
42
42
  }
43
43
 
44
44
  /**
@@ -133,7 +133,7 @@ export interface Props extends HTMLInputAttributes {
133
133
  /** Validity of the input based on the config settings. */
134
134
  valid?: boolean;
135
135
  /** The reason the current value is invalid, or `null` when valid. */
136
- validationError?: ValidationError;
136
+ validationError?: Readonly<ValidationError>;
137
137
  /** You can turn on and off certain features by this object */
138
138
  options?: TelInputOptions;
139
139
  /** Binding to the underlying `<input>` element */
@@ -148,7 +148,7 @@ export interface Props extends HTMLInputAttributes {
148
148
  * @param newValidity The new validity state.
149
149
  * @param error The validation error type.
150
150
  */
151
- onValidityChange?: (newValidity: boolean, error: ValidationError) => void;
151
+ onValidityChange?: (newValidity: boolean, error: Readonly<ValidationError>) => void;
152
152
  /**
153
153
  * Callback fired when the value or details change.
154
154
  * @param newValue The new E164 value.
@@ -164,6 +164,15 @@ export interface Props extends HTMLInputAttributes {
164
164
  * Callback fired after component initialization.
165
165
  */
166
166
  onLoad?: () => void;
167
+ /**
168
+ * Controls how the initial `value` is displayed in the input field.
169
+ * - `'international'` — displays the number with the country dial code prefix (e.g. `+36 20 123 4567`). This is the default.
170
+ * - `'national'` — displays the national part only (e.g. `20 123 4567`), while `value` remains E164. The `country` prop will be auto-detected from the value if not explicitly set.
171
+ *
172
+ * This only affects the initial render and outside change from the provided `value` prop; user input behaviour is unchanged.
173
+ * @default 'international'
174
+ */
175
+ initialFormat?: 'international' | 'national';
167
176
  }
168
177
 
169
- export type { CountryCallingCode, CountryCode, PhoneNumber, MetadataJson };
178
+ export type { CountryCode };
@@ -1,4 +1,4 @@
1
- import type { CountryCode, Country } from '../../types/index.js';
1
+ import type { CountryCode, Country } from '../types/index.js';
2
2
  export declare const getCountry: ({ field, value, countries }: {
3
3
  /**
4
4
  * Field to search by
@@ -1,4 +1,4 @@
1
- import { countries as normalizedCountries } from '../../assets/index.js';
1
+ import { countries as normalizedCountries } from '../assets/index.js';
2
2
  export const getCountry = ({ field, value, countries = normalizedCountries }) => {
3
3
  if (['priority'].includes(field)) {
4
4
  throw new Error(`Field "${field}" is not supported`);
@@ -1,6 +1,6 @@
1
- import { getCountry } from './countryHelpers.js';
1
+ import { getCountry } from '../countryHelpers.js';
2
2
  import { parsePhoneInput } from '../helpers.js';
3
- import { calculateCursorPosition } from './cursorPosition.js';
3
+ import { calculateCursorPosition } from '../cursorPosition.js';
4
4
  let inputState = null;
5
5
  const normalizeUserInput = (input) => {
6
6
  let value = '';
@@ -1,6 +1,6 @@
1
1
  import { AsYouType, getExampleNumber, formatIncompletePhoneNumber, validatePhoneNumberLength } from 'libphonenumber-js/max';
2
2
  import { examplePhoneNumbers, countries } from '../assets/index.js';
3
- import { getCountry } from './directives/countryHelpers.js';
3
+ import { getCountry } from './countryHelpers.js';
4
4
  const whiteSpaceRegex = new RegExp('[\\t\\n\\v\\f\\r \\u00a0\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u200b\\u2028\\u2029\\u3000]', 'g');
5
5
  const plusSignRegex = new RegExp('\\+', 'g');
6
6
  export const generatePlaceholder = (country, { spaces } = {
@@ -1,3 +1 @@
1
- export { generatePlaceholder, inputParser, isSelected, parse, normalizeToE164, parsePhoneInput, pickCountries } from './helpers.js';
2
- export * from './directives/clickOutsideAction.js';
3
- export * from './directives/telInputAction.js';
1
+ export { generatePlaceholder, parse, normalizeToE164, pickCountries } from './helpers.js';
@@ -1,3 +1 @@
1
- export { generatePlaceholder, inputParser, isSelected, parse, normalizeToE164, parsePhoneInput, pickCountries } from './helpers.js';
2
- export * from './directives/clickOutsideAction.js';
3
- export * from './directives/telInputAction.js';
1
+ export { generatePlaceholder, parse, normalizeToE164, pickCountries } from './helpers.js';
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.1",
4
+ "version": "4.1.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/gyurielf/svelte-tel-input.git"
@@ -30,32 +30,33 @@
30
30
  "svelte": "^5.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "libphonenumber-js": "1.12.40"
33
+ "libphonenumber-js": "1.12.41"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@sveltejs/adapter-auto": "7.0.1",
37
- "@sveltejs/kit": "^2.55.0",
37
+ "@sveltejs/kit": "^2.57.1",
38
38
  "@sveltejs/package": "^2.5.7",
39
39
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
40
- "@tailwindcss/vite": "^4.2.2",
40
+ "@tailwindcss/vite": "^4.2.3",
41
41
  "@testing-library/jest-dom": "6.9.1",
42
42
  "@testing-library/svelte": "^5.3.1",
43
43
  "@testing-library/user-event": "^14.6.1",
44
44
  "@types/micromatch": "^4.0.10",
45
- "dotenv": "^17.3.1",
46
- "jsdom": "^29.0.1",
45
+ "@types/node": "^22.17.0",
46
+ "dotenv": "^17.4.2",
47
+ "jsdom": "^29.0.2",
47
48
  "micromatch": "^4.0.8",
48
- "postcss": "^8.5.8",
49
+ "postcss": "^8.5.10",
49
50
  "publint": "^0.3.18",
50
- "svelte": "^5.54.0",
51
- "svelte-check": "^4.4.5",
52
- "svelte2tsx": "^0.7.52",
53
- "tailwindcss": "^4.2.2",
51
+ "svelte": "^5.55.4",
52
+ "svelte-check": "^4.4.6",
53
+ "svelte2tsx": "^0.7.53",
54
+ "tailwindcss": "^4.2.3",
54
55
  "tslib": "^2.8.1",
55
56
  "typescript": "^5.9.3",
56
57
  "valibot": "^1.3.1",
57
- "vite": "^7.3.1",
58
- "vitest": "^4.1.1",
58
+ "vite": "^7.3.2",
59
+ "vitest": "^4.1.4",
59
60
  "zod": "^4.3.6"
60
61
  },
61
62
  "type": "module",
@@ -1,3 +0,0 @@
1
- export declare const clickOutsideAction: (node: HTMLElement, handler: () => void, skipPrevented?: boolean) => {
2
- destroy: () => void;
3
- };
@@ -1,18 +0,0 @@
1
- export const clickOutsideAction = (node, handler, skipPrevented = true) => {
2
- const handleClick = async (event) => {
3
- if (skipPrevented) {
4
- if (!node.contains(event.target) && !event.defaultPrevented)
5
- handler();
6
- }
7
- else {
8
- if (!node.contains(event.target))
9
- handler();
10
- }
11
- };
12
- document.addEventListener('click', handleClick, true);
13
- return {
14
- destroy() {
15
- document.removeEventListener('click', handleClick, true);
16
- }
17
- };
18
- };