ngxsmk-tel-input 1.1.5 → 1.1.8

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.5",
3
+ "version": "1.1.8",
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",
@@ -58,29 +58,37 @@
58
58
  "publishConfig": {
59
59
  "access": "public"
60
60
  },
61
+ "exports": {
62
+ "./*": null
63
+ },
61
64
  "files": [
62
65
  "bundles/",
63
66
  "fesm*",
64
67
  "esm*",
65
68
  "schematics/",
66
69
  "migrations/",
67
- "src/",
68
70
  "README.md",
69
71
  "LICENSE",
70
72
  "docs/"
71
73
  ],
72
74
  "peerDependencies": {
73
- "@angular/common": ">=17",
74
- "@angular/core": ">=17",
75
- "@angular/forms": ">=17",
76
- "rxjs": ">=7.8.0"
75
+ "@angular/common": ">=17 <21",
76
+ "@angular/core": ">=17 <21",
77
+ "@angular/forms": ">=17 <21",
78
+ "rxjs": ">=7.8.0",
79
+ "intl-tel-input": "^25.3.2",
80
+ "libphonenumber-js": "^1.12.10"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "intl-tel-input": { "optional": false },
84
+ "libphonenumber-js": { "optional": false }
77
85
  },
78
86
  "dependencies": {
79
- "intl-tel-input": "^25.3.2",
80
- "libphonenumber-js": "^1.12.10",
81
87
  "tslib": "^2.3.0"
82
88
  },
83
89
  "devDependencies": {
90
+ "intl-tel-input": "^25.3.2",
91
+ "libphonenumber-js": "^1.12.10",
84
92
  "@changesets/cli": "^2.29.5"
85
93
  }
86
94
  }
