ngxsmk-tel-input 1.0.8 → 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,197 +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 { ReactiveFormsModule, FormBuilder, Validators, FormGroup } from '@angular/forms';
94
- import { CommonModule } from '@angular/common';
95
- import { NgxsmkTelInputComponent } from '../../../ngxsmk-tel-input/src/lib/ngxsmk-tel-input.component';
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';
96
93
 
97
94
  @Component({
98
95
  selector: 'app-root',
99
96
  standalone: true,
100
- imports: [ReactiveFormsModule, CommonModule, NgxsmkTelInputComponent],
97
+ imports: [ReactiveFormsModule, NgxsmkTelInputComponent, JsonPipe],
101
98
  template: `
102
99
  <form [formGroup]="fg" style="max-width:420px;display:grid;gap:12px">
103
100
  <ngxsmk-tel-input
104
101
  formControlName="phone"
105
102
  label="Phone"
106
103
  hint="Include area code"
104
+ dir="ltr"
107
105
  [initialCountry]="'US'"
108
106
  [preferredCountries]="['US','GB','AU']"
109
- (countryChange)="onCountry($event)">
107
+ [i18n]="enLabels"
108
+ [localizedCountries]="enCountries"
109
+ [autoPlaceholder]="'off'"
110
+ [clearAriaLabel]="'Clear phone number'">
110
111
  </ngxsmk-tel-input>
112
+
111
113
  <pre>Value: {{ fg.value | json }}</pre>
112
114
  </form>
113
115
  `
114
116
  })
115
117
  export class AppComponent {
116
- fg: FormGroup;
117
-
118
- constructor(private readonly fb: FormBuilder) {
119
- this.fg = this.fb.group({
120
- phone: ['', Validators.required]
121
- });
122
- }
123
-
124
- onCountry(e: { iso2: any }) { console.log('Country changed:', e.iso2); }
125
-
118
+ private readonly fb = inject(FormBuilder);
119
+ fg = this.fb.group({ phone: ['', Validators.required] });
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
+ };
126
137
  }
138
+
127
139
  ```
128
140
 
129
- **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
+ ---
130
144
 
131
- ### Template‑driven
145
+ ## 📝 Template‑driven usage
132
146
 
133
147
  ```html
134
148
  <form #f="ngForm">
135
149
  <ngxsmk-tel-input name="phone" [(ngModel)]="phone"></ngxsmk-tel-input>
136
150
  </form>
151
+ <!-- phone is an E.164 string or null -->
137
152
  ```
138
153
 
139
154
  ---
140
155
 
141
- ## Options (Inputs)
142
-
143
- | Option | Type | Default | Description |
144
- | ---------------------- | -------------------------------------- | ---------------------- | ---------------------------------------------------------------------- |
145
- | `initialCountry` | `CountryCode \| 'auto'` | `'US'` | Starting country. `'auto'` uses a simple geo‑IP stub (defaults to US). |
146
- | `preferredCountries` | `CountryCode[]` | `['US','GB']` | Countries pinned at the top. |
147
- | `onlyCountries` | `CountryCode[]` | — | Restrict selectable countries. |
148
- | `nationalMode` | `boolean` | `false` | Display national format in the box. Value still emits E.164. |
149
- | `separateDialCode` | `boolean` | `false` | Show dial code separately to the left. |
150
- | `allowDropdown` | `boolean` | `true` | Enable/disable country dropdown. |
151
- | `placeholder` | `string` | `'Enter phone number'` | Input placeholder. |
152
- | `autocomplete` | `string` | `'tel'` | Native autocomplete attribute. |
153
- | `disabled` | `boolean` | `false` | Disable the control. |
154
- | `label` | `string` | — | Optional label text. |
155
- | `hint` | `string` | — | Helper text shown below. |
156
- | `errorText` | `string` | — | Custom error message. |
157
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Preset sizing. |
158
- | `variant` | `'outline' \| 'filled' \| 'underline'` | `'outline'` | Visual style. |
159
- | `showClear` | `boolean` | `true` | Show a clear (×) button when not empty. |
160
- | `autoFocus` | `boolean` | `false` | Focus on init. |
161
- | `selectOnFocus` | `boolean` | `false` | Select text on focus. |
162
- | `formatOnBlur` | `boolean` | `true` | Pretty‑print on blur (national if `nationalMode`). |
163
- | `showErrorWhenTouched` | `boolean` | `true` | Only show error styles after the control is touched. |
164
- | `dropdownAttachToBody` | `boolean` | `true` | Attach dropdown to `<body>` to avoid clipping. |
165
- | `dropdownZIndex` | `number` | `2000` | Z‑index for dropdown panel. |
166
-
167
- > `CountryCode` is the ISO‑2 code from `libphonenumber-js` (e.g. `US`, `GB`).
168
-
169
- ### Outputs (Events)
170
-
171
- | Event | Payload | Description |
172
- | ---------------- | ---------------------------------------------------------- | ---------------------------------------- |
173
- | `countryChange` | `{ iso2: CountryCode }` | Fires when the selected country changes. |
174
- | `validityChange` | `boolean` | Emits when validity toggles. |
175
- | `inputChange` | `{ raw: string; e164: string \| null; iso2: CountryCode }` | Emitted on each input. |
176
-
177
- ### Public Methods
156
+ ## 🈺 Localization & RTL
178
157
 
179
- * `focus(): void`
180
- * `selectCountry(iso2: CountryCode): void`
158
+ You can localize the dropdown/search labels and override country names.
181
159
 
182
- ---
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
+ };
188
+
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>
183
204
 
184
- ## Supported Formats
205
+ ```
185
206
 
