ngxsmk-tel-input 1.0.8 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -135
- package/docs/kr.png +0 -0
- package/package.json +1 -1
- package/src/lib/ngxsmk-tel-input.component.ts +152 -98
- package/src/lib/ngxsmk-tel-input.service.ts +2 -2
package/README.md
CHANGED
|
@@ -1,51 +1,52 @@
|
|
|
1
|
-
# ngxsmk-tel-input
|
|
1
|
+
# ngxsmk-tel-input
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[
|
|
5
|
-
[](https://github.com/toozuuu/ngxsmk-tel-input/blob/main/LICENSE)
|
|
6
|
-
[](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
|
-
|
|
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
|
|
|
18
10
|
## Screenshots
|
|
19
11
|
|
|
20
12
|
<p align="left">
|
|
21
|
-
<img src="https://unpkg.com/ngxsmk-tel-input@
|
|
13
|
+
<img src="https://unpkg.com/ngxsmk-tel-input@latest/docs/valid.png" alt="Angular international phone input - valid" width="420" />
|
|
22
14
|
|
|
23
|
-
<img src="https://unpkg.com/ngxsmk-tel-input@
|
|
15
|
+
<img src="https://unpkg.com/ngxsmk-tel-input@latest/docs/invalid.png" alt="Angular international phone input - Invalid" width="420" />
|
|
24
16
|
</p>
|
|
25
17
|
|
|
26
18
|
---
|
|
27
19
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
| ngxsmk-tel-input | Angular |
|
|
31
|
-
|------------------| --------------- |
|
|
32
|
-
| `1.0.x` | `17.x` – `19.x` |
|
|
20
|
+
## ✨ Features
|
|
33
21
|
|
|
34
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
41
|
+
## 📦 Install
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
44
|
npm i ngxsmk-tel-input intl-tel-input libphonenumber-js
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
###
|
|
47
|
+
### Add styles & flag assets (in your **app**, not the library)
|
|
47
48
|
|
|
48
|
-
|
|
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
|
|
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
|
|
81
|
+
Restart the dev server after changes.
|
|
83
82
|
|
|
84
83
|
---
|
|
85
84
|
|
|
86
|
-
##
|
|
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
|
|
94
|
-
import {
|
|
95
|
-
import { NgxsmkTelInputComponent } from '
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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:**
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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@latest/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
|
-
|
|
205
|
+
```
|
|
185
206
|
|
|
186
|
-
* **E164** → `+41446681800`
|
|
187
|
-
* **International** (display) → `+41 44 668 18 00`
|
|
188
|
-
* **National** (display) → `044 668 18 00`
|
|
189
207
|
|
|
190
|
-
|
|
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
|
|
260
|
+
## 🎨 Theming (CSS variables)
|
|
195
261
|
|
|
196
|
-
|
|
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:#
|
|
202
|
-
--tel-radius:
|
|
203
|
-
--tel-dd-item-hover: rgba(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
306
|
+
## 🧪 Local development
|
|
221
307
|
|
|
222
|
-
This
|
|
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
|
-
#
|
|
314
|
+
# Option A: use it inside a demo app in the same workspace
|
|
229
315
|
ng serve demo
|
|
230
316
|
|
|
231
|
-
#
|
|
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
|
|
320
|
+
npm i ../path-to-workspace/dist/ngxsmk-tel-input/ngxsmk-tel-input-<version>.tgz
|
|
236
321
|
```
|
|
237
322
|
|
|
238
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
@@ -20,19 +20,33 @@ import {
|
|
|
20
20
|
ControlValueAccessor,
|
|
21
21
|
NG_VALIDATORS,
|
|
22
22
|
NG_VALUE_ACCESSOR,
|
|
23
|
-
ValidationErrors
|
|
23
|
+
ValidationErrors,
|
|
24
|
+
Validator
|
|
24
25
|
} from '@angular/forms';
|
|
25
26
|
import type {CountryCode} from 'libphonenumber-js';
|
|
26
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"
|
|
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]="
|
|
77
|
+
[attr.aria-label]="clearAriaLabel">
|
|
62
78
|
×
|
|
63
79
|
</button>
|
|
64
80
|
}
|
|
65
|
-
|
|
66
81
|
</div>
|
|
67
82
|
|
|
68
83
|
@if (hint && !showError) {
|
|
@@ -72,7 +87,6 @@ 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: [
|
|
@@ -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) {
|
|
@@ -140,8 +152,7 @@ type IntlTelInstance = any;
|
|
|
140
152
|
position: relative;
|
|
141
153
|
}
|
|
142
154
|
|
|
143
|
-
.ngxsmk-tel-input__wrapper,
|
|
144
|
-
:host ::ng-deep .iti {
|
|
155
|
+
.ngxsmk-tel-input__wrapper, :host ::ng-deep .iti {
|
|
145
156
|
width: 100%;
|
|
146
157
|
}
|
|
147
158
|
|
|
@@ -153,9 +164,9 @@ type IntlTelInstance = any;
|
|
|
153
164
|
background: var(--tel-bg);
|
|
154
165
|
border: 1px solid var(--tel-border);
|
|
155
166
|
border-radius: var(--tel-radius);
|
|
156
|
-
padding: 10px 40px 10px 12px;
|
|
167
|
+
padding: 10px 40px 10px 12px;
|
|
157
168
|
outline: none;
|
|
158
|
-
transition: border-color .15s
|
|
169
|
+
transition: border-color .15s, box-shadow .15s, background .15s;
|
|
159
170
|
}
|
|
160
171
|
|
|
161
172
|
.ngxsmk-tel-input__control::placeholder {
|
|
@@ -171,7 +182,6 @@ type IntlTelInstance = any;
|
|
|
171
182
|
box-shadow: var(--tel-focus-shadow);
|
|
172
183
|
}
|
|
173
184
|
|
|
174
|
-
/* Size presets */
|
|
175
185
|
[data-size="sm"] .ngxsmk-tel-input__control {
|
|
176
186
|
height: 34px;
|
|
177
187
|
font-size: 13px;
|
|
@@ -186,7 +196,6 @@ type IntlTelInstance = any;
|
|
|
186
196
|
border-radius: 14px;
|
|
187
197
|
}
|
|
188
198
|
|
|
189
|
-
/* Variants */
|
|
190
199
|
[data-variant="filled"] .ngxsmk-tel-input__control {
|
|
191
200
|
background: rgba(148, 163, 184, .08);
|
|
192
201
|
}
|
|
@@ -220,7 +229,6 @@ type IntlTelInstance = any;
|
|
|
220
229
|
align-items: center;
|
|
221
230
|
}
|
|
222
231
|
|
|
223
|
-
/* Core dropdown panel */
|
|
224
232
|
:host ::ng-deep .iti__country-list {
|
|
225
233
|
background: var(--tel-dd-bg);
|
|
226
234
|
border: 1px solid var(--tel-dd-border);
|
|
@@ -233,12 +241,10 @@ type IntlTelInstance = any;
|
|
|
233
241
|
z-index: var(--tel-dd-z);
|
|
234
242
|
}
|
|
235
243
|
|
|
236
|
-
/* When attached to <body>, it's wrapped in .iti--container */
|
|
237
244
|
:host ::ng-deep .iti--container .iti__country-list {
|
|
238
245
|
z-index: var(--tel-dd-z);
|
|
239
246
|
}
|
|
240
247
|
|
|
241
|
-
/* Search input (sticky header) */
|
|
242
248
|
:host ::ng-deep .iti__search-input {
|
|
243
249
|
position: sticky;
|
|
244
250
|
top: 0;
|
|
@@ -252,11 +258,6 @@ type IntlTelInstance = any;
|
|
|
252
258
|
color: var(--tel-fg);
|
|
253
259
|
}
|
|
254
260
|
|
|
255
|
-
:host ::ng-deep .iti__search-input::placeholder {
|
|
256
|
-
color: var(--tel-placeholder);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/* Rows: flag | country name | dial code (right) */
|
|
260
261
|
:host ::ng-deep .iti__country {
|
|
261
262
|
display: grid;
|
|
262
263
|
grid-template-columns: 28px 1fr auto;
|
|
@@ -266,65 +267,12 @@ type IntlTelInstance = any;
|
|
|
266
267
|
cursor: pointer;
|
|
267
268
|
}
|
|
268
269
|
|
|
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
270
|
:host ::ng-deep .iti__dial-code {
|
|
280
271
|
color: var(--tel-placeholder);
|
|
281
272
|
font-weight: 600;
|
|
282
273
|
margin-left: 10px;
|
|
283
274
|
}
|
|
284
275
|
|
|
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
|
-
}
|
|
309
|
-
|
|
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
276
|
.ngxsmk-tel__clear {
|
|
329
277
|
position: absolute;
|
|
330
278
|
right: 8px;
|
|
@@ -345,7 +293,6 @@ type IntlTelInstance = any;
|
|
|
345
293
|
background: rgba(148, 163, 184, .15);
|
|
346
294
|
}
|
|
347
295
|
|
|
348
|
-
/* Hint & Error */
|
|
349
296
|
.ngxsmk-tel__hint {
|
|
350
297
|
margin-top: 6px;
|
|
351
298
|
font-size: 12px;
|
|
@@ -364,10 +311,10 @@ type IntlTelInstance = any;
|
|
|
364
311
|
}
|
|
365
312
|
`]
|
|
366
313
|
})
|
|
367
|
-
export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor {
|
|
314
|
+
export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor, Validator {
|
|
368
315
|
@ViewChild('telInput', {static: true}) inputRef!: ElementRef<HTMLInputElement>;
|
|
369
316
|
|
|
370
|
-
/*
|
|
317
|
+
/* Core config */
|
|
371
318
|
@Input() initialCountry: CountryCode | 'auto' = 'US';
|
|
372
319
|
@Input() preferredCountries: CountryCode[] = ['US', 'GB'];
|
|
373
320
|
@Input() onlyCountries?: CountryCode[];
|
|
@@ -375,13 +322,13 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
375
322
|
@Input() separateDialCode = false;
|
|
376
323
|
@Input() allowDropdown = true;
|
|
377
324
|
|
|
378
|
-
|
|
325
|
+
/* UX */
|
|
326
|
+
@Input() placeholder?: string; // keep undefined to let plugin set example placeholders (requires utilsScript)
|
|
379
327
|
@Input() autocomplete = 'tel';
|
|
380
328
|
@Input() name?: string;
|
|
381
329
|
@Input() inputId?: string;
|
|
382
330
|
@Input() disabled = false;
|
|
383
331
|
|
|
384
|
-
/* New UI/UX inputs */
|
|
385
332
|
@Input() label?: string;
|
|
386
333
|
@Input() hint?: string;
|
|
387
334
|
@Input() errorText?: string;
|
|
@@ -393,9 +340,34 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
393
340
|
@Input() formatOnBlur = true;
|
|
394
341
|
@Input() showErrorWhenTouched = true;
|
|
395
342
|
|
|
396
|
-
|
|
397
|
-
@Input() dropdownAttachToBody = true;
|
|
398
|
-
@Input() dropdownZIndex = 2000;
|
|
343
|
+
/* Dropdown plumbing */
|
|
344
|
+
@Input() dropdownAttachToBody = true;
|
|
345
|
+
@Input() dropdownZIndex = 2000;
|
|
346
|
+
|
|
347
|
+
/* Localization + RTL */
|
|
348
|
+
@Input('i18n') i18n?: IntlTelI18n;
|
|
349
|
+
|
|
350
|
+
@Input('telI18n') set telI18n(v: IntlTelI18n | undefined) {
|
|
351
|
+
this.i18n = v;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
@Input('localizedCountries') localizedCountries?: CountryMap;
|
|
355
|
+
|
|
356
|
+
@Input('telLocalizedCountries') set telLocalizedCountries(v: CountryMap | undefined) {
|
|
357
|
+
this.localizedCountries = v;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@Input() clearAriaLabel = 'Clear phone number';
|
|
361
|
+
@Input() dir: 'ltr' | 'rtl' = 'ltr';
|
|
362
|
+
|
|
363
|
+
/* Placeholders (intl-tel-input) */
|
|
364
|
+
@Input() autoPlaceholder: 'off' | 'polite' | 'aggressive' = 'off'; // default OFF since no utils fallback
|
|
365
|
+
@Input() utilsScript?: string;
|
|
366
|
+
@Input() customPlaceholder?: (example: string, country: any) => string;
|
|
367
|
+
|
|
368
|
+
/* Digits-only controls */
|
|
369
|
+
@Input() digitsOnly = true;
|
|
370
|
+
@Input() allowLeadingPlus = true;
|
|
399
371
|
|
|
400
372
|
/* Outputs */
|
|
401
373
|
@Output() countryChange = new EventEmitter<{ iso2: CountryCode }>();
|
|
@@ -408,11 +380,12 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
408
380
|
};
|
|
409
381
|
private onTouchedCb: () => void = () => {
|
|
410
382
|
};
|
|
383
|
+
private validatorChange?: () => void;
|
|
411
384
|
private lastEmittedValid = false;
|
|
412
385
|
private pendingWrite: string | null = null;
|
|
413
386
|
private touched = false;
|
|
414
387
|
|
|
415
|
-
readonly resolvedId =
|
|
388
|
+
readonly resolvedId = this.inputId || ('tel-' + Math.random().toString(36).slice(2));
|
|
416
389
|
|
|
417
390
|
constructor(
|
|
418
391
|
private readonly zone: NgZone,
|
|
@@ -423,11 +396,6 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
423
396
|
|
|
424
397
|
async ngAfterViewInit() {
|
|
425
398
|
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
399
|
await this.initIntlTelInput();
|
|
432
400
|
this.bindDomListeners();
|
|
433
401
|
|
|
@@ -441,9 +409,17 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
441
409
|
|
|
442
410
|
ngOnChanges(changes: SimpleChanges): void {
|
|
443
411
|
if (!isPlatformBrowser(this.platformId)) return;
|
|
444
|
-
const configChanged = [
|
|
445
|
-
|
|
446
|
-
|
|
412
|
+
const configChanged = [
|
|
413
|
+
'initialCountry', 'preferredCountries', 'onlyCountries',
|
|
414
|
+
'separateDialCode', 'allowDropdown', 'nationalMode',
|
|
415
|
+
'i18n', 'localizedCountries', 'dir',
|
|
416
|
+
'autoPlaceholder', 'utilsScript', 'customPlaceholder',
|
|
417
|
+
'digitsOnly', 'allowLeadingPlus'
|
|
418
|
+
].some(k => k in changes && !changes[k]?.firstChange);
|
|
419
|
+
if (configChanged && this.iti) {
|
|
420
|
+
this.reinitPlugin();
|
|
421
|
+
this.validatorChange?.();
|
|
422
|
+
}
|
|
447
423
|
}
|
|
448
424
|
|
|
449
425
|
ngOnDestroy(): void {
|
|
@@ -485,6 +461,10 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
485
461
|
return valid ? null : {phoneInvalid: true};
|
|
486
462
|
}
|
|
487
463
|
|
|
464
|
+
registerOnValidatorChange(fn: () => void): void {
|
|
465
|
+
this.validatorChange = fn;
|
|
466
|
+
}
|
|
467
|
+
|
|
488
468
|
// ----- Public helpers -----
|
|
489
469
|
focus(): void {
|
|
490
470
|
this.inputRef?.nativeElement.focus();
|
|
@@ -510,6 +490,10 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
510
490
|
// ----- Plugin wiring -----
|
|
511
491
|
private async initIntlTelInput() {
|
|
512
492
|
const [{default: intlTelInput}] = await Promise.all([import('intl-tel-input')]);
|
|
493
|
+
|
|
494
|
+
const toLowerKeys = (m?: CountryMap) =>
|
|
495
|
+
m ? Object.fromEntries(Object.entries(m).map(([k, v]) => [k.toLowerCase(), v])) : undefined;
|
|
496
|
+
|
|
513
497
|
const config: any = {
|
|
514
498
|
initialCountry: this.initialCountry === 'auto' ? 'auto' : (this.initialCountry?.toLowerCase() || 'us'),
|
|
515
499
|
preferredCountries: (this.preferredCountries ?? []).map(c => c.toLowerCase()),
|
|
@@ -518,14 +502,25 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
518
502
|
allowDropdown: this.allowDropdown,
|
|
519
503
|
separateDialCode: this.separateDialCode,
|
|
520
504
|
geoIpLookup: (cb: (iso2: string) => void) => cb('us'),
|
|
521
|
-
|
|
505
|
+
|
|
506
|
+
// placeholders — NOTE: no CDN fallback anymore
|
|
507
|
+
autoPlaceholder: this.autoPlaceholder, // 'off' | 'polite' | 'aggressive'
|
|
508
|
+
utilsScript: this.utilsScript,
|
|
509
|
+
customPlaceholder: this.customPlaceholder,
|
|
510
|
+
|
|
511
|
+
// localization
|
|
512
|
+
i18n: this.i18n,
|
|
513
|
+
localizedCountries: toLowerKeys(this.localizedCountries),
|
|
514
|
+
|
|
515
|
+
// dropdown container
|
|
522
516
|
dropdownContainer: this.dropdownAttachToBody && typeof document !== 'undefined' ? document.body : undefined
|
|
523
517
|
};
|
|
518
|
+
|
|
524
519
|
this.zone.runOutsideAngular(() => {
|
|
525
520
|
this.iti = intlTelInput(this.inputRef.nativeElement, config);
|
|
526
521
|
});
|
|
527
522
|
|
|
528
|
-
//
|
|
523
|
+
// z-index for dropdown
|
|
529
524
|
(this.inputRef.nativeElement as HTMLElement).style.setProperty('--tel-dd-z', String(this.dropdownZIndex));
|
|
530
525
|
}
|
|
531
526
|
|
|
@@ -553,16 +548,74 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
553
548
|
}
|
|
554
549
|
}
|
|
555
550
|
|
|
551
|
+
// ----- Input filtering (digits-only) -----
|
|
552
|
+
private sanitizeDigits(value: string): string {
|
|
553
|
+
if (!this.digitsOnly) return value;
|
|
554
|
+
let v = value.replace(/[^\d+]/g, ''); // keep digits and pluses
|
|
555
|
+
// allow only ONE leading + (if enabled)
|
|
556
|
+
if (this.allowLeadingPlus) {
|
|
557
|
+
const hasLeadingPlus = v.startsWith('+');
|
|
558
|
+
v = (hasLeadingPlus ? '+' : '') + v.replace(/\+/g, '');
|
|
559
|
+
} else {
|
|
560
|
+
v = v.replace(/\+/g, '');
|
|
561
|
+
}
|
|
562
|
+
return v;
|
|
563
|
+
}
|
|
564
|
+
|
|
556
565
|
private bindDomListeners() {
|
|
557
566
|
const el = this.inputRef.nativeElement;
|
|
567
|
+
|
|
558
568
|
this.zone.runOutsideAngular(() => {
|
|
559
|
-
|
|
569
|
+
// prevent invalid chars while typing
|
|
570
|
+
el.addEventListener('beforeinput', (ev: InputEvent) => {
|
|
571
|
+
if (!this.digitsOnly) return;
|
|
572
|
+
const data = (ev as any).data as string | null;
|
|
573
|
+
|
|
574
|
+
// allow deletions, cuts, etc.
|
|
575
|
+
if (!data || ev.inputType !== 'insertText') return;
|
|
576
|
+
|
|
577
|
+
const pos = el.selectionStart ?? 0;
|
|
578
|
+
const isDigit = data >= '0' && data <= '9';
|
|
579
|
+
const isPlusAtStart = this.allowLeadingPlus && data === '+' && pos === 0 && !el.value.includes('+');
|
|
580
|
+
|
|
581
|
+
if (!isDigit && !isPlusAtStart) ev.preventDefault();
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// sanitize pastes
|
|
585
|
+
el.addEventListener('paste', (e: ClipboardEvent) => {
|
|
586
|
+
if (!this.digitsOnly) return;
|
|
587
|
+
e.preventDefault();
|
|
588
|
+
const text = (e.clipboardData || (window as any).clipboardData).getData('text');
|
|
589
|
+
const sanitized = this.sanitizeDigits(text);
|
|
590
|
+
const start = el.selectionStart ?? el.value.length;
|
|
591
|
+
const end = el.selectionEnd ?? el.value.length;
|
|
592
|
+
el.setRangeText(sanitized, start, end, 'end');
|
|
593
|
+
queueMicrotask(() => this.handleInput());
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// catch any remaining non-digit changes (e.g., programmatic)
|
|
597
|
+
el.addEventListener('input', () => {
|
|
598
|
+
if (this.digitsOnly) {
|
|
599
|
+
const val = el.value;
|
|
600
|
+
const sanitized = this.sanitizeDigits(val);
|
|
601
|
+
if (val !== sanitized) {
|
|
602
|
+
const caret = el.selectionStart ?? sanitized.length;
|
|
603
|
+
el.value = sanitized;
|
|
604
|
+
el.setSelectionRange(caret, caret);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
this.handleInput();
|
|
608
|
+
});
|
|
609
|
+
|
|
560
610
|
el.addEventListener('countrychange', () => {
|
|
561
611
|
const iso2 = this.currentIso2();
|
|
562
|
-
this.zone.run(() =>
|
|
612
|
+
this.zone.run(() => {
|
|
613
|
+
this.countryChange.emit({iso2});
|
|
614
|
+
this.validatorChange?.();
|
|
615
|
+
});
|
|
563
616
|
this.handleInput();
|
|
564
617
|
});
|
|
565
|
-
|
|
618
|
+
|
|
566
619
|
el.addEventListener('blur', () => this.onBlur());
|
|
567
620
|
});
|
|
568
621
|
}
|
|
@@ -575,7 +628,7 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
575
628
|
if (!raw) return;
|
|
576
629
|
const parsed = this.tel.parse(raw, this.currentIso2());
|
|
577
630
|
if (this.nationalMode && parsed.national) {
|
|
578
|
-
this.setInputValue(parsed.national.replace(/\s{2,}/g, ' '));
|
|
631
|
+
this.setInputValue((parsed.national || '').replace(/\s{2,}/g, ' '));
|
|
579
632
|
}
|
|
580
633
|
}
|
|
581
634
|
|
|
@@ -603,7 +656,8 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
|
|
|
603
656
|
}
|
|
604
657
|
|
|
605
658
|
private currentIso2(): CountryCode {
|
|
606
|
-
const iso2 = (this.iti?.getSelectedCountryData?.().iso2 ?? this.initialCountry ?? 'US')
|
|
659
|
+
const iso2 = (this.iti?.getSelectedCountryData?.().iso2 ?? this.initialCountry ?? 'US')
|
|
660
|
+
.toString().toUpperCase();
|
|
607
661
|
return iso2 as CountryCode;
|
|
608
662
|
}
|
|
609
663
|
|
|
@@ -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
|
}
|