@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.
Files changed (40) hide show
  1. package/README.md +585 -72
  2. package/dist/_chunks/types.d.ts +661 -0
  3. package/dist/_chunks/types.js +52 -0
  4. package/dist/_chunks/types.js.map +1 -0
  5. package/dist/_chunks/usePhoneValidation.js +539 -0
  6. package/dist/_chunks/usePhoneValidation.js.map +1 -0
  7. package/dist/index.cjs +13859 -1240
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.cts +120 -585
  10. package/dist/index.d.ts +120 -585
  11. package/dist/index.js +13752 -1113
  12. package/dist/index.js.map +1 -1
  13. package/dist/styles.css +3 -2
  14. package/dist/vee-validate/index.cjs +113 -0
  15. package/dist/vee-validate/index.cjs.map +1 -0
  16. package/dist/vee-validate/index.d.cts +86 -0
  17. package/dist/vee-validate/index.d.ts +86 -0
  18. package/dist/vee-validate/index.js +112 -0
  19. package/dist/vee-validate/index.js.map +1 -0
  20. package/dist/zod/index.cjs +211 -0
  21. package/dist/zod/index.cjs.map +1 -0
  22. package/dist/zod/index.d.cts +65 -0
  23. package/dist/zod/index.d.ts +65 -0
  24. package/dist/zod/index.js +208 -0
  25. package/dist/zod/index.js.map +1 -0
  26. package/package.json +41 -6
  27. package/src/components/ACountrySelect.vue +79 -1
  28. package/src/components/ATelInput.vue +206 -66
  29. package/src/composables/useCountryDetection.ts +28 -11
  30. package/src/composables/useCountryMatching.ts +160 -20
  31. package/src/composables/useCountrySelection.ts +71 -0
  32. package/src/composables/usePhoneValidation.ts +81 -18
  33. package/src/composables/useSyncedModel.ts +80 -0
  34. package/src/composables/useTelInputValidation.ts +50 -11
  35. package/src/index.ts +2 -0
  36. package/src/types.ts +80 -0
  37. package/src/vee-validate/index.ts +2 -0
  38. package/src/vee-validate/useTelField.ts +202 -0
  39. package/src/zod/index.ts +259 -0
  40. package/web-types.json +44 -1
@@ -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.1",
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
  }