@alikhalilll/a-tel-input 1.0.1 → 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 +585 -72
- package/dist/_chunks/types.d.ts +661 -0
- package/dist/_chunks/types.js +52 -0
- package/dist/_chunks/types.js.map +1 -0
- package/dist/_chunks/usePhoneValidation.js +539 -0
- package/dist/_chunks/usePhoneValidation.js.map +1 -0
- package/dist/index.cjs +13859 -1240
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -585
- package/dist/index.d.ts +120 -585
- package/dist/index.js +13752 -1113
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3 -2
- package/dist/vee-validate/index.cjs +113 -0
- package/dist/vee-validate/index.cjs.map +1 -0
- package/dist/vee-validate/index.d.cts +86 -0
- package/dist/vee-validate/index.d.ts +86 -0
- package/dist/vee-validate/index.js +112 -0
- package/dist/vee-validate/index.js.map +1 -0
- package/dist/zod/index.cjs +211 -0
- package/dist/zod/index.cjs.map +1 -0
- package/dist/zod/index.d.cts +65 -0
- package/dist/zod/index.d.ts +65 -0
- package/dist/zod/index.js +208 -0
- package/dist/zod/index.js.map +1 -0
- package/package.json +41 -6
- package/src/components/ACountrySelect.vue +79 -1
- package/src/components/ATelInput.vue +206 -66
- package/src/composables/useCountryDetection.ts +28 -11
- package/src/composables/useCountryMatching.ts +160 -20
- package/src/composables/useCountrySelection.ts +71 -0
- package/src/composables/usePhoneValidation.ts +81 -18
- package/src/composables/useSyncedModel.ts +80 -0
- package/src/composables/useTelInputValidation.ts +50 -11
- package/src/index.ts +2 -0
- package/src/types.ts +80 -0
- package/src/vee-validate/index.ts +2 -0
- package/src/vee-validate/useTelField.ts +202 -0
- package/src/zod/index.ts +259 -0
- package/web-types.json +44 -1
package/src/zod/index.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod adapter for `@alikhalilll/a-tel-input`.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the same `usePhoneValidation()` engine the component uses, so a Zod schema
|
|
5
|
+
* never disagrees with the visual validation state in the field.
|
|
6
|
+
*
|
|
7
|
+
* Three usage shapes, all backed by libphonenumber-js:
|
|
8
|
+
*
|
|
9
|
+
* 1. **E.164 string** (default) — validate a full international number:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { zPhone } from '@alikhalilll/a-tel-input/zod';
|
|
13
|
+
* const schema = z.object({ phone: zPhone() });
|
|
14
|
+
* schema.parse({ phone: '+201234567890' });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* 2. **National number for a fixed country** — when you already know the country (e.g.,
|
|
18
|
+
* a Saudi-only form) and only need to validate the digits the user typed:
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const schema = z.object({ phone: zPhone({ country: 'SA' }) });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* 3. **Combined object** — matches ATelInput's two v-models out of the box:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { zPhoneObject } from '@alikhalilll/a-tel-input/zod';
|
|
28
|
+
* const schema = z.object({
|
|
29
|
+
* contact: zPhoneObject(), // -> { phone: string, country: number | null }
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* All three honour `allowedDialCodes` and produce a `PhoneValidationReason` localised
|
|
34
|
+
* through the same `DEFAULT_ERROR_MESSAGES` map as the component, so error wording is
|
|
35
|
+
* consistent across UI + schema.
|
|
36
|
+
*
|
|
37
|
+
* `zod` is an **optional peer dependency** — install it yourself in your app.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { z } from 'zod';
|
|
41
|
+
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
|
42
|
+
import {
|
|
43
|
+
usePhoneValidation,
|
|
44
|
+
type PhoneValidationReason,
|
|
45
|
+
type PhoneValidationResult,
|
|
46
|
+
} from '../composables/usePhoneValidation';
|
|
47
|
+
import { DEFAULT_ERROR_MESSAGES } from '../types';
|
|
48
|
+
|
|
49
|
+
export interface ZPhoneOptions {
|
|
50
|
+
/**
|
|
51
|
+
* ISO 3166-1 alpha-2 country code (`'EG'`, `'SA'`, …). When set, the input is treated
|
|
52
|
+
* as a national number for that country. Leave undefined to validate a full E.164
|
|
53
|
+
* string (the country is inferred from the leading `+` dial code).
|
|
54
|
+
*/
|
|
55
|
+
country?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Whitelist of allowed dial-digit codes (no `+`), e.g. `['20', '966']`. Numbers from
|
|
58
|
+
* countries outside this list fail with `country_not_supported`. Matches the
|
|
59
|
+
* `allowedDialCodes` prop on `ATelInput`.
|
|
60
|
+
*/
|
|
61
|
+
allowedDialCodes?: string[];
|
|
62
|
+
/**
|
|
63
|
+
* BCP-47 locale, forwarded to the validator. Doesn't change the validity outcome —
|
|
64
|
+
* only affects locale-dependent fields on the underlying validation result.
|
|
65
|
+
*/
|
|
66
|
+
locale?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Override the error messages used when the Zod issue is raised. Keyed by validation
|
|
69
|
+
* reason. Falls back to the same English defaults the component uses.
|
|
70
|
+
*/
|
|
71
|
+
messages?: Partial<Record<PhoneValidationReason, string>>;
|
|
72
|
+
/**
|
|
73
|
+
* Custom message for the "empty input" case. By default the schema accepts an empty
|
|
74
|
+
* string — wrap with `z.string().min(1)` upstream if you want "required" semantics,
|
|
75
|
+
* or pass a non-empty `requiredMessage` to enforce it here.
|
|
76
|
+
*/
|
|
77
|
+
requiredMessage?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function messageFor(reason: PhoneValidationReason, overrides?: ZPhoneOptions['messages']) {
|
|
81
|
+
return overrides?.[reason] ?? DEFAULT_ERROR_MESSAGES[reason];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function failsAllowList(dialDigits: string, allowed?: string[]) {
|
|
85
|
+
if (!allowed || allowed.length === 0) return false;
|
|
86
|
+
return !allowed.includes(dialDigits);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Run the same validate() the component uses, but resolve country from an E.164 string. */
|
|
90
|
+
function validateE164(
|
|
91
|
+
value: string,
|
|
92
|
+
v: ReturnType<typeof usePhoneValidation>,
|
|
93
|
+
locale?: string
|
|
94
|
+
): PhoneValidationResult {
|
|
95
|
+
const trimmed = String(value ?? '').trim();
|
|
96
|
+
if (!trimmed) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: 'invalid_phone',
|
|
100
|
+
country: null,
|
|
101
|
+
phone: { raw: value, digits: '' },
|
|
102
|
+
full_phone: null,
|
|
103
|
+
required: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// libphonenumber wants a leading `+` to skip the country-hint requirement.
|
|
107
|
+
const e164 = trimmed.startsWith('+') ? trimmed : `+${trimmed}`;
|
|
108
|
+
const parsed = parsePhoneNumberFromString(e164);
|
|
109
|
+
if (!parsed || !parsed.country) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
reason: 'parse_failed',
|
|
113
|
+
country: null,
|
|
114
|
+
phone: { raw: value, digits: '' },
|
|
115
|
+
full_phone: null,
|
|
116
|
+
required: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return v.validate({
|
|
120
|
+
country: { iso2: parsed.country },
|
|
121
|
+
phone: parsed.nationalNumber,
|
|
122
|
+
locale,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Zod schema for a phone string.
|
|
128
|
+
*
|
|
129
|
+
* @see {@link ZPhoneOptions} for behaviour modes.
|
|
130
|
+
*/
|
|
131
|
+
export function zPhone(options: ZPhoneOptions = {}): z.ZodType<string, z.ZodTypeDef, string> {
|
|
132
|
+
// Lazy: only construct the validator on first parse, so apps that bundle this don't
|
|
133
|
+
// pay the libphonenumber metadata load until they actually validate.
|
|
134
|
+
let _v: ReturnType<typeof usePhoneValidation> | null = null;
|
|
135
|
+
const getV = () => {
|
|
136
|
+
if (!_v) {
|
|
137
|
+
_v = usePhoneValidation();
|
|
138
|
+
void _v.getCountries();
|
|
139
|
+
}
|
|
140
|
+
return _v;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return z.string().superRefine((value, ctx) => {
|
|
144
|
+
const v = getV();
|
|
145
|
+
const isEmpty = !value || !String(value).trim();
|
|
146
|
+
|
|
147
|
+
if (isEmpty) {
|
|
148
|
+
if (options.requiredMessage) {
|
|
149
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: options.requiredMessage });
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result: PhoneValidationResult = options.country
|
|
155
|
+
? v.validate({ country: { iso2: options.country }, phone: value, locale: options.locale })
|
|
156
|
+
: validateE164(value, v, options.locale);
|
|
157
|
+
|
|
158
|
+
if (!result.ok) {
|
|
159
|
+
const reason = result.reason ?? 'invalid_phone';
|
|
160
|
+
ctx.addIssue({
|
|
161
|
+
code: z.ZodIssueCode.custom,
|
|
162
|
+
message: messageFor(reason, options.messages),
|
|
163
|
+
params: { reason, validation: result },
|
|
164
|
+
});
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const dialDigits = result.country?.dial_code?.replace(/^\+/, '') ?? '';
|
|
169
|
+
if (failsAllowList(dialDigits, options.allowedDialCodes)) {
|
|
170
|
+
ctx.addIssue({
|
|
171
|
+
code: z.ZodIssueCode.custom,
|
|
172
|
+
message: messageFor('country_not_supported', options.messages),
|
|
173
|
+
params: { reason: 'country_not_supported' as PhoneValidationReason, validation: result },
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Zod schema for the `{ phone, country }` object shape matching `ATelInput`'s two
|
|
181
|
+
* v-models. `phone` is the digits-only national number; `country` is the dial-digit
|
|
182
|
+
* **number** (e.g. `20` for Egypt). NANP (`+1`) maps to `'US'` for validation purposes,
|
|
183
|
+
* which is correct for `isValidPhoneNumber` (all NANP countries share the same rule set).
|
|
184
|
+
*/
|
|
185
|
+
export function zPhoneObject(options: ZPhoneOptions = {}) {
|
|
186
|
+
let _v: ReturnType<typeof usePhoneValidation> | null = null;
|
|
187
|
+
const getV = () => {
|
|
188
|
+
if (!_v) {
|
|
189
|
+
_v = usePhoneValidation();
|
|
190
|
+
void _v.getCountries();
|
|
191
|
+
}
|
|
192
|
+
return _v;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return z
|
|
196
|
+
.object({
|
|
197
|
+
phone: z.string(),
|
|
198
|
+
country: z.number().nullable(),
|
|
199
|
+
})
|
|
200
|
+
.superRefine((input, ctx) => {
|
|
201
|
+
const v = getV();
|
|
202
|
+
const phone = (input.phone ?? '').trim();
|
|
203
|
+
const country = input.country;
|
|
204
|
+
|
|
205
|
+
if (!phone) {
|
|
206
|
+
if (options.requiredMessage) {
|
|
207
|
+
ctx.addIssue({
|
|
208
|
+
code: z.ZodIssueCode.custom,
|
|
209
|
+
path: ['phone'],
|
|
210
|
+
message: options.requiredMessage,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (country == null) {
|
|
217
|
+
ctx.addIssue({
|
|
218
|
+
code: z.ZodIssueCode.custom,
|
|
219
|
+
path: ['country'],
|
|
220
|
+
message: messageFor('missing_country', options.messages),
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const matches = v.getCountriesByDial(String(country));
|
|
226
|
+
const iso2 = matches[0]?.value ?? options.country;
|
|
227
|
+
if (!iso2) {
|
|
228
|
+
ctx.addIssue({
|
|
229
|
+
code: z.ZodIssueCode.custom,
|
|
230
|
+
path: ['country'],
|
|
231
|
+
message: messageFor('country_not_supported', options.messages),
|
|
232
|
+
});
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const result = v.validate({ country: { iso2 }, phone, locale: options.locale });
|
|
237
|
+
if (!result.ok) {
|
|
238
|
+
const reason = result.reason ?? 'invalid_phone';
|
|
239
|
+
ctx.addIssue({
|
|
240
|
+
code: z.ZodIssueCode.custom,
|
|
241
|
+
path: ['phone'],
|
|
242
|
+
message: messageFor(reason, options.messages),
|
|
243
|
+
params: { reason, validation: result },
|
|
244
|
+
});
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (failsAllowList(String(country), options.allowedDialCodes)) {
|
|
249
|
+
ctx.addIssue({
|
|
250
|
+
code: z.ZodIssueCode.custom,
|
|
251
|
+
path: ['country'],
|
|
252
|
+
message: messageFor('country_not_supported', options.messages),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export { DEFAULT_ERROR_MESSAGES };
|
|
259
|
+
export type { PhoneValidationReason, PhoneValidationResult };
|
package/web-types.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/web-types",
|
|
3
3
|
"name": "@alikhalilll/a-tel-input",
|
|
4
|
-
"version": "1.0
|
|
4
|
+
"version": "1.1.0",
|
|
5
5
|
"js-types-syntax": "typescript",
|
|
6
6
|
"description-markup": "markdown",
|
|
7
7
|
"framework": "vue",
|
|
@@ -97,6 +97,12 @@
|
|
|
97
97
|
"type": "string",
|
|
98
98
|
"required": false
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
"name": "error",
|
|
102
|
+
"type": "string | null",
|
|
103
|
+
"required": false,
|
|
104
|
+
"description": "Externally controlled error message. When set to a non-empty string the component\nis forced into the error visual state and renders this message via the `#error`\nslot — overriding internal libphonenumber validation. Wire this from VeeValidate,\nZod, an async server check (\"this phone is already registered\"), or any custom\nvalidation layer.\n\nPass `null` / `undefined` / `''` to defer to internal validation."
|
|
105
|
+
},
|
|
100
106
|
{
|
|
101
107
|
"name": "errorClass",
|
|
102
108
|
"type": "string",
|
|
@@ -161,6 +167,18 @@
|
|
|
161
167
|
"required": false,
|
|
162
168
|
"description": "Localized UI strings. A single bag covering the picker, validation errors, and a11y\nlabels. Individual props (`searchPlaceholder`, `emptyText`, `loadingText`,\n`errorMessages`) take precedence over the matching `messages` key when both are set."
|
|
163
169
|
},
|
|
170
|
+
{
|
|
171
|
+
"name": "modelValue",
|
|
172
|
+
"type": "string",
|
|
173
|
+
"required": false,
|
|
174
|
+
"description": "Default `v-model` — the canonical **E.164** string (e.g. `'+201066105963'`).\n\nReads + writes the full international number as a single value. Designed to drop\nstraight into VeeValidate's `<Field v-slot=\"{ field }\">` pattern (use\n`v-bind=\"field\"`), native `<form>` submission, or any `v-model=\"phoneE164\"` consumer.\n\nStays in sync with the split `v-model:phone` + `v-model:country` contract — use\neither, or both."
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "name",
|
|
178
|
+
"type": "string",
|
|
179
|
+
"required": false,
|
|
180
|
+
"description": "Forwarded to the inner `<input name=\"\">`. Set this when participating in a native\n`<form>` submission, or when a form library (VeeValidate, etc.) wants a stable name."
|
|
181
|
+
},
|
|
164
182
|
{
|
|
165
183
|
"name": "placeholder",
|
|
166
184
|
"type": "string",
|
|
@@ -206,6 +224,18 @@
|
|
|
206
224
|
"name": "size",
|
|
207
225
|
"type": "Size",
|
|
208
226
|
"required": false
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
"name": "validateOn",
|
|
230
|
+
"type": "ATelInputValidateOn",
|
|
231
|
+
"required": false,
|
|
232
|
+
"description": "When to surface validation in the UI.\n- `'change'` (default) — visible state mirrors the typing-paused state.\n- `'blur'` — stays idle until the input has been blurred once (form-library friendly).\n- `'eager'` — mirror raw validation immediately, no typing pause."
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"name": "validating",
|
|
236
|
+
"type": "boolean",
|
|
237
|
+
"required": false,
|
|
238
|
+
"description": "`true` while an async validation is in flight (e.g. a server-side uniqueness\ncheck). Renders a small spinner inside the field and sets `aria-busy=\"true\"` on\nthe input. Does **not** disable the field — use `loading` for that. Replace the\nspinner via the `#validating` slot.\n\nDesigned to be bound to the `validating` ref returned by `useTelField()`."
|
|
209
239
|
}
|
|
210
240
|
],
|
|
211
241
|
"slots": [
|
|
@@ -266,13 +296,26 @@
|
|
|
266
296
|
{
|
|
267
297
|
"name": "valid-icon",
|
|
268
298
|
"description": "Replace the green check shown when the number validates."
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
"name": "validating",
|
|
302
|
+
"description": "Replace the spinner shown inside the field while async validation is in flight."
|
|
269
303
|
}
|
|
270
304
|
],
|
|
271
305
|
"js": {
|
|
272
306
|
"events": [
|
|
307
|
+
{
|
|
308
|
+
"name": "blur"
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "focus"
|
|
312
|
+
},
|
|
273
313
|
{
|
|
274
314
|
"name": "update:country"
|
|
275
315
|
},
|
|
316
|
+
{
|
|
317
|
+
"name": "update:modelValue"
|
|
318
|
+
},
|
|
276
319
|
{
|
|
277
320
|
"name": "update:phone"
|
|
278
321
|
}
|