ngxsmk-tel-input 1.1.1 → 1.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ngxsmk-tel-input",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
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
  "ngxsmk-tel-input",
@@ -0,0 +1,220 @@
1
+ /* ---------- Theme tokens ---------- */
2
+ :host {
3
+ --tel-bg: #fff;
4
+ --tel-fg: #0f172a;
5
+ --tel-border: #c0c0c0;
6
+ --tel-border-hover: #9aa0a6;
7
+ --tel-ring: #2563eb;
8
+ --tel-placeholder: #9ca3af;
9
+ --tel-error: #ef4444;
10
+ --tel-radius: 12px;
11
+ --tel-focus-shadow: 0 0 0 3px rgba(37, 99, 235, .25);
12
+
13
+ --tel-dd-bg: var(--tel-bg);
14
+ --tel-dd-border: var(--tel-border);
15
+ --tel-dd-shadow: 0 24px 60px rgba(0, 0, 0, .18);
16
+ --tel-dd-radius: 12px;
17
+ --tel-dd-item-hover: rgba(37, 99, 235, .08);
18
+ --tel-dd-z: 2000;
19
+ --tel-dd-search-bg: rgba(148, 163, 184, .08);
20
+
21
+ display: block;
22
+ }
23
+
24
+ :host-context(.dark) {
25
+ --tel-bg: #0b0f17;
26
+ --tel-fg: #e5e7eb;
27
+ --tel-border: #334155;
28
+ --tel-border-hover: #475569;
29
+ --tel-ring: #60a5fa;
30
+ --tel-placeholder: #94a3b8;
31
+
32
+ --tel-dd-bg: #0f1521;
33
+ --tel-dd-border: #324056;
34
+ --tel-dd-search-bg: rgba(148, 163, 184, .12);
35
+ }
36
+
37
+ /* ---------- Structure ---------- */
38
+ .ngxsmk-tel {
39
+ width: 100%;
40
+ color: var(--tel-fg);
41
+ }
42
+
43
+ .ngxsmk-tel.disabled {
44
+ opacity: .7;
45
+ cursor: not-allowed;
46
+ }
47
+
48
+ .ngxsmk-tel__label {
49
+ display: inline-block;
50
+ margin-bottom: 6px;
51
+ font-size: .875rem;
52
+ font-weight: 500;
53
+ }
54
+
55
+ .ngxsmk-tel__wrap {
56
+ position: relative;
57
+ }
58
+
59
+ .ngxsmk-tel-input__wrapper,
60
+ :host ::ng-deep .iti {
61
+ width: 100%;
62
+ }
63
+
64
+ .ngxsmk-tel-input__control {
65
+ width: 100%;
66
+ height: 40px;
67
+ font: inherit;
68
+ color: var(--tel-fg);
69
+ background: var(--tel-bg);
70
+ border: 1px solid var(--tel-border);
71
+ border-radius: var(--tel-radius);
72
+ padding: 10px 40px 10px 12px;
73
+ outline: none;
74
+ transition: border-color .15s, box-shadow .15s, background .15s;
75
+ }
76
+
77
+ .ngxsmk-tel-input__control::placeholder {
78
+ color: var(--tel-placeholder);
79
+ }
80
+
81
+ .ngxsmk-tel-input__control:hover {
82
+ border-color: var(--tel-border-hover);
83
+ }
84
+
85
+ .ngxsmk-tel-input__control:focus {
86
+ border-color: var(--tel-ring);
87
+ box-shadow: var(--tel-focus-shadow);
88
+ }
89
+
90
+ /* Size presets */
91
+ [data-size="sm"] .ngxsmk-tel-input__control {
92
+ height: 34px;
93
+ font-size: 13px;
94
+ padding: 6px 36px 6px 10px;
95
+ border-radius: 10px;
96
+ }
97
+
98
+ [data-size="lg"] .ngxsmk-tel-input__control {
99
+ height: 46px;
100
+ font-size: 16px;
101
+ padding: 12px 44px 12px 14px;
102
+ border-radius: 14px;
103
+ }
104
+
105
+ /* Variants */
106
+ [data-variant="filled"] .ngxsmk-tel-input__control {
107
+ background: rgba(148, 163, 184, .08);
108
+ }
109
+
110
+ [data-variant="underline"] .ngxsmk-tel-input__control {
111
+ border: 0;
112
+ border-bottom: 2px solid var(--tel-border);
113
+ border-radius: 0;
114
+ padding-left: 0;
115
+ padding-right: 34px;
116
+ }
117
+
118
+ [data-variant="underline"] .ngxsmk-tel-input__control:focus {
119
+ border-bottom-color: var(--tel-ring);
120
+ box-shadow: none;
121
+ }
122
+
123
+ /* ---------- intl-tel-input dropdown (deep selectors) ---------- */
124
+ :host ::ng-deep .iti__flag-container {
125
+ border-top-left-radius: var(--tel-radius);
126
+ border-bottom-left-radius: var(--tel-radius);
127
+ border: 1px solid var(--tel-border);
128
+ border-right: none;
129
+ background: var(--tel-bg);
130
+ }
131
+
132
+ :host ::ng-deep .iti__selected-flag {
133
+ height: 100%;
134
+ padding: 0 10px;
135
+ display: inline-flex;
136
+ align-items: center;
137
+ }
138
+
139
+ :host ::ng-deep .iti__country-list {
140
+ background: var(--tel-dd-bg);
141
+ border: 1px solid var(--tel-dd-border);
142
+ border-radius: var(--tel-dd-radius);
143
+ box-shadow: var(--tel-dd-shadow);
144
+ max-height: min(50vh, 360px);
145
+ overflow: auto;
146
+ padding: 6px 0;
147
+ width: max(280px, 100%);
148
+ z-index: var(--tel-dd-z);
149
+ }
150
+
151
+ :host ::ng-deep .iti--container .iti__country-list {
152
+ z-index: var(--tel-dd-z);
153
+ }
154
+
155
+ :host ::ng-deep .iti__search-input {
156
+ position: sticky;
157
+ top: 0;
158
+ margin: 0;
159
+ padding: 10px 12px;
160
+ width: 100%;
161
+ border: 0;
162
+ border-bottom: 1px solid var(--tel-dd-border);
163
+ outline: none;
164
+ background: var(--tel-dd-search-bg);
165
+ color: var(--tel-fg);
166
+ }
167
+
168
+ :host ::ng-deep .iti__country {
169
+ display: grid;
170
+ grid-template-columns: 28px 1fr auto;
171
+ align-items: center;
172
+ column-gap: .5rem;
173
+ padding: 10px 12px;
174
+ cursor: pointer;
175
+ }
176
+
177
+ :host ::ng-deep .iti__dial-code {
178
+ color: var(--tel-placeholder);
179
+ font-weight: 600;
180
+ margin-left: 10px;
181
+ }
182
+
183
+ /* Clear button */
184
+ .ngxsmk-tel__clear {
185
+ position: absolute;
186
+ right: 8px;
187
+ top: 50%;
188
+ transform: translateY(-50%);
189
+ border: 0;
190
+ background: transparent;
191
+ font-size: 18px;
192
+ line-height: 1;
193
+ width: 28px;
194
+ height: 28px;
195
+ border-radius: 50%;
196
+ cursor: pointer;
197
+ color: var(--tel-placeholder);
198
+ }
199
+
200
+ .ngxsmk-tel__clear:hover {
201
+ background: rgba(148, 163, 184, .15);
202
+ }
203
+
204
+ /* Hint & Error */
205
+ .ngxsmk-tel__hint {
206
+ margin-top: 6px;
207
+ font-size: 12px;
208
+ color: var(--tel-placeholder);
209
+ }
210
+
211
+ .ngxsmk-tel__error {
212
+ margin-top: 6px;
213
+ font-size: 12px;
214
+ color: var(--tel-error);
215
+ }
216
+
217
+ .ngxsmk-tel__wrap.has-error .ngxsmk-tel-input__control {
218
+ border-color: var(--tel-error);
219
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, .15);
220
+ }
@@ -89,260 +89,45 @@ export interface IntlTelI18n {
89
89
  }
