svelte-tel-input 4.0.2 → 4.1.1

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
@@ -28,6 +28,7 @@
28
28
  onValidityChange,
29
29
  onValueChange,
30
30
  onError,
31
+ initialFormat = 'international',
31
32
  value = $bindable(''),
32
33
  country = $bindable(null),
33
34
  defaultCountry = null,
@@ -127,6 +128,17 @@
127
128
  try {
128
129
  const details = parsePhoneInput(value, countryObj);
129
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
+ }
130
142
  return spaces ? (details.formattedNumber ?? value) : value;
131
143
  } catch {
132
144
  return value;
@@ -149,6 +161,11 @@
149
161
  let _lastWrittenCountry: CountryCode | null | undefined = untrack(() => country);
150
162
  // let isInitialized = $state(false);
151
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
+
152
169
  // Merge options into default opts, to be able to set just one config option.
153
170
  const combinedOptions = $derived({
154
171
  ...defaultOptions,
@@ -157,6 +174,8 @@
157
174
 
158
175
  const handleInputAction = (value: string) => {
159
176
  if (disabled || readonly) return;
177
+ // First user keystroke exits the "initial national format" display mode.
178
+ _showNationalFormat = false;
160
179
  handleParsePhoneNumber(value, country, combinedOptions.validateOn !== 'blur');
161
180
  };
162
181
 
@@ -357,9 +376,27 @@
357
376
  }
358
377
 
359
378
  // `inputValue` is the displayed value (must stay in sync with the directive's formatting).
360
- inputValue = combinedOptions.spaces
361
- ? (detailedValue?.formattedNumber ?? rawInput)
362
- : 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
+ }
363
400
 
364
401
  // `value` is the stored value (E.164 when possible).
365
402
  value = detailedValue?.e164 ?? rawInput;
@@ -387,11 +424,24 @@
387
424
  : placeholder
388
425
  );
389
426
 
390
- // 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.
391
429
  $effect(() => {
392
430
  const spaces = combinedOptions.spaces;
393
431
  untrack(() => {
394
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
+ }
395
445
  inputValue = spaces
396
446
  ? (detailedValue.formattedNumber ?? inputValue)
397
447
  : (detailedValue.e164 ?? inputValue);
@@ -399,13 +449,16 @@
399
449
  });
400
450
  });
401
451
 
402
- //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 '').
403
453
  //The shadow `_lastWrittenValue` is stamped on every internal write inside
404
454
  //handleParsePhoneNumber, so any difference here means the parent changed it.
405
455
  $effect(() => {
406
456
  const currentValue = value;
407
457
  untrack(() => {
408
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';
409
462
  handleParsePhoneNumber(currentValue, country ?? null);
410
463
  }
411
464
  });
@@ -436,6 +489,8 @@
436
489
  if (value) {
437
490
  // If country is specified, use it; otherwise, use the initial country (which is figured out from value)
438
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.
439
494
  handleParsePhoneNumber(value, currentCountry);
440
495
  }
441
496
  // isInitialized = true;
@@ -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
178
  export type { CountryCode };
@@ -108,7 +108,7 @@ export const inputParser = (input, { allowSpaces }) => {
108
108
  return value;
109
109
  };
110
110
  // ---------------------------------------------------------------------------
111
- // Phone-number normalizer (merged from newHelpers.ts)
111
+ // Phone-number normalizer
112
112
  // ---------------------------------------------------------------------------
113
113
  const normalizeForLibphonenumber = (input) => {
114
114
  let value = '';
@@ -193,8 +193,23 @@ export const parsePhoneInput = (input, country) => {
193
193
  }
194
194
  else if (defaultCountryIso2) {
195
195
  const formatted = formatIncompletePhoneNumber(capped, defaultCountryIso2);
196
- if (formatted === capped && formatInternational && countryCallingCode) {
197
- formattedNumber = formatInternational.slice(countryCallingCode.length + 1).trim();
196
+ if (formatted === capped && country?.dialCode) {
197
+ if (phone && phone.nationalNumber !== capped) {
198
+ // Trunk-prefixed complete number (e.g. GB "07947…" → nationalNumber
199
+ // "7947…"). Synthesis would be invalid; use the actual international
200
+ // format which correctly omits the trunk prefix.
201
+ formattedNumber =
202
+ formatInternational && countryCallingCode
203
+ ? formatInternational.slice(countryCallingCode.length + 1).trim()
204
+ : stripSpecialChars(capped);
205
+ }
206
+ else {
207
+ // National significant digits (partial or complete, no trunk prefix).
208
+ // Synthesize an international string to get consistent spacing from
209
+ // the first digit, then strip "+{dialCode} ".
210
+ const intl = formatIncompletePhoneNumber(`+${country.dialCode}${capped}`);
211
+ formattedNumber = stripSpecialChars(intl.slice(country.dialCode.length + 1).trim());
212
+ }
198
213
  }
199
214
  else {
200
215
  formattedNumber = stripSpecialChars(formatted || capped);
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.2",
4
+ "version": "4.1.1",
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.55.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",