@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
|
@@ -15,13 +15,15 @@ import type { TelInputMessages } from '../types';
|
|
|
15
15
|
* component needs:
|
|
16
16
|
*
|
|
17
17
|
* - `validation` / `validationState` — the raw + simplified state of the current input.
|
|
18
|
-
* - `visibleValidationState` — `validationState` gated by
|
|
19
|
-
* from {@link useTypingPhase}, so error tints / icons /
|
|
20
|
-
*
|
|
18
|
+
* - `visibleValidationState` — `validationState` gated by `validateOn` + the
|
|
19
|
+
* `hasFinishedTyping` flag from {@link useTypingPhase}, so error tints / icons /
|
|
20
|
+
* messages only appear at the right moment (after typing pause, after blur, or eagerly).
|
|
21
|
+
* This is the value the template should bind to.
|
|
21
22
|
* - `errorMessage` — localised error string for the current `validation.reason`, or
|
|
22
|
-
* `null` when the input is empty
|
|
23
|
+
* `null` when the input is empty / valid. When an external `error` is supplied
|
|
24
|
+
* (e.g. from VeeValidate), it wins.
|
|
23
25
|
* - `showError` / `showHint` — boolean computed properties for conditional rendering
|
|
24
|
-
* in the template; both already respect `showValidation` and the
|
|
26
|
+
* in the template; both already respect `showValidation` and the visible-state gate.
|
|
25
27
|
* - `selectedDialCode` — the human-readable dial prefix (`+20`, `+1`, …) for the
|
|
26
28
|
* selected country, used as an in-input prefix.
|
|
27
29
|
*
|
|
@@ -43,6 +45,17 @@ export interface UseTelInputValidationDeps {
|
|
|
43
45
|
getCountryByValue: UsePhoneValidationReturn['getCountryByValue'];
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
/** When to surface validation in the UI.
|
|
49
|
+
* - `'change'` (default) — visible state mirrors the typing-paused state. Errors light up
|
|
50
|
+
* a beat after the user stops typing. Best for inline forms.
|
|
51
|
+
* - `'blur'` — visible state stays `'idle'` until the input has been blurred at least
|
|
52
|
+
* once, then mirrors typing-paused state. Best for VeeValidate / form-library flows
|
|
53
|
+
* that validate on blur.
|
|
54
|
+
* - `'eager'` — visible state mirrors raw `validationState` immediately on every keystroke,
|
|
55
|
+
* no typing pause. Use sparingly; can feel aggressive.
|
|
56
|
+
*/
|
|
57
|
+
export type ATelInputValidateOn = 'change' | 'blur' | 'eager';
|
|
58
|
+
|
|
46
59
|
export interface UseTelInputValidationInputs {
|
|
47
60
|
/** Digits-only national number model. */
|
|
48
61
|
phone: Ref<string>;
|
|
@@ -50,6 +63,8 @@ export interface UseTelInputValidationInputs {
|
|
|
50
63
|
selectedIso2: Ref<string>;
|
|
51
64
|
/** From {@link useTypingPhase} — gates visible state during the debounce window. */
|
|
52
65
|
hasFinishedTyping: Readonly<Ref<boolean>>;
|
|
66
|
+
/** Whether the input has been blurred at least once. Drives `validateOn: 'blur'`. */
|
|
67
|
+
hasBlurred: Readonly<Ref<boolean>>;
|
|
53
68
|
/** Resolved i18n messages (merged defaults + consumer overrides). */
|
|
54
69
|
messages: ComputedRef<TelInputMessages>;
|
|
55
70
|
}
|
|
@@ -61,6 +76,15 @@ export interface UseTelInputValidationConfig {
|
|
|
61
76
|
showValidation: () => boolean | undefined;
|
|
62
77
|
/** Per-reason error string overrides. From props. */
|
|
63
78
|
errorMessages: () => Partial<Record<PhoneValidationReason, string>> | undefined;
|
|
79
|
+
/** When to surface validation in the UI. Defaults to `'change'`. */
|
|
80
|
+
validateOn: () => ATelInputValidateOn | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* Externally controlled error (from VeeValidate / Zod / a custom form layer). When set
|
|
83
|
+
* to a non-empty string, the component is forced into `'error'` state and surfaces this
|
|
84
|
+
* message regardless of internal validation. `null` / `undefined` / `''` defers to the
|
|
85
|
+
* internal validator.
|
|
86
|
+
*/
|
|
87
|
+
externalError: () => string | null | undefined;
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
export interface UseTelInputValidationReturn {
|
|
@@ -93,25 +117,40 @@ export function useTelInputValidation(
|
|
|
93
117
|
})
|
|
94
118
|
);
|
|
95
119
|
|
|
120
|
+
const externalErrorActive = computed<boolean>(() => {
|
|
121
|
+
const e = config.externalError();
|
|
122
|
+
return typeof e === 'string' && e.length > 0;
|
|
123
|
+
});
|
|
124
|
+
|
|
96
125
|
const validationState = computed<'idle' | 'valid' | 'error'>(() => {
|
|
126
|
+
if (externalErrorActive.value) return 'error';
|
|
97
127
|
if (!inputs.phone.value) return 'idle';
|
|
98
128
|
return validation.value.ok ? 'valid' : 'error';
|
|
99
129
|
});
|
|
100
130
|
|
|
101
|
-
const visibleValidationState = computed<'idle' | 'valid' | 'error'>(() =>
|
|
102
|
-
|
|
103
|
-
|
|
131
|
+
const visibleValidationState = computed<'idle' | 'valid' | 'error'>(() => {
|
|
132
|
+
if (externalErrorActive.value) return 'error';
|
|
133
|
+
const mode = config.validateOn() ?? 'change';
|
|
134
|
+
if (mode === 'eager') return validationState.value;
|
|
135
|
+
if (mode === 'blur' && !inputs.hasBlurred.value) return 'idle';
|
|
136
|
+
return inputs.hasFinishedTyping.value ? validationState.value : 'idle';
|
|
137
|
+
});
|
|
104
138
|
|
|
105
139
|
const errorMessage = computed<string | null>(() => {
|
|
140
|
+
const ext = config.externalError();
|
|
141
|
+
if (typeof ext === 'string' && ext.length > 0) return ext;
|
|
106
142
|
const v = validation.value;
|
|
107
143
|
if (v.ok || !v.reason) return null;
|
|
108
144
|
if (!inputs.phone.value) return null;
|
|
109
145
|
return config.errorMessages()?.[v.reason] ?? inputs.messages.value.errorMessages[v.reason];
|
|
110
146
|
});
|
|
111
147
|
|
|
112
|
-
const showError = computed<boolean>(() =>
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
const showError = computed<boolean>(() => {
|
|
149
|
+
if (!errorMessage.value) return false;
|
|
150
|
+
if (externalErrorActive.value) return true;
|
|
151
|
+
if (!config.showValidation()) return false;
|
|
152
|
+
return visibleValidationState.value === 'error';
|
|
153
|
+
});
|
|
115
154
|
|
|
116
155
|
const showHint = computed<boolean>(
|
|
117
156
|
() => !showError.value && !inputs.phone.value && !!required.value?.format_hint
|
package/src/index.ts
CHANGED
|
@@ -34,3 +34,5 @@ export * from './composables/useCountryDetection';
|
|
|
34
34
|
export * from './composables/useCountryMatching';
|
|
35
35
|
export * from './composables/useTypingPhase';
|
|
36
36
|
export * from './composables/useTelInputValidation';
|
|
37
|
+
export * from './composables/useCountrySelection';
|
|
38
|
+
export * from './composables/useSyncedModel';
|
package/src/types.ts
CHANGED
|
@@ -6,9 +6,12 @@ import type {
|
|
|
6
6
|
PhoneValidationReason,
|
|
7
7
|
PhoneValidationResult,
|
|
8
8
|
} from './composables/usePhoneValidation';
|
|
9
|
+
import type { ATelInputValidateOn } from './composables/useTelInputValidation';
|
|
9
10
|
import type { FlagUrlBuilder } from './utils/flag-url';
|
|
10
11
|
import type { Size } from '@alikhalilll/a-ui-base';
|
|
11
12
|
|
|
13
|
+
export type { ATelInputValidateOn } from './composables/useTelInputValidation';
|
|
14
|
+
|
|
12
15
|
/** Alias for the shared `Size` scale — kept for backwards-friendly naming. */
|
|
13
16
|
export type ATelInputSize = Size;
|
|
14
17
|
|
|
@@ -61,6 +64,48 @@ export type TelInputMessagesInput = Partial<Omit<TelInputMessages, 'errorMessage
|
|
|
61
64
|
|
|
62
65
|
export interface ATelInputProps {
|
|
63
66
|
class?: HTMLAttributes['class'];
|
|
67
|
+
/**
|
|
68
|
+
* Default `v-model` — the canonical **E.164** string (e.g. `'+201066105963'`).
|
|
69
|
+
*
|
|
70
|
+
* Reads + writes the full international number as a single value. Designed to drop
|
|
71
|
+
* straight into VeeValidate's `<Field v-slot="{ field }">` pattern (use
|
|
72
|
+
* `v-bind="field"`), native `<form>` submission, or any `v-model="phoneE164"` consumer.
|
|
73
|
+
*
|
|
74
|
+
* Stays in sync with the split `v-model:phone` + `v-model:country` contract — use
|
|
75
|
+
* either, or both.
|
|
76
|
+
*/
|
|
77
|
+
modelValue?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Forwarded to the inner `<input name="">`. Set this when participating in a native
|
|
80
|
+
* `<form>` submission, or when a form library (VeeValidate, etc.) wants a stable name.
|
|
81
|
+
*/
|
|
82
|
+
name?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Externally controlled error message. When set to a non-empty string the component
|
|
85
|
+
* is forced into the error visual state and renders this message via the `#error`
|
|
86
|
+
* slot — overriding internal libphonenumber validation. Wire this from VeeValidate,
|
|
87
|
+
* Zod, an async server check ("this phone is already registered"), or any custom
|
|
88
|
+
* validation layer.
|
|
89
|
+
*
|
|
90
|
+
* Pass `null` / `undefined` / `''` to defer to internal validation.
|
|
91
|
+
*/
|
|
92
|
+
error?: string | null;
|
|
93
|
+
/**
|
|
94
|
+
* `true` while an async validation is in flight (e.g. a server-side uniqueness
|
|
95
|
+
* check). Renders a small spinner inside the field and sets `aria-busy="true"` on
|
|
96
|
+
* the input. Does **not** disable the field — use `loading` for that. Replace the
|
|
97
|
+
* spinner via the `#validating` slot.
|
|
98
|
+
*
|
|
99
|
+
* Designed to be bound to the `validating` ref returned by `useTelField()`.
|
|
100
|
+
*/
|
|
101
|
+
validating?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* When to surface validation in the UI.
|
|
104
|
+
* - `'change'` (default) — visible state mirrors the typing-paused state.
|
|
105
|
+
* - `'blur'` — stays idle until the input has been blurred once (form-library friendly).
|
|
106
|
+
* - `'eager'` — mirror raw validation immediately, no typing pause.
|
|
107
|
+
*/
|
|
108
|
+
validateOn?: ATelInputValidateOn;
|
|
64
109
|
placeholder?: string;
|
|
65
110
|
disabled?: boolean;
|
|
66
111
|
loading?: boolean;
|
|
@@ -284,18 +329,53 @@ export interface ATelInputSlots {
|
|
|
284
329
|
empty?: (props: { query: string }) => unknown;
|
|
285
330
|
/** Replace the spinner shown in the picker slot during the debounce window. */
|
|
286
331
|
detecting?: () => unknown;
|
|
332
|
+
/** Replace the spinner shown inside the field while async validation is in flight. */
|
|
333
|
+
validating?: () => unknown;
|
|
287
334
|
}
|
|
288
335
|
|
|
289
336
|
/**
|
|
290
337
|
* Emit map for {@link ATelInput}. `update:phone` carries the digits-only string,
|
|
291
338
|
* `update:country` carries the dial-number (not ISO2). Surface for consumers who
|
|
292
339
|
* wire the events manually instead of via `v-model:phone` / `v-model:country`.
|
|
340
|
+
*
|
|
341
|
+
* `blur` / `focus` mirror the inner input's native events — useful for form
|
|
342
|
+
* libraries (VeeValidate's `handleBlur`, etc.).
|
|
293
343
|
*/
|
|
294
344
|
export type ATelInputEmits = {
|
|
345
|
+
'update:modelValue': [value: string];
|
|
295
346
|
'update:phone': [value: string];
|
|
296
347
|
'update:country': [value: number | null];
|
|
348
|
+
blur: [event: FocusEvent];
|
|
349
|
+
focus: [event: FocusEvent];
|
|
297
350
|
};
|
|
298
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Imperative API exposed by {@link ATelInput} via `defineExpose`. Grab it with
|
|
354
|
+
* `ref="tellRef"` + `tellRef.value?.focus()` — useful for form libraries that want
|
|
355
|
+
* to focus the offending field after a failed submit.
|
|
356
|
+
*/
|
|
357
|
+
export interface ATelInputExpose {
|
|
358
|
+
/** Full {@link PhoneValidationResult} for the current input. */
|
|
359
|
+
validation: import('vue').ComputedRef<PhoneValidationResult>;
|
|
360
|
+
/** Format hint + example for the currently selected country, or `null`. */
|
|
361
|
+
required: import('vue').ComputedRef<unknown>;
|
|
362
|
+
/** Selected country's dial code as a `+`-prefixed string (e.g. `+20`), or `null`. */
|
|
363
|
+
selectedDialCode: import('vue').ComputedRef<string | null>;
|
|
364
|
+
/** Raw validation state — not gated by typing pause / blur / `showValidation`. */
|
|
365
|
+
validationState: import('vue').ComputedRef<'idle' | 'valid' | 'error'>;
|
|
366
|
+
/** Surfacing-gated validation state — the one the UI actually displays. */
|
|
367
|
+
visibleValidationState: import('vue').ComputedRef<'idle' | 'valid' | 'error'>;
|
|
368
|
+
isDetecting: Readonly<import('vue').Ref<boolean>>;
|
|
369
|
+
hasFinishedTyping: Readonly<import('vue').Ref<boolean>>;
|
|
370
|
+
detectionAttempted: Readonly<import('vue').Ref<boolean>>;
|
|
371
|
+
/** Programmatically focus the inner `<input>`. */
|
|
372
|
+
focus(options?: FocusOptions): void;
|
|
373
|
+
/** Programmatically blur the inner `<input>`. */
|
|
374
|
+
blur(): void;
|
|
375
|
+
/** Programmatically select the inner `<input>`'s text. */
|
|
376
|
+
select(): void;
|
|
377
|
+
}
|
|
378
|
+
|
|
299
379
|
/**
|
|
300
380
|
* Props for {@link ACountrySelect} — the standalone country picker. Surface
|
|
301
381
|
* separately so it can be used outside `ATelInput` with full type support.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VeeValidate adapter for `@alikhalilll/a-tel-input`.
|
|
3
|
+
*
|
|
4
|
+
* `useTelField()` owns the two v-models (`phone` + `country`) and the canonical
|
|
5
|
+
* E.164 string used by VeeValidate / Zod / yup, and returns a single ready-to-bind
|
|
6
|
+
* object so a consumer doesn't have to glue the pieces together.
|
|
7
|
+
*
|
|
8
|
+
* Quick start:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { useTelField } from '@alikhalilll/a-tel-input/vee-validate';
|
|
12
|
+
* import { toTypedSchema } from '@vee-validate/zod';
|
|
13
|
+
* import { z } from 'zod';
|
|
14
|
+
* import { zPhone } from '@alikhalilll/a-tel-input/zod';
|
|
15
|
+
*
|
|
16
|
+
* const { phone, country, error, handleBlur, fieldProps } = useTelField('phone', {
|
|
17
|
+
* rules: toTypedSchema(zPhone()),
|
|
18
|
+
* validateOn: 'blur',
|
|
19
|
+
* });
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* ```vue
|
|
23
|
+
* <ATelInput
|
|
24
|
+
* v-model:phone="phone"
|
|
25
|
+
* v-model:country="country"
|
|
26
|
+
* v-bind="fieldProps"
|
|
27
|
+
* @blur="handleBlur"
|
|
28
|
+
* />
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Server-side validation (e.g. "does this phone already exist?") plugs in via the
|
|
32
|
+
* `rules` callback — VeeValidate already supports async rules:
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* const { phone, country, error, handleBlur, fieldProps, validating } = useTelField('phone', {
|
|
36
|
+
* rules: async (value: string) => {
|
|
37
|
+
* const sync = await zPhone().safeParseAsync(value);
|
|
38
|
+
* if (!sync.success) return sync.error.issues[0]!.message;
|
|
39
|
+
* const res = await $fetch('/api/phone/exists', { query: { phone: value } });
|
|
40
|
+
* return res.exists ? 'This phone is already registered.' : true;
|
|
41
|
+
* },
|
|
42
|
+
* validateOn: 'blur',
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* The `validating` ref is `true` while VeeValidate's async pipeline is in flight —
|
|
47
|
+
* bind it to ATelInput's `:validating` prop to surface a spinner inside the field.
|
|
48
|
+
*
|
|
49
|
+
* `vee-validate` is an **optional peer dependency** — install it yourself in your app.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import { computed, ref, watch, type ComputedRef, type MaybeRefOrGetter } from 'vue';
|
|
53
|
+
import { toValue } from 'vue';
|
|
54
|
+
import { useField, type FieldOptions, type RuleExpression } from 'vee-validate';
|
|
55
|
+
import { usePhoneValidation } from '../composables/usePhoneValidation';
|
|
56
|
+
import type { ATelInputValidateOn } from '../composables/useTelInputValidation';
|
|
57
|
+
|
|
58
|
+
export interface UseTelFieldOptions {
|
|
59
|
+
/**
|
|
60
|
+
* VeeValidate rules — a function, schema, or string. Use when the field stands
|
|
61
|
+
* alone (no `useForm` / no form-level `validationSchema`).
|
|
62
|
+
*
|
|
63
|
+
* **Important**: vee-validate **ignores field-level `rules` whenever the parent
|
|
64
|
+
* `useForm` is configured with `validationSchema`**. If you need async / server-
|
|
65
|
+
* side validation inside a form, chain it onto the form schema via
|
|
66
|
+
* `z.refine(async)` instead — that's what `handleSubmit` actually awaits and what
|
|
67
|
+
* drives this composable's `validating` ref (via `meta.pending`). See the README
|
|
68
|
+
* section "Server-side validation" for the full pattern.
|
|
69
|
+
*/
|
|
70
|
+
rules?: RuleExpression<string>;
|
|
71
|
+
/**
|
|
72
|
+
* Initial digits-only national number, e.g. `'1066105963'`. Defaults to `''`.
|
|
73
|
+
*/
|
|
74
|
+
initialPhone?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Initial dial-digit number, e.g. `20` for Egypt. Defaults to `null`.
|
|
77
|
+
*/
|
|
78
|
+
initialCountry?: number | null;
|
|
79
|
+
/**
|
|
80
|
+
* Default country (ISO2 like `'EG'` or dial code like `'20'`). Forwarded as the
|
|
81
|
+
* `defaultCountry` prop on `<ATelInput>` via `fieldProps`.
|
|
82
|
+
*/
|
|
83
|
+
defaultCountry?: string;
|
|
84
|
+
/**
|
|
85
|
+
* When to surface validation in the UI. Defaults to `'blur'` (the typical form-library
|
|
86
|
+
* UX). Forwarded as the `validateOn` prop on `<ATelInput>` via `fieldProps`.
|
|
87
|
+
*/
|
|
88
|
+
validateOn?: ATelInputValidateOn;
|
|
89
|
+
/**
|
|
90
|
+
* Forwarded to `useField` — extra options passed verbatim to VeeValidate. Use to
|
|
91
|
+
* configure `keepValueOnUnmount`, `syncVModel`, etc.
|
|
92
|
+
*/
|
|
93
|
+
fieldOptions?: Omit<FieldOptions<string>, 'initialValue'>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface UseTelFieldReturn {
|
|
97
|
+
/** `v-model:phone` source — digits-only national number. Bind to `<ATelInput>`. */
|
|
98
|
+
phone: import('vue').Ref<string>;
|
|
99
|
+
/** `v-model:country` source — dial-digit number (e.g., `20`). Bind to `<ATelInput>`. */
|
|
100
|
+
country: import('vue').Ref<number | null>;
|
|
101
|
+
/** The canonical E.164 string fed to VeeValidate's schema (read-only). */
|
|
102
|
+
e164: ComputedRef<string>;
|
|
103
|
+
/** Current validation error message, or `undefined` when valid. From `useField`. */
|
|
104
|
+
error: import('vue').Ref<string | undefined>;
|
|
105
|
+
/** `true` while VeeValidate is running an async rule (e.g. server-side check). */
|
|
106
|
+
validating: ComputedRef<boolean>;
|
|
107
|
+
/** Whether the field has been blurred / dirtied / submitted (from VeeValidate `meta`). */
|
|
108
|
+
meta: ReturnType<typeof useField<string>>['meta'];
|
|
109
|
+
/** Forward this to `<ATelInput @blur="handleBlur">` so VeeValidate's blur trigger fires. */
|
|
110
|
+
handleBlur: ReturnType<typeof useField<string>>['handleBlur'];
|
|
111
|
+
/** Imperatively trigger validation (e.g., after a programmatic value change). */
|
|
112
|
+
validate: ReturnType<typeof useField<string>>['validate'];
|
|
113
|
+
/** Imperatively set the error message — useful for server errors not raised by `rules`. */
|
|
114
|
+
setErrors: ReturnType<typeof useField<string>>['setErrors'];
|
|
115
|
+
/** Reset the field to its initial state. */
|
|
116
|
+
resetField: ReturnType<typeof useField<string>>['resetField'];
|
|
117
|
+
/**
|
|
118
|
+
* Ready-to-bind prop bag for `<ATelInput v-bind="fieldProps">`. Carries `name`,
|
|
119
|
+
* `error`, `validateOn`, `validating`, and `defaultCountry`.
|
|
120
|
+
*/
|
|
121
|
+
fieldProps: ComputedRef<{
|
|
122
|
+
name: string;
|
|
123
|
+
error: string | null;
|
|
124
|
+
validateOn: ATelInputValidateOn;
|
|
125
|
+
validating: boolean;
|
|
126
|
+
defaultCountry?: string;
|
|
127
|
+
}>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Register a phone field with VeeValidate. See file header for usage examples.
|
|
132
|
+
*
|
|
133
|
+
* @param name The field name (used by VeeValidate, also forwarded to the inner
|
|
134
|
+
* `<input name="">` for native form submission).
|
|
135
|
+
* @param options See {@link UseTelFieldOptions}.
|
|
136
|
+
*/
|
|
137
|
+
export function useTelField(
|
|
138
|
+
name: MaybeRefOrGetter<string>,
|
|
139
|
+
options: UseTelFieldOptions = {}
|
|
140
|
+
): UseTelFieldReturn {
|
|
141
|
+
const phone = ref<string>(options.initialPhone ?? '');
|
|
142
|
+
const country = ref<number | null>(options.initialCountry ?? null);
|
|
143
|
+
|
|
144
|
+
const v = usePhoneValidation();
|
|
145
|
+
void v.getCountries();
|
|
146
|
+
|
|
147
|
+
// Compose E.164 from the two v-models — this is the canonical value VeeValidate
|
|
148
|
+
// tracks. NANP (`+1`) resolves to `'US'` for validation; libphonenumber-js applies
|
|
149
|
+
// the same rule set to every NANP country so this is correct.
|
|
150
|
+
function composeE164(): string {
|
|
151
|
+
if (!phone.value) return '';
|
|
152
|
+
const dial = country.value;
|
|
153
|
+
if (dial == null) return '';
|
|
154
|
+
const matches = v.getCountriesByDial(String(dial));
|
|
155
|
+
const iso2 = matches[0]?.value;
|
|
156
|
+
if (!iso2) return '';
|
|
157
|
+
const res = v.validate({ country: { iso2 }, phone: phone.value });
|
|
158
|
+
return res.full_phone ?? '';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const initialValue = composeE164();
|
|
162
|
+
|
|
163
|
+
const field = useField<string>(name, options.rules, {
|
|
164
|
+
initialValue,
|
|
165
|
+
// Don't run rules on every keystroke — let validateOn drive when validation fires.
|
|
166
|
+
validateOnValueUpdate: false,
|
|
167
|
+
...options.fieldOptions,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Keep VeeValidate's value in sync with the two v-models.
|
|
171
|
+
watch(
|
|
172
|
+
[phone, country],
|
|
173
|
+
() => {
|
|
174
|
+
field.value.value = composeE164();
|
|
175
|
+
},
|
|
176
|
+
{ flush: 'post' }
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
const validating = computed(() => !!field.meta.pending);
|
|
180
|
+
|
|
181
|
+
const fieldProps = computed(() => ({
|
|
182
|
+
name: toValue(name),
|
|
183
|
+
error: (field.errorMessage.value ?? null) as string | null,
|
|
184
|
+
validateOn: options.validateOn ?? ('blur' as ATelInputValidateOn),
|
|
185
|
+
validating: validating.value,
|
|
186
|
+
defaultCountry: options.defaultCountry,
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
phone,
|
|
191
|
+
country,
|
|
192
|
+
e164: computed(() => field.value.value ?? ''),
|
|
193
|
+
error: field.errorMessage,
|
|
194
|
+
validating,
|
|
195
|
+
meta: field.meta,
|
|
196
|
+
handleBlur: field.handleBlur,
|
|
197
|
+
validate: field.validate,
|
|
198
|
+
setErrors: field.setErrors,
|
|
199
|
+
resetField: field.resetField,
|
|
200
|
+
fieldProps,
|
|
201
|
+
};
|
|
202
|
+
}
|