90
90
  </div>
91
91
  `,
92
+ styleUrls: ['./ngxsmk-tel-input.component.scss'],
92
93
  providers: [
93
94
  {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true},
94
95
  {provide: NG_VALIDATORS, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true}
95
- ],
96
- styles: [`
97
- /* ---------- Theme tokens ---------- */
98
- :host {
99
- --tel-bg: #fff;
100
- --tel-fg: #0f172a;
101
- --tel-border: #c0c0c0;
102
- --tel-border-hover: #9aa0a6;
103
- --tel-ring: #2563eb;
104
- --tel-placeholder: #9ca3af;
105
- --tel-error: #ef4444;
106
- --tel-radius: 12px;
107
- --tel-focus-shadow: 0 0 0 3px rgba(37, 99, 235, .25);
108
-
109
- --tel-dd-bg: var(--tel-bg);
110
- --tel-dd-border: var(--tel-border);
111
- --tel-dd-shadow: 0 24px 60px rgba(0, 0, 0, .18);
112
- --tel-dd-radius: 12px;
113
- --tel-dd-item-hover: rgba(37, 99, 235, .08);
114
- --tel-dd-z: 2000;
115
- --tel-dd-search-bg: rgba(148, 163, 184, .08);
116
-
117
- display: block;
118
- }
119
-
120
- :host-context(.dark) {
121
- --tel-bg: #0b0f17;
122
- --tel-fg: #e5e7eb;
123
- --tel-border: #334155;
124
- --tel-border-hover: #475569;
125
- --tel-ring: #60a5fa;
126
- --tel-placeholder: #94a3b8;
127
-
128
- --tel-dd-bg: #0f1521;
129
- --tel-dd-border: #324056;
130
- --tel-dd-search-bg: rgba(148, 163, 184, .12);
131
- }
132
-
133
- /* ---------- Structure ---------- */
134
- .ngxsmk-tel {
135
- width: 100%;
136
- color: var(--tel-fg);
137
- }
138
-
139
- .ngxsmk-tel.disabled {
140
- opacity: .7;
141
- cursor: not-allowed;
142
- }
143
-
144
- .ngxsmk-tel__label {
145
- display: inline-block;
146
- margin-bottom: 6px;
147
- font-size: .875rem;
148
- font-weight: 500;
149
- }
150
-
151
- .ngxsmk-tel__wrap {
152
- position: relative;
153
- }
154
-
155
- .ngxsmk-tel-input__wrapper, :host ::ng-deep .iti {
156
- width: 100%;
157
- }
158
-
159
- .ngxsmk-tel-input__control {
160
- width: 100%;
161
- height: 40px;
162
- font: inherit;
163
- color: var(--tel-fg);
164
- background: var(--tel-bg);
165
- border: 1px solid var(--tel-border);
166
- border-radius: var(--tel-radius);
167
- padding: 10px 40px 10px 12px;
168
- outline: none;
169
- transition: border-color .15s, box-shadow .15s, background .15s;
170
- }
171
-
172
- .ngxsmk-tel-input__control::placeholder {
173
- color: var(--tel-placeholder);
174
- }
175
-
176
- .ngxsmk-tel-input__control:hover {
177
- border-color: var(--tel-border-hover);
178
- }
179
-
180
- .ngxsmk-tel-input__control:focus {
181
- border-color: var(--tel-ring);
182
- box-shadow: var(--tel-focus-shadow);
183
- }
184
-
185
- [data-size="sm"] .ngxsmk-tel-input__control {
186
- height: 34px;
187
- font-size: 13px;
188
- padding: 6px 36px 6px 10px;
189
- border-radius: 10px;
190
- }
191
-
192
- [data-size="lg"] .ngxsmk-tel-input__control {
193
- height: 46px;
194
- font-size: 16px;
195
- padding: 12px 44px 12px 14px;
196
- border-radius: 14px;
197
- }
198
-
199
- [data-variant="filled"] .ngxsmk-tel-input__control {
200
- background: rgba(148, 163, 184, .08);
201
- }
202
-
203
- [data-variant="underline"] .ngxsmk-tel-input__control {
204
- border: 0;
205
- border-bottom: 2px solid var(--tel-border);
206
- border-radius: 0;
207
- padding-left: 0;
208
- padding-right: 34px;
209
- }
210
-
211
- [data-variant="underline"] .ngxsmk-tel-input__control:focus {
212
- border-bottom-color: var(--tel-ring);
213
- box-shadow: none;
214
- }
215
-
216
- /* ---------- intl-tel-input dropdown (deep selectors) ---------- */
217
- :host ::ng-deep .iti__flag-container {
218
- border-top-left-radius: var(--tel-radius);
219
- border-bottom-left-radius: var(--tel-radius);
220
- border: 1px solid var(--tel-border);
221
- border-right: none;
222
- background: var(--tel-bg);
223
- }
224
-
225
- :host ::ng-deep .iti__selected-flag {
226
- height: 100%;
227
- padding: 0 10px;
228
- display: inline-flex;
229
- align-items: center;
230
- }
231
-
232
- :host ::ng-deep .iti__country-list {
233
- background: var(--tel-dd-bg);
234
- border: 1px solid var(--tel-dd-border);
235
- border-radius: var(--tel-dd-radius);
236
- box-shadow: var(--tel-dd-shadow);
237
- max-height: min(50vh, 360px);
238
- overflow: auto;
239
- padding: 6px 0;
240
- width: max(280px, 100%);
241
- z-index: var(--tel-dd-z);
242
- }
243
-
244
- :host ::ng-deep .iti--container .iti__country-list {
245
- z-index: var(--tel-dd-z);
246
- }
247
-
248
- :host ::ng-deep .iti__search-input {
249
- position: sticky;
250
- top: 0;
251
- margin: 0;
252
- padding: 10px 12px;
253
- width: 100%;
254
- border: 0;
255
- border-bottom: 1px solid var(--tel-dd-border);
256
- outline: none;
257
- background: var(--tel-dd-search-bg);
258
- color: var(--tel-fg);
259
- }
260
-
261
- :host ::ng-deep .iti__country {
262
- display: grid;
263
- grid-template-columns: 28px 1fr auto;
264
- align-items: center;
265
- column-gap: .5rem;
266
- padding: 10px 12px;
267
- cursor: pointer;
268
- }
269
-
270
- :host ::ng-deep .iti__dial-code {
271
- color: var(--tel-placeholder);
272
- font-weight: 600;
273
- margin-left: 10px;
274
- }
275
-
276
- .ngxsmk-tel__clear {
277
- position: absolute;
278
- right: 8px;
279
- top: 50%;
280
- transform: translateY(-50%);
281
- border: 0;
282
- background: transparent;
283
- font-size: 18px;
284
- line-height: 1;
285
- width: 28px;
286
- height: 28px;
287
- border-radius: 50%;
288
- cursor: pointer;
289
- color: var(--tel-placeholder);
290
- }
291
-
292
- .ngxsmk-tel__clear:hover {
293
- background: rgba(148, 163, 184, .15);
294
- }
295
-
296
- .ngxsmk-tel__hint {
297
- margin-top: 6px;
298
- font-size: 12px;
299
- color: var(--tel-placeholder);
300
- }
301
-
302
- .ngxsmk-tel__error {
303
- margin-top: 6px;
304
- font-size: 12px;
305
- color: var(--tel-error);
306
- }
307
-
308
- .ngxsmk-tel__wrap.has-error .ngxsmk-tel-input__control {
309
- border-color: var(--tel-error);
310
- box-shadow: 0 0 0 3px rgba(239, 68, 68, .15);
311
- }
312
- `]
96
+ ]
313
97
  })
314
98
  export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor, Validator {
99
+
315
100
  @ViewChild('telInput', {static: true}) inputRef!: ElementRef<HTMLInputElement>;
316
101
 
317
102
  /* Core config */
318
103
  @Input() initialCountry: CountryCode | 'auto' = 'US';
319
104
  @Input() preferredCountries: CountryCode[] = ['US', 'GB'];
320
105
  @Input() onlyCountries?: CountryCode[];
321
- @Input() nationalMode = false;
322
- @Input() separateDialCode = false;
323
- @Input() allowDropdown = true;
106
+ @Input() nationalMode: boolean = false;
107
+ @Input() separateDialCode: boolean = false;
108
+ @Input() allowDropdown: boolean = true;
324
109
 
325
110
  /* UX */
326
- @Input() placeholder?: string; // keep undefined to let plugin set example placeholders (requires utilsScript)
111
+ @Input() placeholder?: string;
327
112
  @Input() autocomplete = 'tel';
328
113
  @Input() name?: string;
329
114
  @Input() inputId?: string;
330
- @Input() disabled = false;
115
+ @Input() disabled: boolean = false;
331
116
 
332
117
  @Input() label?: string;
333
118
  @Input() hint?: string;
334
119
  @Input() errorText?: string;
335
120
  @Input() size: 'sm' | 'md' | 'lg' = 'md';
336
121
  @Input() variant: 'outline' | 'filled' | 'underline' = 'outline';
337
- @Input() showClear = true;
338
- @Input() autoFocus = false;
339
- @Input() selectOnFocus = false;
340
- @Input() formatOnBlur = true;
341
- @Input() showErrorWhenTouched = true;
122
+ @Input() showClear: boolean = true;
123
+ @Input() autoFocus: boolean = false;
124
+ @Input() selectOnFocus: boolean = false;
125
+ @Input() formatOnBlur: boolean = true;
126
+ @Input() showErrorWhenTouched: boolean = true;
342
127
 
343
128
  /* Dropdown plumbing */
344
- @Input() dropdownAttachToBody = true;
345
- @Input() dropdownZIndex = 2000;
129
+ @Input() dropdownAttachToBody: boolean = true;
130
+ @Input() dropdownZIndex: number = 2000;
346
131
 
347
132
  /* Localization + RTL */
348
133
  @Input('i18n') i18n?: IntlTelI18n;
@@ -357,7 +142,7 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
357
142
  this.localizedCountries = v;
358
143
  }
359
144
 
360
- @Input() clearAriaLabel = 'Clear phone number';
145
+ @Input() clearAriaLabel: string = 'Clear phone number';
361
146
  @Input() dir: 'ltr' | 'rtl' = 'ltr';
362
147
 
363
148
  /* Placeholders (intl-tel-input) */
@@ -365,9 +150,11 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
365
150
  @Input() utilsScript?: string;
366
151
  @Input() customPlaceholder?: (example: string, country: any) => string;
367
152
 
153
+ @Input() formatWhenValid: 'off' | 'blur' | 'typing' = 'blur';
154
+
368
155
  /* Digits-only controls */
369
- @Input() digitsOnly = true;
370
- @Input() allowLeadingPlus = true;
156
+ @Input() digitsOnly: boolean = true;
157
+ @Input() allowLeadingPlus: boolean = true;
371
158
 
372
159
  /* Outputs */
373
160
  @Output() countryChange = new EventEmitter<{ iso2: CountryCode }>();
@@ -383,9 +170,9 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
383
170
  private validatorChange?: () => void;
384
171
  private lastEmittedValid = false;
385
172
  private pendingWrite: string | null = null;
386
- private touched = false;
173
+ private touched: boolean = false;
387
174
 
388
- readonly resolvedId = this.inputId || ('tel-' + Math.random().toString(36).slice(2));
175
+ readonly resolvedId: string = this.inputId || ('tel-' + Math.random().toString(36).slice(2));
389
176
 
390
177
  constructor(
391
178
  private readonly zone: NgZone,
@@ -394,8 +181,12 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
394
181
  ) {
395
182
  }
396
183
 
397
- async ngAfterViewInit() {
184
+ ngAfterViewInit(): void {
398
185
  if (!isPlatformBrowser(this.platformId)) return;
186
+ void this.initAndWire();
187
+ }
188
+
189
+ private async initAndWire(): Promise<void> {
399
190
  await this.initIntlTelInput();
400
191
  this.bindDomListeners();
401
192
 
@@ -503,8 +294,8 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
503
294
  separateDialCode: this.separateDialCode,
504
295
  geoIpLookup: (cb: (iso2: string) => void) => cb('us'),
505
296
 
506
- // placeholders — NOTE: no CDN fallback anymore
507
- autoPlaceholder: this.autoPlaceholder, // 'off' | 'polite' | 'aggressive'
297
+ // placeholders
298
+ autoPlaceholder: this.autoPlaceholder,
508
299
  utilsScript: this.utilsScript,
509
300
  customPlaceholder: this.customPlaceholder,
510
301
 
@@ -520,7 +311,6 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
520
311
  this.iti = intlTelInput(this.inputRef.nativeElement, config);
521
312
  });
522
313
 
523
- // z-index for dropdown
524
314
  (this.inputRef.nativeElement as HTMLElement).style.setProperty('--tel-dd-z', String(this.dropdownZIndex));
525
315
  }
526
316
 
@@ -551,8 +341,7 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
551
341
  // ----- Input filtering (digits-only) -----
552
342
  private sanitizeDigits(value: string): string {
553
343
  if (!this.digitsOnly) return value;
554
- let v = value.replace(/[^\d+]/g, ''); // keep digits and pluses
555
- // allow only ONE leading + (if enabled)
344
+ let v = value.replace(/[^\d+]/g, '');
556
345
  if (this.allowLeadingPlus) {
557
346
  const hasLeadingPlus = v.startsWith('+');
558
347
  v = (hasLeadingPlus ? '+' : '') + v.replace(/\+/g, '');
@@ -566,12 +355,9 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
566
355
  const el = this.inputRef.nativeElement;
567
356
 
568
357
  this.zone.runOutsideAngular(() => {
569
- // prevent invalid chars while typing
570
358
  el.addEventListener('beforeinput', (ev: InputEvent) => {
571
359
  if (!this.digitsOnly) return;
572
360
  const data = (ev as any).data as string | null;
573
-
574
- // allow deletions, cuts, etc.
575
361
  if (!data || ev.inputType !== 'insertText') return;
576
362
 
577
363
  const pos = el.selectionStart ?? 0;
@@ -581,7 +367,6 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
581
367
  if (!isDigit && !isPlusAtStart) ev.preventDefault();
582
368
  });
583
369
 
584
- // sanitize pastes
585
370
  el.addEventListener('paste', (e: ClipboardEvent) => {
586
371
  if (!this.digitsOnly) return;
587
372
  e.preventDefault();
@@ -593,7 +378,6 @@ export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDest
593
378
  queueMicrotask(() => this.handleInput());
594
379
  });
595
380
 
596
- // catch any remaining non-digit changes (e.g., programmatic)
597
381
  el.addEventListener('input', () => {
598
382
  if (this.digitsOnly) {
599
383
  const val = el.value;