ngxsmk-tel-input 1.0.7 → 1.0.9

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
@@ -1,17 +1,9 @@
1
- # ngxsmk-tel-input — International Telephone Input for Angular
1
+ # ngxsmk-tel-input
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/ngxsmk-tel-input)](https://www.npmjs.com/package/ngxsmk-tel-input)
4
- [![npm downloads](https://img.shields.io/npm/dm/ngxsmk-tel-input)](https://www.npmjs.com/package/ngxsmk-tel-input)
5
- [![license: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/toozuuu/ngxsmk-tel-input/blob/main/LICENSE)
6
- [![GitHub stars](https://img.shields.io/github/stars/toozuuu/ngxsmk-tel-input?style=social)](https://github.com/toozuuu/ngxsmk-tel-input)
3
+ An Angular **telephone input** component with country dropdown, flags, and robust validation/formatting.
4
+ Wraps [`intl-tel-input`](https://github.com/jackocnr/intl-tel-input) for the UI and [`libphonenumber-js`](https://github.com/catamphetamine/libphonenumber-js) for parsing/validation. Implements `ControlValueAccessor` so it plugs into Angular Forms.
7
5
 
8
- An Angular component for entering and validating **international telephone numbers**. It adds a country flag dropdown, formats input, and validates using real numbering rules.
9
-
10
- * UI powered by [`intl-tel-input`](https://github.com/jackocnr/intl-tel-input)
11
- * Parsing & validation via [`libphonenumber-js`](https://github.com/catamphetamine/libphonenumber-js)
12
- * Implements Angular **ControlValueAccessor** (works with Reactive & Template‑driven Forms)
13
-
14
- > Emits **E.164** by default (e.g. `+14155550123`). SSR‑safe via lazy, browser‑only import.
6
+ > Emits **E.164** by default (e.g. `+14155550123`). SSR‑safe via lazy browser‑only import.
15
7
 
16
8
  ---
17
9
 
@@ -25,27 +17,36 @@ An Angular component for entering and validating **international telephone numbe
25
17
 
26
18
  ---
27
19
 
28
- ## Compatibility
29
-
30
- | ngxsmk-tel-input | Angular |
31
- |------------------| --------------- |
32
- | `1.0.x` | `17.x` – `19.x` |
20
+ ## ✨ Features
33
21
 
34
- > The library declares `peerDependencies` of `@angular/* >=17 <20`.
22
+ * Country dropdown with flags
23
+ * E.164 output (display can be national with `nationalMode`)
24
+ * Reactive & template‑driven Forms support (CVA)
25
+ * Built‑in validation using libphonenumber‑js
26
+ * SSR‑friendly (no `window` on the server)
27
+ * Easy theming via CSS variables
28
+ * Nice UX options: label/hint/error text, sizes, variants, clear button, autofocus, select-on-focus
35
29
 
36
30
  ---
37
31
 
38
- ## Installation
32
+ ## ✅ Requirements
33
+
34
+ * Angular **17 – 19**
35
+ * Node **18** or **20**
36
+
37
+ > Library `peerDependencies` target Angular `>=17 <20`. Your app can be 17, 18, or 19.
38
+
39
+ ---
39
40
 
40
- ### 1) Install dependencies
41
+ ## 📦 Install
41
42
 
42
43
  ```bash
43
44
  npm i ngxsmk-tel-input intl-tel-input libphonenumber-js
44
45
  ```
45
46
 
46
- ### 2) Add styles & flag assets (in your **app**, not the library)
47
+ ### Add styles & flag assets (in your **app**, not the library)
47
48
 
48
- Add intl‑tel‑input CSS and copy its flag images via `angular.json`:
49
+ Update your app’s `angular.json`:
49
50
 
50
51
  ```jsonc
51
52
  {
@@ -55,11 +56,9 @@ Add intl‑tel‑input CSS and copy its flag images via `angular.json`:
55
56
  "build": {
56
57
  "options": {
57
58
  "styles": [
58
- "node_modules/intl-tel-input/build/css/intlTelInput.css",
59
- "src/styles.css"
59
+ "node_modules/intl-tel-input/build/css/intlTelInput.css"
60
60
  ],
61
61
  "assets": [
62
- { "glob": "**/*", "input": "src/assets" },
63
62
  { "glob": "**/*", "input": "node_modules/intl-tel-input/build/img", "output": "assets/intl-tel-input/img" }
64
63
  ]
65
64
  }
@@ -70,7 +69,7 @@ Add intl‑tel‑input CSS and copy its flag images via `angular.json`:
70
69
  }
71
70
  ```
72
71
 
73
- Optional (helps some bundlers resolve flags): add to your global styles
72
+ Optional override to ensure flags resolve (e.g., Vite/Angular 17+): add to your global styles
74
73
 
75
74
  ```css
76
75
  .iti__flag { background-image: url("/assets/intl-tel-input/img/flags.png"); }
@@ -79,194 +78,276 @@ Optional (helps some bundlers resolve flags): add to your global styles
79
78
  }
80
79
  ```
81
80
 
82
- Restart your dev server after editing `angular.json`.
81
+ Restart the dev server after changes.
83
82
 
84
83
  ---
85
84
 
86
- ## Usage
87
-
88
- ### Reactive Forms
85
+ ## 🚀 Quick start (Reactive Forms)
89
86
 
90
87
  ```ts
91
88
  // app.component.ts
92
- import { Component } from '@angular/core';
93
- import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
94
- import { NgxsmkTelInputComponent } from 'ngxsmk-tel-input';
89
+ import { Component, inject } from '@angular/core';
90
+ import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
91
+ import { JsonPipe } from '@angular/common';
92
+ import { NgxsmkTelInputComponent, IntlTelI18n, CountryMap } from 'ngxsmk-tel-input';
95
93
 
96
94
  @Component({
97
95
  selector: 'app-root',
98
96
  standalone: true,
99
- imports: [ReactiveFormsModule, NgxsmkTelInputComponent],
97
+ imports: [ReactiveFormsModule, NgxsmkTelInputComponent, JsonPipe],
100
98
  template: `
101
99
  <form [formGroup]="fg" style="max-width:420px;display:grid;gap:12px">
102
100
  <ngxsmk-tel-input
103
101
  formControlName="phone"
104
102
  label="Phone"
105
103
  hint="Include area code"
104
+ dir="ltr"
106
105
  [initialCountry]="'US'"
107
106
  [preferredCountries]="['US','GB','AU']"
108
- (countryChange)="onCountry($event)">
107
+ [i18n]="enLabels"
108
+ [localizedCountries]="enCountries"
109
+ [autoPlaceholder]="'off'"
110
+ [clearAriaLabel]="'Clear phone number'">
109
111
  </ngxsmk-tel-input>
110
112
 
111
- <p *ngIf="fg.get('phone')?.hasError('phoneInvalid') && fg.get('phone')?.touched">
112
- Please enter a valid phone number.
113
- </p>
114
-
115
113
  <pre>Value: {{ fg.value | json }}</pre>
116
114
  </form>
117
115
  `
118
116
  })
119
117
  export class AppComponent {
118
+ private readonly fb = inject(FormBuilder);
120
119
  fg = this.fb.group({ phone: ['', Validators.required] });
121
- constructor(private readonly fb: FormBuilder) {}
122
- onCountry(e: { iso2: any }) { console.log('Country changed:', e.iso2); }
120
+
121
+ // English UI labels (dropdown/search/ARIA)
122
+ enLabels: IntlTelI18n = {
123
+ selectedCountryAriaLabel: 'Selected country',
124
+ countryListAriaLabel: 'Country list',
125
+ searchPlaceholder: 'Search country',
126
+ zeroSearchResults: 'No results',
127
+ noCountrySelected: 'No country selected'
128
+ };
129
+
130
+ // Optional: only override the names you care about
131
+ enCountries: CountryMap = {
132
+ US: 'United States',
133
+ GB: 'United Kingdom',
134
+ AU: 'Australia',
135
+ CA: 'Canada'
136
+ };
123
137
  }
138
+
124
139
  ```
125
140
 
126
- **Value semantics:** The control emits **E.164** when valid, or `null` when empty/invalid.
141
+ **Value semantics:** the form control value is **E.164** (e.g., `+14155550123`) when valid, or `null` when empty/invalid.
142
+
143
+ ---
127
144
 
128
- ### Template‑driven
145
+ ## 📝 Template‑driven usage
129
146
 
130
147
  ```html
131
148
  <form #f="ngForm">
132
149
  <ngxsmk-tel-input name="phone" [(ngModel)]="phone"></ngxsmk-tel-input>
133
150
  </form>
151
+ <!-- phone is an E.164 string or null -->
134
152
  ```
135
153
 
136
154
  ---
137
155
 
138
- ## Options (Inputs)
139
-
140
- | Option | Type | Default | Description |
141
- | ---------------------- | -------------------------------------- | ---------------------- | ---------------------------------------------------------------------- |
142
- | `initialCountry` | `CountryCode \| 'auto'` | `'US'` | Starting country. `'auto'` uses a simple geo‑IP stub (defaults to US). |
143
- | `preferredCountries` | `CountryCode[]` | `['US','GB']` | Countries pinned at the top. |
144
- | `onlyCountries` | `CountryCode[]` | — | Restrict selectable countries. |
145
- | `nationalMode` | `boolean` | `false` | Display national format in the box. Value still emits E.164. |
146
- | `separateDialCode` | `boolean` | `false` | Show dial code separately to the left. |
147
- | `allowDropdown` | `boolean` | `true` | Enable/disable country dropdown. |
148
- | `placeholder` | `string` | `'Enter phone number'` | Input placeholder. |
149
- | `autocomplete` | `string` | `'tel'` | Native autocomplete attribute. |
150
- | `disabled` | `boolean` | `false` | Disable the control. |
151
- | `label` | `string` | — | Optional label text. |
152
- | `hint` | `string` | — | Helper text shown below. |
153
- | `errorText` | `string` | — | Custom error message. |
154
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Preset sizing. |
155
- | `variant` | `'outline' \| 'filled' \| 'underline'` | `'outline'` | Visual style. |
156
- | `showClear` | `boolean` | `true` | Show a clear (×) button when not empty. |
157
- | `autoFocus` | `boolean` | `false` | Focus on init. |
158
- | `selectOnFocus` | `boolean` | `false` | Select text on focus. |
159
- | `formatOnBlur` | `boolean` | `true` | Pretty‑print on blur (national if `nationalMode`). |
160
- | `showErrorWhenTouched` | `boolean` | `true` | Only show error styles after the control is touched. |
161
- | `dropdownAttachToBody` | `boolean` | `true` | Attach dropdown to `<body>` to avoid clipping. |
162
- | `dropdownZIndex` | `number` | `2000` | Z‑index for dropdown panel. |
163
-
164
- > `CountryCode` is the ISO‑2 code from `libphonenumber-js` (e.g. `US`, `GB`).
165
-
166
- ### Outputs (Events)
167
-
168
- | Event | Payload | Description |
169
- | ---------------- | ---------------------------------------------------------- | ---------------------------------------- |
170
- | `countryChange` | `{ iso2: CountryCode }` | Fires when the selected country changes. |
171
- | `validityChange` | `boolean` | Emits when validity toggles. |
172
- | `inputChange` | `{ raw: string; e164: string \| null; iso2: CountryCode }` | Emitted on each input. |
173
-
174
- ### Public Methods
156
+ ## 🈺 Localization & RTL
175
157
 
176
- * `focus(): void`
177
- * `selectCountry(iso2: CountryCode): void`
158
+ You can localize the dropdown/search labels and override country names.
178
159
 
179
- ---
160
+ <img src="https://unpkg.com/ngxsmk-tel-input@1.0.4/docs/kr.png" alt="Angular international phone input - Korean Localization & RTL" width="420" />
161
+
162
+ Korean example
163
+
164
+ ```ts
165
+
166
+ <ngxsmk-tel-input
167
+ [initialCountry]="'KR'"
168
+ [preferredCountries]="['KR','US','JP']"
169
+ [i18n]="koLabels"
170
+ [localizedCountries]="koCountries">
171
+ </ngxsmk-tel-input>
172
+
173
+ // in component
174
+ koLabels = {
175
+ selectedCountryAriaLabel: '선택한 국가',
176
+ countryListAriaLabel: '국가 목록',
177
+ searchPlaceholder: '국가 검색',
178
+ zeroSearchResults: '결과 없음',
179
+ noCountrySelected: '선택된 국가 없음'
180
+ };
181
+
182
+ koCountries = {
183
+ KR: '대한민국',
184
+ US: '미국',
185
+ JP: '일본',
186
+ CN: '중국'
187
+ };
180
188
 
181
- ## Supported Formats
189
+ ```
190
+
191
+ Arabic + RTL example
192
+
193
+ ```ts
194
+ <ngxsmk-tel-input
195
+ dir="rtl"
196
+ label="الهاتف"
197
+ hint="اكتب رمز المنطقة"
198
+ [initialCountry]="'AE'"
199
+ [preferredCountries]="['AE','SA','EG']"
200
+ [i18n]="arLabels"
201
+ [localizedCountries]="arCountries"
202
+ [dropdownAttachToBody]="false" <!-- so popup inherits rtl from host -->
203
+ ></ngxsmk-tel-input>
204
+
205
+ ```
182
206
 
183
- * **E164** → `+41446681800`
184
- * **International** (display) → `+41 44 668 18 00`
185
- * **National** (display) → `044 668 18 00`
186
207
 
187
- > The control **emits E.164** by default. If you need the currently typed format too, use `(inputChange)`.
208
+ ## ⚙️ API
209
+
210
+ ### Inputs
211
+
212
+ | Name | Type | Default | Description |
213
+ |------------------------|---------------------------------------------|-------------------------|-------------------------------------------------------------------------------|
214
+ | `initialCountry` | `CountryCode \| 'auto'` | `'US'` | Starting country. `'auto'` uses geoIp stub (`US` by default). |
215
+ | `preferredCountries` | `CountryCode[]` | `['US','GB']` | Pin these at the top. |
216
+ | `onlyCountries` | `CountryCode[]` | — | Limit selectable countries. |
217
+ | `nationalMode` | `boolean` | `false` | If `true`, **display** national format in the input. Value still emits E.164. |
218
+ | `separateDialCode` | `boolean` | `false` | Show dial code outside the input. |
219
+ | `allowDropdown` | `boolean` | `true` | Enable/disable dropdown. |
220
+ | `placeholder` | `string` | `'Enter phone number'` | Input placeholder. |
221
+ | `autocomplete` | `string` | `'tel'` | Native autocomplete. |
222
+ | `disabled` | `boolean` | `false` | Disable the control. |
223
+ | `label` | `string` | — | Optional floating label text. |
224
+ | `hint` | `string` | — | Helper text below the control. |
225
+ | `errorText` | `string` | — | Custom error text. |
226
+ | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Control height/typography. |
227
+ | `variant` | `'outline' \| 'filled' \| 'underline'` | `'outline'` | Visual variant. |
228
+ | `showClear` | `boolean` | `true` | Show a clear (×) button when not empty. |
229
+ | `autoFocus` | `boolean` | `false` | Focus on init. |
230
+ | `selectOnFocus` | `boolean` | `false` | Select all text on focus. |
231
+ | `formatOnBlur` | `boolean` | `true` | Pretty‑print on blur (national if `nationalMode`). |
232
+ | `showErrorWhenTouched` | `boolean` | `true` | Show error styles only after blur. |
233
+ | `dropdownAttachToBody` | `boolean` | `true` | Attach dropdown to `<body>` (avoids clipping/overflow). |
234
+ | `dropdownZIndex` | `number` | `2000` | Z‑index for dropdown panel. |
235
+ | `i18n` | `IntlTelI18n` | — | Localize dropdown/search/ARIA labels. |
236
+ | `localizedCountries` | `Partial<Record<CountryCode, string>>` | — | Override country display names (ISO-2 keys). |
237
+ | `dir` | `'ltr' \| 'rtl'` | `'ltr'` | Text direction for the control. |
238
+ | `autoPlaceholder` | `'off' \| 'polite' \| 'aggressive'` | `'polite'` | Example placeholders. Requires `utilsScript` unless `off`. |
239
+ | `utilsScript` | `string` | — | Path/URL to `utils.js` (needed for example placeholders). |
240
+ | `customPlaceholder` | `(example: string, country: any) => string` | — | Transform the example placeholder. |
241
+ | `clearAriaLabel` | `string` | `'Clear phone number'` | ARIA label for the clear button. |
242
+
243
+ > `CountryCode` is the ISO‑2 uppercase code from `libphonenumber-js` (e.g. `US`, `GB`).
244
+
245
+ ### Outputs
246
+
247
+ | Event | Payload | Description |
248
+ | ---------------- | ---------------------------------------------------------- | ------------------------------------ |
249
+ | `countryChange` | `{ iso2: CountryCode }` | Fired when selected country changes. |
250
+ | `validityChange` | `boolean` | Fired when validity flips. |
251
+ | `inputChange` | `{ raw: string; e164: string \| null; iso2: CountryCode }` | Emitted on every keystroke. |
252
+
253
+ ### Public methods
254
+
255
+ * `focus(): void`
256
+ * `selectCountry(iso2: CountryCode): void`
188
257
 
189
258
  ---
190
259
 
191
- ## Theming & Styling
260
+ ## 🎨 Theming (CSS variables)
192
261
 
193
- This component exposes CSS variables for quick theming:
262
+ Override on the element or a parent container:
194
263
 
195
264
  ```html
196
265
  <ngxsmk-tel-input style="
197
266
  --tel-border:#cbd5e1;
198
- --tel-ring:#2563eb;
199
- --tel-radius:12px;
200
- --tel-dd-item-hover: rgba(37,99,235,.08);
267
+ --tel-ring:#22c55e;
268
+ --tel-radius:14px;
269
+ --tel-dd-item-hover: rgba(34,197,94,.12);
201
270
  --tel-dd-z: 3000;
202
271
  "></ngxsmk-tel-input>
203
272
  ```
204
273
 
205
- Key variables: `--tel-bg`, `--tel-fg`, `--tel-border`, `--tel-border-hover`, `--tel-ring`, `--tel-placeholder`, `--tel-error`, `--tel-radius`, `--tel-focus-shadow`, `--tel-dd-bg`, `--tel-dd-border`, `--tel-dd-shadow`, `--tel-dd-radius`, `--tel-dd-item-hover`, `--tel-dd-search-bg`, `--tel-dd-z`.
274
+ Available tokens:
275
+
276
+ * Input: `--tel-bg`, `--tel-fg`, `--tel-border`, `--tel-border-hover`, `--tel-ring`, `--tel-placeholder`, `--tel-error`, `--tel-radius`, `--tel-focus-shadow`
277
+ * Dropdown: `--tel-dd-bg`, `--tel-dd-border`, `--tel-dd-shadow`, `--tel-dd-radius`, `--tel-dd-item-hover`, `--tel-dd-search-bg`, `--tel-dd-z`
278
+
279
+ Dark mode: wrap in a `.dark` parent — tokens adapt automatically.
280
+
281
+ ---
282
+
283
+ ## ✔️ Validation patterns
284
+
285
+ ```html
286
+ <ngxsmk-tel-input formControlName="phone"></ngxsmk-tel-input>
287
+
288
+ <div class="error" *ngIf="fg.get('phone')?.hasError('required')">Phone is required</div>
289
+ <div class="error" *ngIf="fg.get('phone')?.hasError('phoneInvalid')">Please enter a valid phone number</div>
290
+ ```
291
+
292
+ * When **valid** → control value = **E.164** string
293
+ * When **invalid/empty** → value = **null**, and validator sets `{ phoneInvalid: true }`
206
294
 
207
- Dark mode: wrap in a `.dark` container; tokens adapt.
295
+ > Need national string instead of E.164? Use `(inputChange)` and store `raw`/`national` yourself, or adapt the emitter to output national.
208
296
 
209
297
  ---
210
298
 
211
- ## SSR
299
+ ## 🌐 SSR notes
212
300
 
213
- The library guards `intl-tel-input` import behind `isPlatformBrowser`, so Angular Universal builds won’t try to access `window` during SSR.
301
+ * The library lazy‑imports `intl-tel-input` only in the **browser** (guards with `isPlatformBrowser`).
302
+ * No `window`/`document` usage on the server path.
214
303
 
215
304
  ---
216
305
 
217
- ## Local Development
306
+ ## 🧪 Local development
218
307
 
219
- This repository is an Angular workspace with a library.
308
+ This repo is an Angular workspace with a library.
220
309
 
221
310
  ```bash
222
311
  # Build the library
223
312
  ng build ngxsmk-tel-input
224
313
 
225
- # Try it in a demo app inside the workspace
314
+ # Option A: use it inside a demo app in the same workspace
226
315
  ng serve demo
227
316
 
228
- # Or pack and install in another app locally
317
+ # Option B: install locally via tarball in another app
229
318
  cd dist/ngxsmk-tel-input && npm pack
230
-
231
319
  # in your other app
232
- npm i ../path/to/dist/ngxsmk-tel-input/ngxsmk-tel-input-<version>.tgz
320
+ npm i ../path-to-workspace/dist/ngxsmk-tel-input/ngxsmk-tel-input-<version>.tgz
233
321
  ```
234
322
 
235
- Tip: you can also map a workspace path alias in `tsconfig.base.json`:
236
-
237
- ```jsonc
238
- {
239
- "compilerOptions": {
240
- "paths": {
241
- "ngxsmk-tel-input": ["dist/ngxsmk-tel-input"]
242
- }
243
- }
244
- }
245
- ```
323
+ > Workspace aliasing via `tsconfig.paths` also works (map `"ngxsmk-tel-input": ["dist/ngxsmk-tel-input"]`).
246
324
 
247
325
  ---
248
326
 
249
- ## Troubleshooting
250
-
251
- * **Unstyled dropdown / bullets / missing flags** → Ensure CSS & assets are added in your app’s `angular.json`, then restart the dev server.
252
- * **`TS2307: Cannot find module 'ngxsmk-tel-input'`** → Build the library first so `dist/ngxsmk-tel-input` exists (or install from npm).
253
- * **Peer dependency conflict** → Your app must be Angular 17–19 to satisfy peers.
254
- * **Dropdown clipped by parent** → Keep `dropdownAttachToBody=true` or raise `dropdownZIndex`.
327
+ ## 🧯 Troubleshooting
255
328
 
256
- ---
329
+ **UI looks unstyled / bullets in dropdown**
330
+ Add the CSS and assets in `angular.json` (see Install). Restart the dev server.
257
331
 
258
- ## Contributing
332
+ **Flags don’t show**
333
+ Ensure the assets copy exists under `/assets/intl-tel-input/img` and add the CSS override block above.
259
334
 
260
- PRs welcome! Please:
335
+ **`TS2307: Cannot find module 'ngxsmk-tel-input'`**
336
+ Build the library first so `dist/ngxsmk-tel-input` exists. If using workspace aliasing, add a `paths` entry to the root `tsconfig.base.json`.
261
337
 
262
- 1. `npm ci` and `ng build`
263
- 2. Cover behavior changes with tests if possible
264
- 3. Update README for any API changes
338
+ **Peer dependency conflict when installing**
339
+ The lib peers are `@angular/* >=17 <20`. Upgrade your app or install a compatible version.
265
340
 
266
- This project is open to using the [all-contributors](https://github.com/all-contributors/all-contributors) spec. Contributions of any kind welcome!
341
+ **Vite/Angular “Failed to resolve import …”**
342
+ Clear `.angular/cache`, rebuild the lib, and restart `ng serve`.
267
343
 
268
344
  ---
269
345
 
270
- ## License
346
+ ## 📃 License
271
347
 
272
348
  [MIT](./LICENSE)
349
+
350
+ ## 🙌 Credits
351
+
352
+ * UI powered by [`intl-tel-input`](https://github.com/jackocnr/intl-tel-input)
353
+ * Parsing & validation by [`libphonenumber-js`](https://github.com/catamphetamine/libphonenumber-js)
package/docs/kr.png ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngxsmk-tel-input",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Angular international telephone input with country flag dropdown, formatting & validation (intl-tel-input + libphonenumber). ControlValueAccessor. Supports Angular 17–19.",
5
5
  "keywords": [
6
6
  "angular",
@@ -14,30 +14,44 @@ import {
14
14
  SimpleChanges,
15
15
  ViewChild
16
16
  } from '@angular/core';
17
- import {isPlatformBrowser} from '@angular/common';
17
+ import { isPlatformBrowser } from '@angular/common';
18
18
  import {
19
19
  AbstractControl,
20
20
  ControlValueAccessor,
21
21
  NG_VALIDATORS,
22
22
  NG_VALUE_ACCESSOR,
23
- ValidationErrors
23
+ ValidationErrors,
24
+ Validator
24
25
  } from '@angular/forms';
25
- import type {CountryCode} from 'libphonenumber-js';
26
- import {NgxsmkTelInputService} from './ngxsmk-tel-input.service';
26
+ import type { CountryCode } from 'libphonenumber-js';
27
+ import { NgxsmkTelInputService } from './ngxsmk-tel-input.service';
27
28
 
28
29
  type IntlTelInstance = any;
30
+ export type CountryMap = Partial<Record<CountryCode, string>>;
31
+
32
+ export interface IntlTelI18n {
33
+ selectedCountryAriaLabel?: string;
34
+ countryListAriaLabel?: string;
35
+ searchPlaceholder?: string;
36
+ zeroSearchResults?: string;
37
+ noCountrySelected?: string;
38
+ }
29
39
 
30
40
  @Component({
31
41
  selector: 'ngxsmk-tel-input',
32
42
  standalone: true,
33
43
  imports: [],
34
44
  template: `
35
- <div class="ngx-tel" [class.disabled]="disabled" [attr.data-size]="size" [attr.data-variant]="variant">
45
+ <div class="ngxsmk-tel"
46
+ [class.disabled]="disabled"
47
+ [attr.data-size]="size"
48
+ [attr.data-variant]="variant"
49
+ [attr.dir]="dir">
36
50
  @if (label) {
37
- <label class="ngx-tel__label" [for]="resolvedId">{{ label }}</label>
51
+ <label class="ngxsmk-tel__label" [for]="resolvedId">{{ label }}</label>
38
52
  }
39
53
 
40
- <div class="ngx-tel__wrap" [class.has-error]="showError">
54
+ <div class="ngxsmk-tel__wrap" [class.has-error]="showError">
41
55
  <div class="ngxsmk-tel-input__wrapper">
42
56
  <input
43
57
  #telInput
@@ -47,6 +61,8 @@ type IntlTelInstance = any;
47
61
  [attr.name]="name || null"
48
62
  [attr.placeholder]="placeholder || null"
49
63
  [attr.autocomplete]="autocomplete"
64
+ [attr.inputmode]="digitsOnly ? 'numeric' : 'tel'"
65
+ [attr.pattern]="digitsOnly ? (allowLeadingPlus ? '\\\\+?[0-9]*' : '[0-9]*') : null"
50
66
  [disabled]="disabled"
51
67
  [attr.aria-invalid]="showError ? 'true' : 'false'"
52
68
  (blur)="onBlur()"
@@ -56,28 +72,26 @@ type IntlTelInstance = any;
56
72
 
57
73
  @if (showClear && currentRaw()) {
58
74
  <button type="button"
59
- class="ngx-tel__clear"
75
+ class="ngxsmk-tel__clear"
60
76
  (click)="clearInput()"
61
- [attr.aria-label]="'Clear phone number'">
77
+ [attr.aria-label]="clearAriaLabel">
62
78
  ×
63
79
  </button>
64
80
  }
65
-
66
81
  </div>
67
82
 
68
83
  @if (hint && !showError) {
69
- <div class="ngx-tel__hint">{{ hint }}</div>
84
+ <div class="ngxsmk-tel__hint">{{ hint }}</div>
70
85
  }
71
86
 
72
87
  @if (showError) {
73
- <div class="ngx-tel__error">{{ errorText || 'Please enter a valid phone number.' }}</div>
88
+ <div class="ngxsmk-tel__error">{{ errorText || 'Please enter a valid phone number.' }}</div>
74
89
  }
75
-
76
90
  </div>
77
91
  `,
78
92
  providers: [
79
- {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true},
80
- {provide: NG_VALIDATORS, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true}
93
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true },
94
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true }
81
95
  ],
82
96
  styles: [`
83
97
  /* ---------- Theme tokens ---------- */
@@ -92,7 +106,6 @@ type IntlTelInstance = any;
92
106
  --tel-radius: 12px;
93
107
  --tel-focus-shadow: 0 0 0 3px rgba(37, 99, 235, .25);
94
108
 
95
- /* dropdown tokens */
96
109
  --tel-dd-bg: var(--tel-bg);
97
110
  --tel-dd-border: var(--tel-border);
98
111
  --tel-dd-shadow: 0 24px 60px rgba(0, 0, 0, .18);
@@ -102,7 +115,6 @@ type IntlTelInstance = any;
102
115
  --tel-dd-search-bg: rgba(148, 163, 184, .08);
103
116
 
104
117
  display: block;
105
-
106
118
  }
107
119
 
108
120
  :host-context(.dark) {
@@ -119,90 +131,37 @@ type IntlTelInstance = any;
119
131
  }
120
132
 
121
133
  /* ---------- Structure ---------- */
122
- .ngx-tel {
123
- width: 100%;
124
- color: var(--tel-fg);
125
- }
126
-
127
- .ngx-tel.disabled {
128
- opacity: .7;
129
- cursor: not-allowed;
130
- }
131
-
132
- .ngx-tel__label {
133
- display: inline-block;
134
- margin-bottom: 6px;
135
- font-size: .875rem;
136
- font-weight: 500;
137
- }
134
+ .ngxsmk-tel { width: 100%; color: var(--tel-fg); }
135
+ .ngxsmk-tel.disabled { opacity: .7; cursor: not-allowed; }
138
136
 
139
- .ngx-tel__wrap {
140
- position: relative;
141
- }
142
-
143
- .ngxsmk-tel-input__wrapper,
144
- :host ::ng-deep .iti {
145
- width: 100%;
146
- }
137
+ .ngxsmk-tel__label { display: inline-block; margin-bottom: 6px; font-size: .875rem; font-weight: 500; }
138
+ .ngxsmk-tel__wrap { position: relative; }
139
+ .ngxsmk-tel-input__wrapper, :host ::ng-deep .iti { width: 100%; }
147
140
 
148
141
  .ngxsmk-tel-input__control {
149
- width: 100%;
150
- height: 40px;
151
- font: inherit;
152
- color: var(--tel-fg);
153
- background: var(--tel-bg);
154
- border: 1px solid var(--tel-border);
142
+ width: 100%; height: 40px; font: inherit; color: var(--tel-fg);
143
+ background: var(--tel-bg); border: 1px solid var(--tel-border);
155
144
  border-radius: var(--tel-radius);
156
- padding: 10px 40px 10px 12px; /* room for clear button */
145
+ padding: 10px 40px 10px 12px;
157
146
  outline: none;
158
- transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
159
- }
160
-
161
- .ngxsmk-tel-input__control::placeholder {
162
- color: var(--tel-placeholder);
163
- }
164
-
165
- .ngxsmk-tel-input__control:hover {
166
- border-color: var(--tel-border-hover);
147
+ transition: border-color .15s, box-shadow .15s, background .15s;
167
148
  }
149
+ .ngxsmk-tel-input__control::placeholder { color: var(--tel-placeholder); }
150
+ .ngxsmk-tel-input__control:hover { border-color: var(--tel-border-hover); }
151
+ .ngxsmk-tel-input__control:focus { border-color: var(--tel-ring); box-shadow: var(--tel-focus-shadow); }
168
152
 
169
- .ngxsmk-tel-input__control:focus {
170
- border-color: var(--tel-ring);
171
- box-shadow: var(--tel-focus-shadow);
172
- }
173
-
174
- /* Size presets */
175
153
  [data-size="sm"] .ngxsmk-tel-input__control {
176
- height: 34px;
177
- font-size: 13px;
178
- padding: 6px 36px 6px 10px;
179
- border-radius: 10px;
154
+ height: 34px; font-size: 13px; padding: 6px 36px 6px 10px; border-radius: 10px;
180
155
  }
181
-
182
156
  [data-size="lg"] .ngxsmk-tel-input__control {
183
- height: 46px;
184
- font-size: 16px;
185
- padding: 12px 44px 12px 14px;
186
- border-radius: 14px;
187
- }
188
-
189
- /* Variants */
190
- [data-variant="filled"] .ngxsmk-tel-input__control {
191
- background: rgba(148, 163, 184, .08);
157
+ height: 46px; font-size: 16px; padding: 12px 44px 12px 14px; border-radius: 14px;
192
158
  }
193
159
 
160
+ [data-variant="filled"] .ngxsmk-tel-input__control { background: rgba(148, 163, 184, .08); }
194
161
  [data-variant="underline"] .ngxsmk-tel-input__control {
195
- border: 0;
196
- border-bottom: 2px solid var(--tel-border);
197
- border-radius: 0;
198
- padding-left: 0;
199
- padding-right: 34px;
200
- }
201
-
202
- [data-variant="underline"] .ngxsmk-tel-input__control:focus {
203
- border-bottom-color: var(--tel-ring);
204
- box-shadow: none;
162
+ border: 0; border-bottom: 2px solid var(--tel-border); border-radius: 0; padding-left: 0; padding-right: 34px;
205
163
  }
164
+ [data-variant="underline"] .ngxsmk-tel-input__control:focus { border-bottom-color: var(--tel-ring); box-shadow: none; }
206
165
 
207
166
  /* ---------- intl-tel-input dropdown (deep selectors) ---------- */
208
167
  :host ::ng-deep .iti__flag-container {
@@ -212,162 +171,48 @@ type IntlTelInstance = any;
212
171
  border-right: none;
213
172
  background: var(--tel-bg);
214
173
  }
174
+ :host ::ng-deep .iti__selected-flag { height: 100%; padding: 0 10px; display: inline-flex; align-items: center; }
215
175
 
216
- :host ::ng-deep .iti__selected-flag {
217
- height: 100%;
218
- padding: 0 10px;
219
- display: inline-flex;
220
- align-items: center;
221
- }
222
-
223
- /* Core dropdown panel */
224
176
  :host ::ng-deep .iti__country-list {
225
- background: var(--tel-dd-bg);
226
- border: 1px solid var(--tel-dd-border);
227
- border-radius: var(--tel-dd-radius);
228
- box-shadow: var(--tel-dd-shadow);
229
- max-height: min(50vh, 360px);
230
- overflow: auto;
231
- padding: 6px 0;
232
- width: max(280px, 100%);
233
- z-index: var(--tel-dd-z);
177
+ background: var(--tel-dd-bg); border: 1px solid var(--tel-dd-border);
178
+ border-radius: var(--tel-dd-radius); box-shadow: var(--tel-dd-shadow);
179
+ max-height: min(50vh, 360px); overflow: auto; padding: 6px 0;
180
+ width: max(280px, 100%); z-index: var(--tel-dd-z);
234
181
  }
182
+ :host ::ng-deep .iti--container .iti__country-list { z-index: var(--tel-dd-z); }
235
183
 
236
- /* When attached to <body>, it's wrapped in .iti--container */
237
- :host ::ng-deep .iti--container .iti__country-list {
238
- z-index: var(--tel-dd-z);
239
- }
240
-
241
- /* Search input (sticky header) */
242
184
  :host ::ng-deep .iti__search-input {
243
- position: sticky;
244
- top: 0;
245
- margin: 0;
246
- padding: 10px 12px;
247
- width: 100%;
248
- border: 0;
249
- border-bottom: 1px solid var(--tel-dd-border);
250
- outline: none;
251
- background: var(--tel-dd-search-bg);
252
- color: var(--tel-fg);
185
+ position: sticky; top: 0; margin: 0; padding: 10px 12px; width: 100%;
186
+ border: 0; border-bottom: 1px solid var(--tel-dd-border); outline: none;
187
+ background: var(--tel-dd-search-bg); color: var(--tel-fg);
253
188
  }
254
189
 
255
- :host ::ng-deep .iti__search-input::placeholder {
256
- color: var(--tel-placeholder);
257
- }
258
-
259
- /* Rows: flag | country name | dial code (right) */
260
190
  :host ::ng-deep .iti__country {
261
- display: grid;
262
- grid-template-columns: 28px 1fr auto;
263
- align-items: center;
264
- column-gap: .5rem;
265
- padding: 10px 12px;
266
- cursor: pointer;
267
- }
268
-
269
- :host ::ng-deep .iti__flag-box {
270
- width: 28px;
271
- display: inline-flex;
272
- justify-content: center;
273
- }
274
-
275
- :host ::ng-deep .iti__country-name {
276
- color: var(--tel-fg);
191
+ display: grid; grid-template-columns: 28px 1fr auto; align-items: center;
192
+ column-gap: .5rem; padding: 10px 12px; cursor: pointer;
277
193
  }
194
+ :host ::ng-deep .iti__dial-code { color: var(--tel-placeholder); font-weight: 600; margin-left: 10px; }
278
195
 
279
- :host ::ng-deep .iti__dial-code {
280
- color: var(--tel-placeholder);
281
- font-weight: 600;
282
- margin-left: 10px;
196
+ .ngxsmk-tel__clear {
197
+ position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
198
+ border: 0; background: transparent; font-size: 18px; line-height: 1;
199
+ width: 28px; height: 28px; border-radius: 50%; cursor: pointer; color: var(--tel-placeholder);
283
200
  }
201
+ .ngxsmk-tel__clear:hover { background: rgba(148, 163, 184, .15); }
284
202
 
285
- :host ::ng-deep .iti__country:hover,
286
- :host ::ng-deep .iti__country.iti__highlight {
287
- background: var(--tel-dd-item-hover);
288
- }
289
-
290
- :host ::ng-deep .iti__country:focus {
291
- outline: 2px solid var(--tel-ring);
292
- outline-offset: -2px;
293
- }
294
-
295
- :host ::ng-deep .iti__divider {
296
- margin: 6px 0;
297
- border-top: 1px dashed var(--tel-dd-border);
298
- }
299
-
300
- /* Separate dial code pushes input text */
301
- :host ::ng-deep .iti--separate-dial-code .ngxsmk-tel-input__control {
302
- padding-left: 56px;
303
- }
304
-
305
- /* Custom scrollbar (WebKit/Chromium) */
306
- :host ::ng-deep .iti__country-list::-webkit-scrollbar {
307
- width: 10px;
308
- }
203
+ .ngxsmk-tel__hint { margin-top: 6px; font-size: 12px; color: var(--tel-placeholder); }
204
+ .ngxsmk-tel__error { margin-top: 6px; font-size: 12px; color: var(--tel-error); }
309
205
 
310
- :host ::ng-deep .iti__country-list::-webkit-scrollbar-thumb {
311
- background: rgba(148, 163, 184, .4);
312
- border-radius: 8px;
313
- }
314
-
315
- :host ::ng-deep .iti__country-list::-webkit-scrollbar-track {
316
- background: transparent;
317
- }
318
-
319
- /* Mobile tweak: make it breathe */
320
- @media (max-width: 480px) {
321
- :host ::ng-deep .iti__country-list {
322
- width: 100vw;
323
- max-width: 100vw;
324
- }
325
- }
326
-
327
- /* Clear button */
328
- .ngx-tel__clear {
329
- position: absolute;
330
- right: 8px;
331
- top: 50%;
332
- transform: translateY(-50%);
333
- border: 0;
334
- background: transparent;
335
- font-size: 18px;
336
- line-height: 1;
337
- width: 28px;
338
- height: 28px;
339
- border-radius: 50%;
340
- cursor: pointer;
341
- color: var(--tel-placeholder);
342
- }
343
-
344
- .ngx-tel__clear:hover {
345
- background: rgba(148, 163, 184, .15);
346
- }
347
-
348
- /* Hint & Error */
349
- .ngx-tel__hint {
350
- margin-top: 6px;
351
- font-size: 12px;
352
- color: var(--tel-placeholder);
353
- }
354
-
355
- .ngx-tel__error {
356
- margin-top: 6px;
357
- font-size: 12px;
358
- color: var(--tel-error);
359
- }
360
-
361
- .ngx-tel__wrap.has-error .ngxsmk-tel-input__control {
206
+ .ngxsmk-tel__wrap.has-error .ngxsmk-tel-input__control {
362
207
  border-color: var(--tel-error);
363
208
  box-shadow: 0 0 0 3px rgba(239, 68, 68, .15);
364
209
  }
365
210
  `]
366
211
  })
367
- export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor {
368
- @ViewChild('telInput', {static: true}) inputRef!: ElementRef<HTMLInputElement>;
212
+ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor, Validator {
213
+ @ViewChild('telInput', { static: true }) inputRef!: ElementRef<HTMLInputElement>;
369
214
 
370
- /* Existing inputs */
215
+ /* Core config */
371
216
  @Input() initialCountry: CountryCode | 'auto' = 'US';
372
217
  @Input() preferredCountries: CountryCode[] = ['US', 'GB'];
373
218
  @Input() onlyCountries?: CountryCode[];
@@ -375,13 +220,13 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
375
220
  @Input() separateDialCode = false;
376
221
  @Input() allowDropdown = true;
377
222
 
378
- @Input() placeholder = 'Enter phone number';
223
+ /* UX */
224
+ @Input() placeholder?: string; // keep undefined to let plugin set example placeholders (requires utilsScript)
379
225
  @Input() autocomplete = 'tel';
380
226
  @Input() name?: string;
381
227
  @Input() inputId?: string;
382
228
  @Input() disabled = false;
383
229
 
384
- /* New UI/UX inputs */
385
230
  @Input() label?: string;
386
231
  @Input() hint?: string;
387
232
  @Input() errorText?: string;
@@ -393,9 +238,26 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
393
238
  @Input() formatOnBlur = true;
394
239
  @Input() showErrorWhenTouched = true;
395
240
 
396
- /** Dropdown plumbing */
397
- @Input() dropdownAttachToBody = true; // append dropdown to <body> (escapes overflow/clip)
398
- @Input() dropdownZIndex = 2000; // used by CSS var --tel-dd-z
241
+ /* Dropdown plumbing */
242
+ @Input() dropdownAttachToBody = true;
243
+ @Input() dropdownZIndex = 2000;
244
+
245
+ /* Localization + RTL */
246
+ @Input('i18n') i18n?: IntlTelI18n;
247
+ @Input('telI18n') set telI18n(v: IntlTelI18n | undefined) { this.i18n = v; }
248
+ @Input('localizedCountries') localizedCountries?: CountryMap;
249
+ @Input('telLocalizedCountries') set telLocalizedCountries(v: CountryMap | undefined) { this.localizedCountries = v; }
250
+ @Input() clearAriaLabel = 'Clear phone number';
251
+ @Input() dir: 'ltr' | 'rtl' = 'ltr';
252
+
253
+ /* Placeholders (intl-tel-input) */
254
+ @Input() autoPlaceholder: 'off' | 'polite' | 'aggressive' = 'off'; // default OFF since no utils fallback
255
+ @Input() utilsScript?: string;
256
+ @Input() customPlaceholder?: (example: string, country: any) => string;
257
+
258
+ /* Digits-only controls */
259
+ @Input() digitsOnly = true;
260
+ @Input() allowLeadingPlus = true;
399
261
 
400
262
  /* Outputs */
401
263
  @Output() countryChange = new EventEmitter<{ iso2: CountryCode }>();
@@ -404,30 +266,23 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
404
266
 
405
267
  /* Internal */
406
268
  private iti: IntlTelInstance | null = null;
407
- private onChange: (val: string | null) => void = () => {
408
- };
409
- private onTouchedCb: () => void = () => {
410
- };
269
+ private onChange: (val: string | null) => void = () => {};
270
+ private onTouchedCb: () => void = () => {};
271
+ private validatorChange?: () => void;
411
272
  private lastEmittedValid = false;
412
273
  private pendingWrite: string | null = null;
413
274
  private touched = false;
414
275
 
415
- readonly resolvedId = (() => 'tel-' + Math.random().toString(36).slice(2))();
276
+ readonly resolvedId = this.inputId || ('tel-' + Math.random().toString(36).slice(2));
416
277
 
417
278
  constructor(
418
279
  private readonly zone: NgZone,
419
280
  private readonly tel: NgxsmkTelInputService,
420
281
  @Inject(PLATFORM_ID) private readonly platformId: Object
421
- ) {
422
- }
282
+ ) {}
423
283
 
424
284
  async ngAfterViewInit() {
425
285
  if (!isPlatformBrowser(this.platformId)) return;
426
-
427
- // set z-index via CSS var
428
- (this as any).constructor; // no-op to keep TS calm
429
- (this.inputRef.nativeElement.closest(':host') as any);
430
-
431
286
  await this.initIntlTelInput();
432
287
  this.bindDomListeners();
433
288
 
@@ -441,9 +296,17 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
441
296
 
442
297
  ngOnChanges(changes: SimpleChanges): void {
443
298
  if (!isPlatformBrowser(this.platformId)) return;
444
- const configChanged = ['initialCountry', 'preferredCountries', 'onlyCountries', 'separateDialCode', 'allowDropdown', 'nationalMode']
445
- .some(k => k in changes && !changes[k].firstChange);
446
- if (configChanged && this.iti) this.reinitPlugin();
299
+ const configChanged = [
300
+ 'initialCountry','preferredCountries','onlyCountries',
301
+ 'separateDialCode','allowDropdown','nationalMode',
302
+ 'i18n','localizedCountries','dir',
303
+ 'autoPlaceholder','utilsScript','customPlaceholder',
304
+ 'digitsOnly','allowLeadingPlus'
305
+ ].some(k => k in changes && !changes[k]?.firstChange);
306
+ if (configChanged && this.iti) {
307
+ this.reinitPlugin();
308
+ this.validatorChange?.();
309
+ }
447
310
  }
448
311
 
449
312
  ngOnDestroy(): void {
@@ -459,15 +322,8 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
459
322
  }
460
323
  this.setInputValue(val ?? '');
461
324
  }
462
-
463
- registerOnChange(fn: any): void {
464
- this.onChange = fn;
465
- }
466
-
467
- registerOnTouched(fn: any): void {
468
- this.onTouchedCb = fn;
469
- }
470
-
325
+ registerOnChange(fn: any): void { this.onChange = fn; }
326
+ registerOnTouched(fn: any): void { this.onTouchedCb = fn; }
471
327
  setDisabledState(isDisabled: boolean): void {
472
328
  this.disabled = isDisabled;
473
329
  if (this.inputRef) this.inputRef.nativeElement.disabled = isDisabled;
@@ -482,8 +338,9 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
482
338
  this.lastEmittedValid = valid;
483
339
  this.validityChange.emit(valid);
484
340
  }
485
- return valid ? null : {phoneInvalid: true};
341
+ return valid ? null : { phoneInvalid: true };
486
342
  }
343
+ registerOnValidatorChange(fn: () => void): void { this.validatorChange = fn; }
487
344
 
488
345
  // ----- Public helpers -----
489
346
  focus(): void {
@@ -493,14 +350,12 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
493
350
  queueMicrotask(() => el.setSelectionRange(0, el.value.length));
494
351
  }
495
352
  }
496
-
497
353
  selectCountry(iso2: CountryCode): void {
498
354
  if (this.iti) {
499
355
  this.iti.setCountry(iso2.toLowerCase());
500
356
  this.handleInput();
501
357
  }
502
358
  }
503
-
504
359
  clearInput() {
505
360
  this.setInputValue('');
506
361
  this.handleInput();
@@ -509,7 +364,11 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
509
364
 
510
365
  // ----- Plugin wiring -----
511
366
  private async initIntlTelInput() {
512
- const [{default: intlTelInput}] = await Promise.all([import('intl-tel-input')]);
367
+ const [{ default: intlTelInput }] = await Promise.all([import('intl-tel-input')]);
368
+
369
+ const toLowerKeys = (m?: CountryMap) =>
370
+ m ? Object.fromEntries(Object.entries(m).map(([k, v]) => [k.toLowerCase(), v])) : undefined;
371
+
513
372
  const config: any = {
514
373
  initialCountry: this.initialCountry === 'auto' ? 'auto' : (this.initialCountry?.toLowerCase() || 'us'),
515
374
  preferredCountries: (this.preferredCountries ?? []).map(c => c.toLowerCase()),
@@ -518,14 +377,25 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
518
377
  allowDropdown: this.allowDropdown,
519
378
  separateDialCode: this.separateDialCode,
520
379
  geoIpLookup: (cb: (iso2: string) => void) => cb('us'),
521
- utilsScript: undefined,
380
+
381
+ // placeholders — NOTE: no CDN fallback anymore
382
+ autoPlaceholder: this.autoPlaceholder, // 'off' | 'polite' | 'aggressive'
383
+ utilsScript: this.utilsScript,
384
+ customPlaceholder: this.customPlaceholder,
385
+
386
+ // localization
387
+ i18n: this.i18n,
388
+ localizedCountries: toLowerKeys(this.localizedCountries),
389
+
390
+ // dropdown container
522
391
  dropdownContainer: this.dropdownAttachToBody && typeof document !== 'undefined' ? document.body : undefined
523
392
  };
393
+
524
394
  this.zone.runOutsideAngular(() => {
525
395
  this.iti = intlTelInput(this.inputRef.nativeElement, config);
526
396
  });
527
397
 
528
- // expose z-index var to host (so CSS picks it up)
398
+ // z-index for dropdown
529
399
  (this.inputRef.nativeElement as HTMLElement).style.setProperty('--tel-dd-z', String(this.dropdownZIndex));
530
400
  }
531
401
 
@@ -553,16 +423,74 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
553
423
  }
554
424
  }
555
425
 
426
+ // ----- Input filtering (digits-only) -----
427
+ private sanitizeDigits(value: string): string {
428
+ if (!this.digitsOnly) return value;
429
+ let v = value.replace(/[^\d+]/g, ''); // keep digits and pluses
430
+ // allow only ONE leading + (if enabled)
431
+ if (this.allowLeadingPlus) {
432
+ const hasLeadingPlus = v.startsWith('+');
433
+ v = (hasLeadingPlus ? '+' : '') + v.replace(/\+/g, '');
434
+ } else {
435
+ v = v.replace(/\+/g, '');
436
+ }
437
+ return v;
438
+ }
439
+
556
440
  private bindDomListeners() {
557
441
  const el = this.inputRef.nativeElement;
442
+
558
443
  this.zone.runOutsideAngular(() => {
559
- el.addEventListener('input', () => this.handleInput());
444
+ // prevent invalid chars while typing
445
+ el.addEventListener('beforeinput', (ev: InputEvent) => {
446
+ if (!this.digitsOnly) return;
447
+ const data = (ev as any).data as string | null;
448
+
449
+ // allow deletions, cuts, etc.
450
+ if (!data || ev.inputType !== 'insertText') return;
451
+
452
+ const pos = el.selectionStart ?? 0;
453
+ const isDigit = data >= '0' && data <= '9';
454
+ const isPlusAtStart = this.allowLeadingPlus && data === '+' && pos === 0 && !el.value.includes('+');
455
+
456
+ if (!isDigit && !isPlusAtStart) ev.preventDefault();
457
+ });
458
+
459
+ // sanitize pastes
460
+ el.addEventListener('paste', (e: ClipboardEvent) => {
461
+ if (!this.digitsOnly) return;
462
+ e.preventDefault();
463
+ const text = (e.clipboardData || (window as any).clipboardData).getData('text');
464
+ const sanitized = this.sanitizeDigits(text);
465
+ const start = el.selectionStart ?? el.value.length;
466
+ const end = el.selectionEnd ?? el.value.length;
467
+ el.setRangeText(sanitized, start, end, 'end');
468
+ queueMicrotask(() => this.handleInput());
469
+ });
470
+
471
+ // catch any remaining non-digit changes (e.g., programmatic)
472
+ el.addEventListener('input', () => {
473
+ if (this.digitsOnly) {
474
+ const val = el.value;
475
+ const sanitized = this.sanitizeDigits(val);
476
+ if (val !== sanitized) {
477
+ const caret = el.selectionStart ?? sanitized.length;
478
+ el.value = sanitized;
479
+ el.setSelectionRange(caret, caret);
480
+ }
481
+ }
482
+ this.handleInput();
483
+ });
484
+
560
485
  el.addEventListener('countrychange', () => {
561
486
  const iso2 = this.currentIso2();
562
- this.zone.run(() => this.countryChange.emit({iso2}));
487
+ this.zone.run(() => {
488
+ this.countryChange.emit({ iso2 });
489
+ this.validatorChange?.();
490
+ });
563
491
  this.handleInput();
564
492
  });
565
- el.addEventListener('paste', () => queueMicrotask(() => this.handleInput()));
493
+
566
494
  el.addEventListener('blur', () => this.onBlur());
567
495
  });
568
496
  }
@@ -575,7 +503,7 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
575
503
  if (!raw) return;
576
504
  const parsed = this.tel.parse(raw, this.currentIso2());
577
505
  if (this.nationalMode && parsed.national) {
578
- this.setInputValue(parsed.national.replace(/\s{2,}/g, ' '));
506
+ this.setInputValue((parsed.national || '').replace(/\s{2,}/g, ' '));
579
507
  }
580
508
  }
581
509
 
@@ -591,7 +519,7 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
591
519
  const iso2 = this.currentIso2();
592
520
  const parsed = this.tel.parse(raw, iso2);
593
521
  this.zone.run(() => this.onChange(parsed.e164)); // E.164 or null
594
- this.zone.run(() => this.inputChange.emit({raw, e164: parsed.e164, iso2}));
522
+ this.zone.run(() => this.inputChange.emit({ raw, e164: parsed.e164, iso2 }));
595
523
  if (raw && this.nationalMode && parsed.national) {
596
524
  const normalized = parsed.national.replace(/\s{2,}/g, ' ');
597
525
  if (normalized !== raw) this.setInputValue(normalized);
@@ -603,7 +531,8 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
603
531
  }
604
532
 
605
533
  private currentIso2(): CountryCode {
606
- const iso2 = (this.iti?.getSelectedCountryData?.().iso2 ?? this.initialCountry ?? 'US').toString().toUpperCase();
534
+ const iso2 = (this.iti?.getSelectedCountryData?.().iso2 ?? this.initialCountry ?? 'US')
535
+ .toString().toUpperCase();
607
536
  return iso2 as CountryCode;
608
537
  }
609
538
 
@@ -4,14 +4,14 @@ import { parsePhoneNumberFromString, type CountryCode } from 'libphonenumber-js'
4
4
  @Injectable({ providedIn: 'root' })
5
5
  export class NgxsmkTelInputService {
6
6
  parse(input: string, iso2: CountryCode): { e164: string | null; national: string | null; isValid: boolean } {
7
- const phone = parsePhoneNumberFromString(input, iso2);
7
+ const phone = parsePhoneNumberFromString(input || '', iso2);
8
8
  if (!phone) return { e164: null, national: null, isValid: false };
9
9
  const isValid = phone.isValid();
10
10
  return { e164: isValid ? phone.number : null, national: phone.formatNational(), isValid };
11
11
  }
12
12
 
13
13
  isValid(input: string, iso2: CountryCode): boolean {
14
- const phone = parsePhoneNumberFromString(input, iso2);
14
+ const phone = parsePhoneNumberFromString(input || '', iso2);
15
15
  return !!phone && phone.isValid();
16
16
  }
17
17
  }