@@ -1,220 +0,0 @@
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
- }
@@ -1,24 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { PLATFORM_ID } from '@angular/core';
3
- import { NgxsmkTelInputComponent } from './ngxsmk-tel-input.component';
4
-
5
- describe('NgxsmkTelInputComponent', () => {
6
- let component: NgxsmkTelInputComponent;
7
- let fixture: ComponentFixture<NgxsmkTelInputComponent>;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [NgxsmkTelInputComponent],
12
- // Pretend we're on the server so ngAfterViewInit skips intl-tel-input init
13
- providers: [{ provide: PLATFORM_ID, useValue: 'server' }],
14
- }).compileComponents();
15
-
16
- fixture = TestBed.createComponent(NgxsmkTelInputComponent);
17
- component = fixture.componentInstance;
18
- fixture.detectChanges();
19
- });
20
-
21
- it('should create', () => {
22
- expect(component).toBeTruthy();
23
- });
24
- });
@@ -1,465 +0,0 @@
1
- import {
2
- AfterViewInit,
3
- Component,
4
- ElementRef,
5
- EventEmitter,
6
- forwardRef,
7
- Inject,
8
- Input,
9
- NgZone,
10
- OnChanges,
11
- OnDestroy,
12
- Output,
13
- PLATFORM_ID,
14
- SimpleChanges,
15
- ViewChild
16
- } from '@angular/core';
17
- import {isPlatformBrowser} from '@angular/common';
18
- import {
19
- AbstractControl,
20
- ControlValueAccessor,
21
- NG_VALIDATORS,
22
- NG_VALUE_ACCESSOR,
23
- ValidationErrors,
24
- Validator
25
- } from '@angular/forms';
26
- import type {CountryCode} from 'libphonenumber-js';
27
- import {NgxsmkTelInputService} from './ngxsmk-tel-input.service';
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
- }
39
-
40
- @Component({
41
- selector: 'ngxsmk-tel-input',
42
- standalone: true,
43
- imports: [],
44
- template: `
45
- <div class="ngxsmk-tel"
46
- [class.disabled]="disabled"
47
- [attr.data-size]="size"
48
- [attr.data-variant]="variant"
49
- [attr.dir]="dir">
50
- @if (label) {
51
- <label class="ngxsmk-tel__label" [for]="resolvedId">{{ label }}</label>
52
- }
53
-
54
- <div class="ngxsmk-tel__wrap" [class.has-error]="showError">
55
- <div class="ngxsmk-tel-input__wrapper">
56
- <input
57
- #telInput
58
- type="tel"
59
- class="ngxsmk-tel-input__control"
60
- [id]="resolvedId"
61
- [attr.name]="name || null"
62
- [attr.placeholder]="placeholder || null"
63
- [attr.autocomplete]="autocomplete"
64
- [attr.inputmode]="digitsOnly ? 'numeric' : 'tel'"
65
- [attr.pattern]="digitsOnly ? (allowLeadingPlus ? '\\\\+?[0-9]*' : '[0-9]*') : null"
66
- [disabled]="disabled"
67
- [attr.aria-invalid]="showError ? 'true' : 'false'"
68
- (blur)="onBlur()"
69
- (focus)="onFocus()"
70
- />
71
- </div>
72
-
73
- @if (showClear && currentRaw()) {
74
- <button type="button"
75
- class="ngxsmk-tel__clear"
76
- (click)="clearInput()"
77
- [attr.aria-label]="clearAriaLabel">
78
- ×
79
- </button>
80
- }
81
- </div>
82
-
83
- @if (hint && !showError) {
84
- <div class="ngxsmk-tel__hint">{{ hint }}</div>
85
- }
86
-
87
- @if (showError) {
88
- <div class="ngxsmk-tel__error">{{ errorText || 'Please enter a valid phone number.' }}</div>
89
- }
90
- </div>
91
- `,
92
- styleUrls: ['./ngxsmk-tel-input.component.scss'],
93
- providers: [
94
- {provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true},
95
- {provide: NG_VALIDATORS, useExisting: forwardRef(() => NgxsmkTelInputComponent), multi: true}
96
- ]
97
- })
98
- export class NgxsmkTelInputComponent implements AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor, Validator {
99
-
100
- @ViewChild('telInput', {static: true}) inputRef!: ElementRef<HTMLInputElement>;
101
-
102
- /* Core config */
103
- @Input() initialCountry: CountryCode | 'auto' = 'US';
104
- @Input() preferredCountries: CountryCode[] = ['US', 'GB'];
105
- @Input() onlyCountries?: CountryCode[];
106
- @Input() nationalMode: boolean = false;
107
- @Input() separateDialCode: boolean = false;
108
- @Input() allowDropdown: boolean = true;
109
-
110
- /* UX */
111
- @Input() placeholder?: string;
112
- @Input() autocomplete = 'tel';
113
- @Input() name?: string;
114
- @Input() inputId?: string;
115
- @Input() disabled: boolean = false;
116
-
117
- @Input() label?: string;
118
- @Input() hint?: string;
119
- @Input() errorText?: string;
120
- @Input() size: 'sm' | 'md' | 'lg' = 'md';
121
- @Input() variant: 'outline' | 'filled' | 'underline' = 'outline';
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;
127
-
128
- /* Dropdown plumbing */
129
- @Input() dropdownAttachToBody: boolean = true;
130
- @Input() dropdownZIndex: number = 2000;
131
-
132
- /* Localization + RTL */
133
- @Input('i18n') i18n?: IntlTelI18n;
134
-
135
- @Input('telI18n') set telI18n(v: IntlTelI18n | undefined) {
136
- this.i18n = v;
137
- }
138
-
139
- @Input('localizedCountries') localizedCountries?: CountryMap;
140
-
141
- @Input('telLocalizedCountries') set telLocalizedCountries(v: CountryMap | undefined) {
142
- this.localizedCountries = v;
143
- }
144
-
145
- @Input() clearAriaLabel: string = 'Clear phone number';
146
- @Input() dir: 'ltr' | 'rtl' = 'ltr';
147
-
148
- /* Placeholders (intl-tel-input) */
149
- @Input() autoPlaceholder: 'off' | 'polite' | 'aggressive' = 'off'; // default OFF since no utils fallback
150
- @Input() utilsScript?: string;
151
- @Input() customPlaceholder?: (example: string, country: any) => string;
152
-
153
- @Input() formatWhenValid: 'off' | 'blur' | 'typing' = 'blur';
154
-
155
- /* Digits-only controls */
156
- @Input() digitsOnly: boolean = true;
157
- @Input() allowLeadingPlus: boolean = true;
158
-
159
- /* Outputs */
160
- @Output() countryChange = new EventEmitter<{ iso2: CountryCode }>();
161
- @Output() validityChange = new EventEmitter<boolean>();
162
- @Output() inputChange = new EventEmitter<{ raw: string; e164: string | null; iso2: CountryCode }>();
163
-
164
- /* Internal */
165
- private iti: IntlTelInstance | null = null;
166
- private onChange: (val: string | null) => void = () => {
167
- };
168
- private onTouchedCb: () => void = () => {
169
- };
170
- private validatorChange?: () => void;
171
- private lastEmittedValid = false;
172
- private pendingWrite: string | null = null;
173
- private touched: boolean = false;
174
-
175
- readonly resolvedId: string = this.inputId || ('tel-' + Math.random().toString(36).slice(2));
176
-
177
- constructor(
178
- private readonly zone: NgZone,
179
- private readonly tel: NgxsmkTelInputService,
180
- @Inject(PLATFORM_ID) private readonly platformId: Object
181
- ) {
182
- }
183
-
184
- ngAfterViewInit(): void {
185
- if (!isPlatformBrowser(this.platformId)) return;
186
- void this.initAndWire();
187
- }
188
-
189
- private async initAndWire(): Promise<void> {
190
- await this.initIntlTelInput();
191
- this.bindDomListeners();
192
-
193
- if (this.pendingWrite !== null) {
194
- this.setInputValue(this.pendingWrite);
195
- this.handleInput();
196
- this.pendingWrite = null;
197
- }
198
- if (this.autoFocus) setTimeout(() => this.focus(), 0);
199
- }
200
-
201
- ngOnChanges(changes: SimpleChanges): void {
202
- if (!isPlatformBrowser(this.platformId)) return;
203
- const configChanged = [
204
- 'initialCountry', 'preferredCountries', 'onlyCountries',
205
- 'separateDialCode', 'allowDropdown', 'nationalMode',
206
- 'i18n', 'localizedCountries', 'dir',
207
- 'autoPlaceholder', 'utilsScript', 'customPlaceholder',
208
- 'digitsOnly', 'allowLeadingPlus'
209
- ].some(k => k in changes && !changes[k]?.firstChange);
210
- if (configChanged && this.iti) {
211
- this.reinitPlugin();
212
- this.validatorChange?.();
213
- }
214
- }
215
-
216
- ngOnDestroy(): void {
217
- this.destroyPlugin();
218
- }
219
-
220
- // ----- CVA -----
221
- writeValue(val: string | null): void {
222
- if (!this.inputRef) return;
223
- if (!this.iti) {
224
- this.pendingWrite = val ?? '';
225
- return;
226
- }
227
- this.setInputValue(val ?? '');
228
- }
229
-
230
- registerOnChange(fn: any): void {
231
- this.onChange = fn;
232
- }
233
-
234
- registerOnTouched(fn: any): void {
235
- this.onTouchedCb = fn;
236
- }
237
-
238
- setDisabledState(isDisabled: boolean): void {
239
- this.disabled = isDisabled;
240
- if (this.inputRef) this.inputRef.nativeElement.disabled = isDisabled;
241
- }
242
-
243
- // ----- Validator -----
244
- validate(_: AbstractControl): ValidationErrors | null {
245
- const raw = this.currentRaw();
246
- if (!raw) return null;
247
- const valid = this.tel.isValid(raw, this.currentIso2());
248
- if (valid !== this.lastEmittedValid) {
249
- this.lastEmittedValid = valid;
250
- this.validityChange.emit(valid);
251
- }
252
- return valid ? null : {phoneInvalid: true};
253
- }
254
-
255
- registerOnValidatorChange(fn: () => void): void {
256
- this.validatorChange = fn;
257
- }
258
-
259
- // ----- Public helpers -----
260
- focus(): void {
261
- this.inputRef?.nativeElement.focus();
262
- if (this.selectOnFocus) {
263
- const el = this.inputRef.nativeElement;
264
- queueMicrotask(() => el.setSelectionRange(0, el.value.length));
265
- }
266
- }
267
-
268
- selectCountry(iso2: CountryCode): void {
269
- if (this.iti) {
270
- this.iti.setCountry(iso2.toLowerCase());
271
- this.handleInput();
272
- }
273
- }
274
-
275
- clearInput() {
276
- this.setInputValue('');
277
- this.handleInput();
278
- this.inputRef.nativeElement.focus();
279
- }
280
-
281
- // ----- Plugin wiring -----
282
- private async initIntlTelInput() {
283
- const [{default: intlTelInput}] = await Promise.all([import('intl-tel-input')]);
284
-
285
- const toLowerKeys = (m?: CountryMap) => {
286
- if (!m) return undefined;
287
- const out: Record<string, string> = {};
288
- for (const k in m) {
289
- if (Object.prototype.hasOwnProperty.call(m, k)) {
290
- const v = (m as Record<string, string | undefined>)[k];
291
- if (v != null) out[k.toLowerCase()] = v;
292
- }
293
- }
294
- return out;
295
- };
296
-
297
- const config: any = {
298
- initialCountry: this.initialCountry === 'auto' ? 'auto' : (this.initialCountry?.toLowerCase() || 'us'),
299
- preferredCountries: (this.preferredCountries ?? []).map(c => c.toLowerCase()),
300
- onlyCountries: (this.onlyCountries ?? []).map(c => c.toLowerCase()),
301
- nationalMode: this.nationalMode,
302
- allowDropdown: this.allowDropdown,
303
- separateDialCode: this.separateDialCode,
304
- geoIpLookup: (cb: (iso2: string) => void) => cb('us'),
305
-
306
- // placeholders
307
- autoPlaceholder: this.autoPlaceholder,
308
- utilsScript: this.utilsScript,
309
- customPlaceholder: this.customPlaceholder,
310
-
311
- // localization
312
- i18n: this.i18n,
313
- localizedCountries: toLowerKeys(this.localizedCountries),
314
-
315
- // dropdown container
316
- dropdownContainer: this.dropdownAttachToBody && typeof document !== 'undefined' ? document.body : undefined
317
- };
318
-
319
- this.zone.runOutsideAngular(() => {
320
- this.iti = intlTelInput(this.inputRef.nativeElement, config);
321
- });
322
-
323
- (this.inputRef.nativeElement as HTMLElement).style.setProperty('--tel-dd-z', String(this.dropdownZIndex));
324
- }
325
-
326
- private reinitPlugin() {
327
- const current = this.currentRaw();
328
- this.destroyPlugin();
329
- this.initIntlTelInput().then(() => {
330
- if (current) {
331
- this.setInputValue(current);
332
- this.handleInput();
333
- }
334
- });
335
- }
336
-
337
- private destroyPlugin() {
338
- if (this.iti) {
339
- this.iti.destroy();
340
- this.iti = null;
341
- }
342
- if (this.inputRef?.nativeElement) {
343
- const el = this.inputRef.nativeElement;
344
- const clone = el.cloneNode(true) as HTMLInputElement;
345
- el.parentNode?.replaceChild(clone, el);
346
- (this.inputRef as any).nativeElement = clone;
347
- }
348
- }
349
-
350
- // ----- Input filtering (digits-only) -----
351
- private sanitizeDigits(value: string): string {
352
- if (!this.digitsOnly) return value;
353
- let v = value.replace(/[^\d+]/g, '');
354
- if (this.allowLeadingPlus) {
355
- const hasLeadingPlus = v.startsWith('+');
356
- v = (hasLeadingPlus ? '+' : '') + v.replace(/\+/g, '');
357
- } else {
358
- v = v.replace(/\+/g, '');
359
- }
360
- return v;
361
- }
362
-
363
- private bindDomListeners() {
364
- const el = this.inputRef.nativeElement;
365
-
366
- this.zone.runOutsideAngular(() => {
367
- el.addEventListener('beforeinput', (ev: InputEvent) => {
368
- if (!this.digitsOnly) return;
369
- const data = (ev as any).data as string | null;
370
- if (!data || ev.inputType !== 'insertText') return;
371
-
372
- const pos = el.selectionStart ?? 0;
373
- const isDigit = data >= '0' && data <= '9';
374
- const isPlusAtStart = this.allowLeadingPlus && data === '+' && pos === 0 && !el.value.includes('+');
375
-
376
- if (!isDigit && !isPlusAtStart) ev.preventDefault();
377
- });
378
-
379
- el.addEventListener('paste', (e: ClipboardEvent) => {
380
- if (!this.digitsOnly) return;
381
- e.preventDefault();
382
- const text = (e.clipboardData || (window as any).clipboardData).getData('text');
383
- const sanitized = this.sanitizeDigits(text);
384
- const start = el.selectionStart ?? el.value.length;
385
- const end = el.selectionEnd ?? el.value.length;
386
- el.setRangeText(sanitized, start, end, 'end');
387
- queueMicrotask(() => this.handleInput());
388
- });
389
-
390
- el.addEventListener('input', () => {
391
- if (this.digitsOnly) {
392
- const val = el.value;
393
- const sanitized = this.sanitizeDigits(val);
394
- if (val !== sanitized) {
395
- const caret = el.selectionStart ?? sanitized.length;
396
- el.value = sanitized;
397
- el.setSelectionRange(caret, caret);
398
- }
399
- }
400
- this.handleInput();
401
- });
402
-
403
- el.addEventListener('countrychange', () => {
404
- const iso2 = this.currentIso2();
405
- this.zone.run(() => {
406
- this.countryChange.emit({iso2});
407
- this.validatorChange?.();
408
- });
409
- this.handleInput();
410
- });
411
-
412
- el.addEventListener('blur', () => this.onBlur());
413
- });
414
- }
415
-
416
- onBlur() {
417
- this.touched = true;
418
- this.zone.run(() => this.onTouchedCb());
419
- if (!this.formatOnBlur) return;
420
- const raw = this.currentRaw();
421
- if (!raw) return;
422
- const parsed = this.tel.parse(raw, this.currentIso2());
423
- if (this.nationalMode && parsed.national) {
424
- this.setInputValue((parsed.national || '').replace(/\s{2,}/g, ' '));
425
- }
426
- }
427
-
428
- onFocus() {
429
- if (this.selectOnFocus) {
430
- const el = this.inputRef.nativeElement;
431
- queueMicrotask(() => el.setSelectionRange(0, el.value.length));
432
- }
433
- }
434
-
435
- private handleInput() {
436
- const raw = this.currentRaw();
437
- const iso2 = this.currentIso2();
438
- const parsed = this.tel.parse(raw, iso2);
439
- this.zone.run(() => this.onChange(parsed.e164)); // E.164 or null
440
- this.zone.run(() => this.inputChange.emit({raw, e164: parsed.e164, iso2}));
441
- if (raw && this.nationalMode && parsed.national) {
442
- const normalized = parsed.national.replace(/\s{2,}/g, ' ');
443
- if (normalized !== raw) this.setInputValue(normalized);
444
- }
445
- }
446
-
447
- currentRaw(): string {
448
- return (this.inputRef?.nativeElement.value ?? '').trim();
449
- }
450
-
451
- private currentIso2(): CountryCode {
452
- const iso2 = (this.iti?.getSelectedCountryData?.().iso2 ?? this.initialCountry ?? 'US')
453
- .toString().toUpperCase();
454
- return iso2 as CountryCode;
455
- }
456
-
457
- private setInputValue(v: string) {
458
- this.inputRef.nativeElement.value = v ?? '';
459
- }
460
-
461
- get showError(): boolean {
462
- const invalid = !!this.validate({} as AbstractControl);
463
- return this.showErrorWhenTouched ? (this.touched && invalid) : invalid;
464
- }
465
- }
@@ -1,15 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
- import { NgxsmkTelInputService } from './ngxsmk-tel-input.service';
3
-
4
- describe('NgxsmkTelInputService', () => {
5
- let service: NgxsmkTelInputService;
6
-
7
- beforeEach(() => {
8
- TestBed.configureTestingModule({});
9
- service = TestBed.inject(NgxsmkTelInputService);
10
- });
11
-
12
- it('should be created', () => {
13
- expect(service).toBeTruthy();
14
- });
15
- });
@@ -1,17 +0,0 @@
1
- import { Injectable } from '@angular/core';
2
- import { parsePhoneNumberFromString, type CountryCode } from 'libphonenumber-js';
3
-
4
- @Injectable({ providedIn: 'root' })
5
- export class NgxsmkTelInputService {
6
- parse(input: string, iso2: CountryCode): { e164: string | null; national: string | null; isValid: boolean } {
7
- const phone = parsePhoneNumberFromString(input || '', iso2);
8
- if (!phone) return { e164: null, national: null, isValid: false };
9
- const isValid = phone.isValid();
10
- return { e164: isValid ? phone.number : null, national: phone.formatNational(), isValid };
11
- }
12
-
13
- isValid(input: string, iso2: CountryCode): boolean {
14
- const phone = parsePhoneNumberFromString(input || '', iso2);
15
- return !!phone && phone.isValid();
16
- }
17
- }
package/src/lib/types.ts DELETED
@@ -1,11 +0,0 @@
1
- import type { CountryCode } from 'libphonenumber-js';
2
-
3
- export type CountryMap = Partial<Record<CountryCode, string>>;
4
-
5
- export interface IntlTelI18n {
6
- selectedCountryAriaLabel?: string;
7
- countryListAriaLabel?: string;
8
- searchPlaceholder?: string;
9
- zeroSearchResults?: string;
10
- noCountrySelected?: string;
11
- }
package/src/public-api.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './lib/ngxsmk-tel-input.component';
2
- export * from './lib/ngxsmk-tel-input.service';
3
- export type { IntlTelI18n, CountryMap } from './lib/types';