186
- * **E164** → `+41446681800`
187
- * **International** (display) → `+41 44 668 18 00`
188
- * **National** (display) → `044 668 18 00`
189
207
 
190
- > 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`
191
257
 
192
258
  ---
193
259
 
194
- ## Theming & Styling
260
+ ## 🎨 Theming (CSS variables)
195
261
 
196
- This component exposes CSS variables for quick theming:
262
+ Override on the element or a parent container:
197
263
 
198
264
  ```html
199
265
  <ngxsmk-tel-input style="
200
266
  --tel-border:#cbd5e1;
201
- --tel-ring:#2563eb;
202
- --tel-radius:12px;
203
- --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);
204
270
  --tel-dd-z: 3000;
205
271
  "></ngxsmk-tel-input>
206
272
  ```
207
273
 
208
- 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 }`
209
294
 
210
- 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.
211
296
 
212
297
  ---
213
298
 
214
- ## SSR
299
+ ## 🌐 SSR notes
215
300
 
216
- 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.
217
303
 
218
304
  ---
219
305
 
220
- ## Local Development
306
+ ## 🧪 Local development
221
307
 
222
- This repository is an Angular workspace with a library.
308
+ This repo is an Angular workspace with a library.
223
309
 
224
310
  ```bash
225
311
  # Build the library
226
312
  ng build ngxsmk-tel-input
227
313
 
228
- # Try it in a demo app inside the workspace
314
+ # Option A: use it inside a demo app in the same workspace
229
315
  ng serve demo
230
316
 
231
- # Or pack and install in another app locally
317
+ # Option B: install locally via tarball in another app
232
318
  cd dist/ngxsmk-tel-input && npm pack
233
-
234
319
  # in your other app
235
- 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
236
321
  ```
237
322
 
238
- Tip: you can also map a workspace path alias in `tsconfig.base.json`:
239
-
240
- ```jsonc
241
- {
242
- "compilerOptions": {
243
- "paths": {
244
- "ngxsmk-tel-input": ["dist/ngxsmk-tel-input"]
245
- }
246
- }
247
- }
248
- ```
323
+ > Workspace aliasing via `tsconfig.paths` also works (map `"ngxsmk-tel-input": ["dist/ngxsmk-tel-input"]`).
249
324
 
250
325
  ---
251
326
 
252
- ## Troubleshooting
327
+ ## 🧯 Troubleshooting
253
328
 
254
- * **Unstyled dropdown / bullets / missing flags** → Ensure CSS & assets are added in your app’s `angular.json`, then restart the dev server.
255
- * **`TS2307: Cannot find module 'ngxsmk-tel-input'`** → Build the library first so `dist/ngxsmk-tel-input` exists (or install from npm).
256
- * **Peer dependency conflict** → Your app must be Angular 17–19 to satisfy peers.
257
- * **Dropdown clipped by parent** → Keep `dropdownAttachToBody=true` or raise `dropdownZIndex`.
258
-
259
- ---
329
+ **UI looks unstyled / bullets in dropdown**
330
+ Add the CSS and assets in `angular.json` (see Install). Restart the dev server.
260
331
 
261
- ## 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.
262
334
 
263
- 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`.
264
337
 
265
- 1. `npm ci` and `ng build`
266
- 2. Cover behavior changes with tests if possible
267
- 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.
268
340
 
269
- 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`.
270
343
 
271
344
  ---
272
345
 
273
- ## License
346
+ ## 📃 License
274
347
 
275
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.8",
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,25 +14,39 @@ 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="ngxsmk-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
51
  <label class="ngxsmk-tel__label" [for]="resolvedId">{{ label }}</label>
38
52
  }
@@ -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()"
@@ -58,11 +74,10 @@ type IntlTelInstance = any;
58
74
  <button type="button"
59
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) {
@@ -72,12 +87,11 @@ type IntlTelInstance = any;
72
87
  @if (showError) {
73
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
- .ngxsmk-tel {
123
- width: 100%;
124
- color: var(--tel-fg);
125
- }
126
-
127
- .ngxsmk-tel.disabled {
128
- opacity: .7;
129
- cursor: not-allowed;
130
- }
131
-
132
- .ngxsmk-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
- .ngxsmk-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,151 +171,37 @@ 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);
277
- }
278
-
279
- :host ::ng-deep .iti__dial-code {
280
- color: var(--tel-placeholder);
281
- font-weight: 600;
282
- margin-left: 10px;
283
- }
284
-
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;
191
+ display: grid; grid-template-columns: 28px 1fr auto; align-items: center;
192
+ column-gap: .5rem; padding: 10px 12px; cursor: pointer;
308
193
  }
194
+ :host ::ng-deep .iti__dial-code { color: var(--tel-placeholder); font-weight: 600; margin-left: 10px; }
309
195
 
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
196
  .ngxsmk-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
- .ngxsmk-tel__clear:hover {
345
- background: rgba(148, 163, 184, .15);
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);
346
200
  }
201
+ .ngxsmk-tel__clear:hover { background: rgba(148, 163, 184, .15); }
347
202
 
348
- /* Hint & Error */
349
- .ngxsmk-tel__hint {
350
- margin-top: 6px;
351
- font-size: 12px;
352
- color: var(--tel-placeholder);
353
- }
354
-
355
- .ngxsmk-tel__error {
356
- margin-top: 6px;
357
- font-size: 12px;
358
- color: var(--tel-error);
359
- }
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); }
360
205
 
361
206
  .ngxsmk-tel__wrap.has-error .ngxsmk-tel-input__control {
362
207
  border-color: var(--tel-error);
@@ -364,10 +209,10 @@ type IntlTelInstance = any;
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
  }