@fastkit/vue-form-control
Version:
Basic form implementation library for Vue applications.
1 lines • 277 kB
Source Map (JSON)
{"version":3,"file":"vue-form-control.mjs","names":["update:modelValue","update:errors","pattern","patternFactory","minLengthFactory","maxLengthFactory","update:masked","update:typed","update:unmasked","update:modelValue","modelValue"],"sources":["../src/schemes/autocomplete.ts","../src/schemes/textinput.ts","../src/schemes/imask.ts","../src/logger.ts","../src/injections.ts","../src/composables/node.tsx","../src/composables/number.ts","../src/composables/autocompletable.ts","../src/composables/textable.ts","../src/composables/imask.ts","../src/composables/textinput.tsx","../src/components/VTextareaAutosize/VTextareaAutosize.tsx","../src/composables/textarea.tsx","../src/composables/selector.ts","../src/composables/selector-item-group.tsx","../src/composables/selector-item.tsx","../src/composables/boundable-input.ts","../src/composables/date-input.ts","../src/composables/file.tsx","../src/composables/wrapper.ts","../src/composables/group.ts","../src/composables/form.ts","../src/directives/imask.ts","../src/service.ts","../src/plugin.ts"],"sourcesContent":["export const FORM_AUTO_COMPLETES = [\n 'off',\n 'on',\n 'name',\n 'honorific-prefix',\n 'given-name',\n 'additional-name',\n 'family-name',\n 'honorific-suffix',\n 'nickname',\n 'email',\n 'username',\n 'new-password',\n 'current-password',\n 'one-time-code',\n 'organization-title',\n 'organization',\n 'street-address',\n 'address-line1',\n 'address-line2',\n 'address-line3',\n 'address-level4',\n 'address-level3',\n 'address-level2',\n 'address-level1',\n 'country',\n 'country-name',\n 'postal-code',\n 'cc-name',\n 'cc-given-name',\n 'cc-additional-name',\n 'cc-family-name',\n 'cc-number',\n 'cc-exp',\n 'cc-exp-month',\n 'cc-exp-year',\n 'cc-csc',\n 'cc-type',\n 'transaction-currency',\n 'transaction-amount',\n 'language',\n 'bday',\n 'bday-day',\n 'bday-month',\n 'bday-year',\n 'sex',\n 'tel',\n 'tel-country-code',\n 'tel-national',\n 'tel-area-code',\n 'tel-local',\n 'tel-extension',\n 'impp',\n 'url',\n 'photo',\n] as const;\n\nexport type FormAutoComplete = (typeof FORM_AUTO_COMPLETES)[number];\n","import {\n nilToEmptyString,\n removeSpace,\n toHalfWidth,\n toSingleSpace,\n} from '@fastkit/helpers';\n\nexport const TEXT_INPUT_TYPES = [\n 'color',\n 'date',\n 'datetime-local',\n 'email',\n 'month',\n 'number',\n 'password',\n 'search',\n 'tel',\n 'text',\n 'time',\n 'url',\n] as const;\n\nexport type TextInputType = (typeof TEXT_INPUT_TYPES)[number];\n\nexport const TEXT_INPUT_MODES = [\n 'decimal',\n 'email',\n 'none',\n 'numeric',\n 'search',\n 'tel',\n 'text',\n 'url',\n] as const;\n\nexport type TextInputMode = (typeof TEXT_INPUT_MODES)[number];\n\nexport type TextFinalizer = (value?: string | null) => string | Promise<string>;\n\nfunction defineFinalizers<T extends string>(\n finalizers: Record<T, TextFinalizer>,\n) {\n return finalizers;\n}\n\nexport const BUILTIN_TEXT_FINALIZERS = defineFinalizers({\n trim: (v) => nilToEmptyString(v).trim(),\n removeSpace: (v) => removeSpace(v),\n upper: (v) => nilToEmptyString(v).toUpperCase(),\n lower: (v) => nilToEmptyString(v).toLowerCase(),\n halfWidth: toHalfWidth,\n singleSpace: toSingleSpace,\n // kana: xxx,\n});\n\nexport type BuiltinTextFinalizerName = keyof typeof BUILTIN_TEXT_FINALIZERS;\n","import type {\n MaskedFunction,\n MaskedRegExp,\n MaskedEnum,\n MaskedRange,\n Masked,\n InputMask,\n MaskedDynamic,\n FactoryOpts,\n MaskedDynamicOptions,\n AppendFlags,\n} from 'imask';\n\nexport type IMaskEventType = 'accept' | 'complete';\n\nexport type AnyMaskedOptions = FactoryOpts;\n\nexport type IMaskEvent = CustomEvent<InputMask>;\n\nexport function createIMaskEvent(\n type: IMaskEventType,\n eventInitDict?: CustomEventInit<InputMask>,\n): IMaskEvent {\n return new CustomEvent<InputMask>(type, eventInitDict);\n}\n\nexport type IMaskTypedValue = string | number | Date;\n\n// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\ntype IMaskRawInput = RegExp | Function | string;\n\nfunction isRawType(source: unknown): source is IMaskRawInput {\n const t = typeof source;\n return t === 'string' || t === 'function' || source instanceof RegExp;\n}\n\ntype DynamicMaskedMeta = {\n /** Metadata assigned to dynamic mask options. */\n meta?: any;\n};\n\ntype AnyMaskedOptionsWithMeta = AnyMaskedOptions & DynamicMaskedMeta;\n\ntype AnyMaskedWithMeta = Masked & DynamicMaskedMeta;\n\ntype MaskedDynamicWithMeta = Omit<\n MaskedDynamic,\n 'currentMask' | 'compiledMasks'\n> & {\n currentMask?: AnyMaskedWithMeta;\n compiledMasks: AnyMaskedWithMeta[];\n};\n\nexport type MaskedDynamicOptionsWithMeta = Omit<\n MaskedDynamicOptions,\n 'mask' | 'dispatch'\n> & {\n mask: AnyMaskedOptionsWithMeta[];\n dispatch?: (\n value: string,\n masked: MaskedDynamicWithMeta,\n flags: AppendFlags,\n ) => Masked;\n};\n\nexport type IMaskInput =\n | Exclude<AnyMaskedOptions, MaskedDynamicOptions>\n | IMaskRawInput\n | Masked<any>\n | MaskedFunction\n | MaskedRegExp\n | MaskedEnum\n | MaskedRange\n | MaskedDynamicOptionsWithMeta\n | false\n | null\n | undefined\n | void;\n\nexport function resolveIMaskInput(\n source?: IMaskInput,\n): AnyMaskedOptions | undefined {\n if (isRawType(source)) {\n return <AnyMaskedOptions>{ mask: source };\n }\n if (source) {\n return source as AnyMaskedOptions;\n }\n}\n","import { TinyLogger, createTinyError } from '@fastkit/tiny-logger';\n\nconst name = 'vue-form-control';\n\nexport const logger = new TinyLogger(name);\n\nexport const VueFormControlError = createTinyError(name);\n","import { InjectionKey, inject } from 'vue';\nimport type { FormNodeControl } from './composables/node';\nimport type { FormSelectorControl } from './composables/selector';\nimport type { FormSelectorItemGroupControl } from './composables/selector-item-group';\nimport type { FormNodeWrapper } from './composables/wrapper';\nimport type { FormGroupControl } from './composables/group';\nimport type { VueForm } from './composables/form';\nimport type { VueFormService } from './service';\nimport { VueFormControlError } from './logger';\n\nexport const FormNodeInjectionKey: InjectionKey<FormNodeControl | null> =\n Symbol('FormNodeControl');\n\nexport const FormSelectorInjectionKey: InjectionKey<FormSelectorControl | null> =\n Symbol('FormSelectorControl');\n\nexport const FormSelectorItemGroupInjectionKey: InjectionKey<FormSelectorItemGroupControl | null> =\n Symbol('FormSelectorItemGroupControl');\n\nexport const FormNodeWrapperInjectionKey: InjectionKey<FormNodeWrapper | null> =\n Symbol('FormNodeWrapper');\n\nexport function useParentFormNodeWrapper() {\n return inject(FormNodeWrapperInjectionKey, null);\n}\n\nexport const FormGroupInjectionKey: InjectionKey<FormGroupControl | null> =\n Symbol('FormGroupControl');\n\nexport function useParentFormGroup() {\n return inject(FormGroupInjectionKey, null);\n}\n\nexport const FormInjectionKey: InjectionKey<VueForm | null> = Symbol('VueForm');\n\nexport function useParentForm() {\n return inject(FormInjectionKey, null);\n}\n\nexport const FormServiceInjectionKey: InjectionKey<VueFormService> =\n Symbol('VueFormService');\n\nexport function useVueForm() {\n const service = inject(FormServiceInjectionKey);\n if (!service) {\n throw new VueFormControlError('missing provided VueFormService');\n }\n return service;\n}\n","import {\n Prop,\n PropType,\n ExtractPropTypes,\n SetupContext,\n WritableComputedRef,\n ComputedRef,\n computed,\n ref,\n shallowRef,\n Ref,\n watch,\n provide,\n inject,\n onBeforeMount,\n onMounted,\n onBeforeUnmount,\n getCurrentInstance,\n ComponentInternalInstance,\n VNodeArrayChildren,\n markRaw,\n nextTick,\n} from 'vue';\n\nimport {\n VerifiableRule,\n VerifiableRuleOrFn,\n resolveVerifiableRule,\n ValidationError,\n validate,\n required as requiredRule,\n type Rule,\n} from '@fastkit/rules';\nimport {\n RecursiveArray,\n flattenRecursiveArray,\n toInt,\n mixin,\n Mixin,\n arrayRemove,\n} from '@fastkit/helpers';\nimport {\n createPropsOptions,\n DefineSlotsType,\n cleanupEmptyVNodeChild,\n} from '@fastkit/vue-utils';\nimport {\n FormNodeInjectionKey,\n useParentForm,\n useParentFormGroup,\n useParentFormNodeWrapper,\n useVueForm,\n} from '../injections';\nimport type { VueForm } from './form';\nimport type { FormGroupControl } from './group';\nimport type { FormNodeWrapper } from './wrapper';\nimport type { VueFormService } from '../service';\n\nexport type RecursiveVerifiableRuleOrFn = RecursiveArray<VerifiableRuleOrFn>;\n\nexport type RecursiveVerifiableRuleOrFnArray = Exclude<\n RecursiveVerifiableRuleOrFn,\n VerifiableRuleOrFn\n>;\n\nconst rulesToArray = (\n rules: RecursiveVerifiableRuleOrFn | undefined,\n shallowCopy: boolean = false,\n): RecursiveVerifiableRuleOrFnArray => {\n if (!rules) return [];\n\n return Array.isArray(rules) ? (shallowCopy ? rules.slice() : rules) : [rules];\n};\n\nconst rulesHasChanged = (\n currentRules: VerifiableRule[],\n beforeRules: VerifiableRule[],\n) => {\n if (currentRules.length !== beforeRules.length) {\n return true;\n }\n for (let i = 0, l = currentRules.length; i < l; i++) {\n const ar = currentRules[i];\n const br = beforeRules[i];\n if (ar !== br) return true;\n }\n return false;\n};\n\n/**\n * Merge two specified recursive rule specifications into a single array and return it.\n *\n * @param baseRules - Base rules\n * @param mergeRules - Merge rules\n * @returns Array of merged rules\n */\nexport function mergeFormNodeRules(\n baseRules: RecursiveVerifiableRuleOrFn | undefined,\n mergeRules: RecursiveVerifiableRuleOrFn | undefined,\n): RecursiveVerifiableRuleOrFnArray {\n const _mergeRules = rulesToArray(mergeRules);\n if (!baseRules) return _mergeRules;\n const _baseRules = rulesToArray(baseRules);\n return [..._baseRules, ..._mergeRules];\n}\n\nexport type FormNodeType = string | number | symbol;\n\nexport interface FormNodeError extends Omit<ValidationError, '$$symbol'> {}\n\nexport type FormNodeErrors = FormNodeError[];\n\nexport function toFormNodeError(\n source: string | ValidationError | FormNodeError,\n): FormNodeError {\n if (typeof source === 'string') {\n return {\n name: source,\n message: source,\n };\n }\n return source;\n}\n\n/**\n * Validation Timing\n *\n * - `always` Always validate\n * - `touch` Once touched, always validated thereafter.\n * - `blur` Once the focus is removed from the element at least once, subsequent validations will always be performed.\n * - `change` Once the value is changed at least once, subsequent validations will always be performed.\n * - `manual` Validation is not performed automatically. Only manual validation through programming is possible.\n *\n */\nexport type ValidateTiming = 'always' | 'touch' | 'blur' | 'change' | 'manual';\n\nexport type ValidationResult = ValidationError[] | null;\n\ntype ValidateResolver = (result: ValidationResult) => void;\n\nconst HAS_REQUIRED_RULE_RE = /(^|:)required($|:)/;\n\nfunction cheepDeepEqual(a: any, b: any) {\n return toCompareValue(a) === toCompareValue(b);\n}\n\nfunction toCompareValue(source: any): any {\n if (source && typeof source === 'object') {\n return JSON.stringify(source);\n }\n return source;\n}\n\nfunction cheepClone<T = any>(source: T): T {\n if (source && typeof source === 'object') {\n return JSON.parse(JSON.stringify(source));\n }\n return source;\n}\n\nexport type FormNodeStateExtension = (\n nodeControl: FormNodeControl,\n computedValue: boolean,\n) => boolean;\n\nexport interface FormNodeStateExtensions {\n disabled?: FormNodeStateExtension;\n readonly?: FormNodeStateExtension;\n viewonly?: FormNodeStateExtension;\n canOperation?: FormNodeStateExtension;\n}\n\nexport interface FormNodeControlBaseOptions {\n nodeType?: FormNodeType;\n requiredFactory?: () => Rule<any> | undefined;\n defaultValidateTiming?: ValidateTiming;\n validationValue?: () => any;\n stateExtensions?: FormNodeStateExtensions;\n /** Add a custom error message */\n errorMessages?: () => string | string[] | undefined;\n}\n\nexport interface FormNodeControlOptions<\n T = any,\n D = T,\n Required extends Prop<any> = BooleanConstructor,\n> extends FormNodeControlBaseOptions {\n modelValue?: Prop<T, D>;\n required?: Required;\n shallow?: boolean;\n}\n\nexport type FormNodeErrorSlotsSource = {\n /** Error message */\n error?: (error: FormNodeError) => any;\n} & {\n /** Error messages per validation rule */\n [K in `error:${string}`]: (error: FormNodeError) => any;\n};\n\nexport type FormNodeErrorSlots = DefineSlotsType<FormNodeErrorSlotsSource>;\n\nexport function createFormNodeProps<\n T,\n D = T,\n Required extends Prop<any> = PropType<boolean>,\n>(options: FormNodeControlOptions<T, D, Required> = {}) {\n const { modelValue, defaultValidateTiming, required = Boolean } = options;\n return {\n ...createPropsOptions({\n /**\n * form node name\n *\n * This is set as is for input elements.\n */\n name: String,\n /**\n * Tag string for node searching\n */\n tag: String,\n /** model value */\n modelValue: modelValue || {},\n /**\n * Tab index\n *\n * @default 0\n *\n * @see https://developer.mozilla.org/docs/Web/HTML/Global_attributes/tabindex\n */\n tabindex: {\n type: [String, Number],\n default: 0,\n },\n /** Automatic focus */\n autofocus: Boolean,\n /** disabled state */\n disabled: Boolean,\n /** read-only state */\n readonly: Boolean,\n /** view-only state */\n viewonly: Boolean,\n /**\n * Spell Check Settings\n *\n * @see https://developer.mozilla.org/docs/Web/HTML/Global_attributes/spellcheck\n */\n spellcheck: Boolean,\n /** required */\n required,\n /** clearable */\n clearable: Boolean,\n /**\n * Validation Timing\n *\n * - `always` Always validate\n * - `touch` Once touched, always validated thereafter.\n * - `blur` Once the focus is removed from the element at least once, subsequent validations will always be performed.\n * - `change` Once the value is changed at least once, subsequent validations will always be performed.\n * - `manual` Validation is not performed automatically. Only manual validation through programming is possible.\n *\n * @see {@link ValidateTiming}\n */\n validateTiming: {\n type: String as PropType<ValidateTiming>,\n default: defaultValidateTiming || 'touch',\n },\n /**\n * List of validation rules\n */\n rules: {\n type: [Array, Object] as PropType<RecursiveVerifiableRuleOrFn>,\n default: () => [],\n },\n /**\n * Validation dependencies\n *\n * In vue-form-control, validation is performed again whenever the input value or related values change. However, if the validation condition references values from other nodes or external reactive values that are not included in the node, those changes cannot be detected.\n * By registering a function that returns such external reactive values, validation can be automatically triggered.\n */\n validationDeps: Function as PropType<\n (nodeControl: FormNodeControl) => any\n >,\n /** Force an error state. */\n error: Boolean,\n /** List of error messages. */\n errorMessages: [String, Array] as PropType<string | string[]>,\n /**\n * Display error messages on this node itself\n *\n * If `true`, attempts to render errors for this node itself; if `false`, delegates error message display to the associated form group or wrapper.\n *\n * @default `false` if a parent group or wrapper exists and the `collectErrorMessages` setting is enabled; otherwise, `true`.\n */\n showOwnErrors: {\n type: Boolean,\n default: undefined,\n },\n /**\n * Detach and become independent from the parent node\n *\n * By default, the form node inherits the state of the form node existing in the parent tree and notifies the parent node of its own validation status, among other things. This option disables that behavior, allowing this node and its descendants to be detached from the parent node.\n */\n detach: Boolean,\n }),\n };\n}\n\nexport type FormNodeProps = ExtractPropTypes<\n ReturnType<typeof createFormNodeProps>\n>;\n\nexport function createFormNodeEmits<T, D = T>(\n options: FormNodeControlOptions<T, D> = {},\n) {\n return {\n /**\n * Update Model Values\n */\n 'update:modelValue': (value: T | D) => true,\n /**\n * Updating error content.\n * @param errors - List of error contents.\n */\n 'update:errors': (errors: FormNodeError[]) => true,\n /**\n * Update Model Values\n */\n change: (value: T | D) => true,\n /**\n * Focus on an element.\n * @param ev - FocusEvent\n */\n focus: (ev: FocusEvent) => true,\n /**\n * The focus is removed from an element.\n * @param ev - FocusEvent\n */\n blur: (ev: FocusEvent) => true,\n };\n}\n\nclass Wrapper<T, D = T> {\n // wrapped has no explicit return type so we can infer it\n wrapped(options: FormNodeControlOptions<T, D>) {\n return createFormNodeEmits<T, D>(options);\n }\n}\n\nexport interface FormNodeEmitOptions<T, D = T> extends ReturnType<\n Wrapper<T, D>['wrapped']\n> {}\n\nexport function createFormNodeSettings<T, D = T>(\n options: FormNodeControlOptions<T, D>,\n) {\n const props = createFormNodeProps<T, D>(options);\n const emits = createFormNodeEmits<T, D>(options);\n return { options, props, emits };\n}\n\nexport type FormNodeContext<T, D = T> = SetupContext<FormNodeEmitOptions<T, D>>;\n\n/**\n * Source code for rendering error messages of form nodes\n */\nexport interface FormNodeErrorMessageSource {\n /** Render error message */\n render: (slotsOverrides?: FormNodeErrorSlotsSource) => VNodeArrayChildren;\n /**\n * Error object\n *\n * @see {@link FormNodeError}\n */\n error: FormNodeError;\n /**\n * Node holding the error\n *\n * @see {@link FormNodeControl}\n */\n node: FormNodeControl;\n /**\n * Automatically generated key\n *\n * Can be safely used as the key for the vnode when rendering in lists, etc.\n */\n key: string;\n}\n\nlet _mountedId = 0;\n\n/**\n * Base class for all form nodes\n */\nexport class FormNodeControl<\n T = any,\n D = T,\n Required extends Prop<any> = BooleanConstructor,\n> {\n readonly _props: FormNodeProps;\n\n readonly _service: VueFormService;\n\n readonly nodeType?: FormNodeType;\n\n readonly __multiple: boolean;\n\n protected _isMounted = ref(false);\n\n protected _mountedId = ref<number>();\n\n protected _ctx: FormNodeContext<T, D>;\n\n protected _parentNode: FormNodeControl | null;\n\n protected _parentForm: VueForm | null;\n\n protected _parentFormGroup: FormGroupControl | null;\n\n protected _parentFormNodeWrapper: FormNodeWrapper | null;\n\n protected _booted = ref(false);\n\n protected _name: ComputedRef<string | undefined>;\n\n protected _value: Ref<T | D>;\n\n protected _initialValue: Ref<T | D>;\n\n protected _focused = ref(false);\n\n protected _children: Ref<FormNodeControl[]> = ref([]);\n\n protected _invalidChildren: ComputedRef<FormNodeControl[]>;\n\n protected _finalizePromise: Ref<(() => Promise<void>) | null> = ref(null);\n\n protected _validationErrors: Ref<ValidationError[]> = ref([]);\n\n protected _validateResolvers: ValidateResolver[] = [];\n\n protected _lastValidateValueChanged = true;\n\n protected _validating = ref(false);\n\n protected _validateRequestId = 0;\n\n protected _isDestroyed = false;\n\n protected _dirty: ComputedRef<boolean>;\n\n protected _touched = ref(false);\n\n protected _shouldValidate = ref(false);\n\n protected _currentValue: WritableComputedRef<T | D>;\n\n protected _errorMessages: ComputedRef<string[]>;\n\n protected _errors: ComputedRef<FormNodeError[]>;\n\n protected _resolvedErrorMessages: ComputedRef<FormNodeErrorMessageSource[]>;\n\n protected _errorCount: ComputedRef<number>;\n\n protected _isDisabled: ComputedRef<boolean>;\n\n protected _isReadonly: ComputedRef<boolean>;\n\n protected _isViewonly: ComputedRef<boolean>;\n\n protected _canOperation: ComputedRef<boolean>;\n\n protected _rules: ComputedRef<VerifiableRule[]>;\n\n protected _hasRequired: ComputedRef<boolean>;\n\n protected _tabindex: ComputedRef<number>;\n\n protected _cii: ComponentInternalInstance | null = null;\n\n protected _validationValueGetter?: () => any;\n\n protected _shouldSkipValidation = false;\n\n protected _stateExtensions: FormNodeStateExtensions;\n\n protected _requiredFactory: () => Rule<any> | undefined;\n\n /**\n * Root service of `vue-form-control`\n *\n * @see {@link VueFormService}\n */\n get service() {\n return this._service;\n }\n\n /**\n * form node name\n *\n * This is set as is for input elements.\n */\n get name(): string | undefined {\n return this._name.value;\n }\n\n /**\n * Tag string for node searching\n */\n get tag(): string | undefined {\n return this._props.tag;\n }\n\n /** Parent node */\n get parentNode(): FormNodeControl | null {\n return this._parentNode;\n }\n\n /** Parent form group */\n get parentFormGroup(): FormGroupControl | null {\n return this._parentFormGroup;\n }\n\n /** Parent form node wrapper */\n get parentFormNodeWrapper(): FormNodeWrapper | null {\n return this._parentFormNodeWrapper;\n }\n\n /** Parent form */\n get parentForm(): VueForm | null {\n return this._parentForm;\n }\n\n /** Automatic focus */\n get autofocus(): boolean {\n return this._props.autofocus;\n }\n\n /**\n * Detached and independent from the parent node\n */\n get detached(): boolean {\n return this._props.detach;\n }\n\n /**\n * Component created\n *\n * @remarks\n * Please be aware that it may not have been mounted yet.\n */\n get booted(): boolean {\n return this._booted.value;\n }\n\n /** Component mounted */\n get isMounted(): boolean {\n return this._isMounted.value;\n }\n\n /** If already mounted, its unique ID. */\n get mountedId(): number | undefined {\n return this._mountedId.value;\n }\n\n /** If already mounted, its unique ID. */\n get mountedNodeId(): string | undefined {\n const { mountedId } = this;\n return mountedId ? `_vfc-node-${mountedId}` : undefined;\n }\n\n /** Finalizing the value adjustment process */\n get isFinalizing(): boolean {\n return !!this._finalizePromise.value;\n }\n\n /** Validating the value */\n get validating(): boolean {\n return this._validating.value;\n }\n\n /**\n * Pending processing\n *\n * This is marked as `true` during the validation and finalization process of the value\n */\n get pending(): boolean {\n return this.validating || this.isFinalizing;\n }\n\n /** Current input value */\n get value(): T | D {\n return this._currentValue.value;\n }\n\n set value(value) {\n this._currentValue.value = value;\n }\n\n /** Value used for validation */\n get validationValue(): any {\n if (this._validationValueGetter) {\n return this._validationValueGetter();\n }\n return this.value;\n }\n\n /** In focus */\n get focused(): boolean {\n return this._focused.value;\n }\n\n /**\n * Initial value before commit\n *\n * This value, once initialized with the value passed when the FormNode is instantiated, will not be modified from within the vue-form-control package internals.\n * Calling the commit series of methods from the application side updates the value to its state at that moment.\n *\n * @see {@link FormNodeControl.commitSelfValue commitSelfValue}\n * @see {@link FormNodeControl.commitValue commitValue}\n * @see {@link FormNodeControl.commitSelf commitSelf}\n * @see {@link FormNodeControl.commit commit}\n */\n get initialValue(): T | D {\n return this._initialValue.value;\n }\n\n /**\n * The changes to the input value have not been committed yet\n *\n * @see {@link FormNodeControl.initialValue initialValue}\n */\n get dirty(): boolean {\n return this._dirty.value;\n }\n\n /**\n * The input value has not been changed from its initial value\n */\n get pristine(): boolean {\n return !this.dirty;\n }\n\n /**\n * Touched the elements of this node at least once\n */\n get touched(): boolean {\n return this._touched.value;\n }\n\n set touched(touched) {\n if (this._touched.value !== touched) {\n this._touched.value = touched;\n if (\n (touched && this.validateTimingIsTouch) ||\n this.validateTimingIsAlways\n ) {\n this.validateSelf();\n }\n }\n }\n\n /**\n * Not touched the elements of this node yet.\n */\n get untouched(): boolean {\n return !this.touched;\n }\n\n /**\n * The list of FormNode instances directly belonging to this node as children\n */\n get children(): FormNodeControl[] {\n return this._children.value;\n }\n\n /**\n * The list of FormNode instances directly belonging to itself, and possessing one or more errors\n */\n get invalidChildren(): FormNodeControl[] {\n return this._invalidChildren.value;\n }\n\n /**\n * The list of validation errors for the value within itself\n */\n get validationErrors(): ValidationError[] {\n return this._validationErrors.value;\n }\n\n /**\n * The list of all errors within itself\n *\n * This is a merged list of error messages injected through properties and its own `validationErrors`.\n *\n * @remarks\n * This list does not include error information specified with the `error` attribute.\n */\n get errors(): FormNodeError[] {\n return this._errors.value;\n }\n\n /**\n * Source code for all collected error messages\n *\n * This list is generated based on the setting of {@link FormNodeControl.showOwnErrors showOwnErrors}.\n *\n * @see {@link FormNodeErrorMessageSource}\n */\n get errorMessages(): FormNodeErrorMessageSource[] {\n return this._resolvedErrorMessages.value;\n }\n\n /**\n * Source code for the first error message among all collected messages\n *\n * This list is generated based on the setting of {@link FormNodeControl.showOwnErrors showOwnErrors}.\n *\n * @see {@link FormNodeErrorMessageSource}\n */\n get firstErrorMessage(): FormNodeErrorMessageSource | undefined {\n return this.errorMessages[0];\n }\n\n /**\n * Whether itself has one or more errors\n *\n * @remarks\n * This also takes into consideration the configuration of the `error` property.\n */\n get hasMyError(): boolean {\n return this.errorCount > 0;\n }\n\n /**\n * The number of errors it possesses\n */\n get errorCount(): number {\n return this._errorCount.value;\n }\n\n /**\n * Either itself or the parent node has one or more errors.\n */\n get hasError(): boolean {\n return this.hasMyError || (!this.detached && !!this.parentNode?.hasMyError);\n }\n\n /**\n * Either itself or the parent node is in a disabled state.\n */\n get isDisabled(): boolean {\n return this._isDisabled.value;\n }\n\n /**\n * Either itself or the parent node is in a read-only state.\n */\n get isReadonly(): boolean {\n return this._isReadonly.value;\n }\n\n /**\n * Either itself or the parent node is in a view-only state.\n */\n get isViewonly(): boolean {\n return this._isViewonly.value;\n }\n\n /**\n * Operable\n */\n get canOperation(): boolean {\n return this._canOperation.value;\n }\n\n /**\n * Validation Timing\n *\n *@see {@link ValidateTiming}\n */\n get validateTiming(): ValidateTiming {\n return this._props.validateTiming;\n }\n\n /**\n * Always perform value validation.\n */\n get validateTimingIsAlways(): boolean {\n return this.validateTiming === 'always';\n }\n\n /**\n * Perform value validation only when the elements of this node have been touched at least once.\n */\n get validateTimingIsTouch(): boolean {\n return this.validateTiming === 'touch';\n }\n\n /**\n * Perform value validation when focus is removed from the elements of this node.\n */\n get validateTimingIsBlur(): boolean {\n return this.validateTiming === 'blur';\n }\n\n /**\n * Perform value validation when the input value changes.\n */\n get validateTimingIsChange(): boolean {\n return this.validateTiming === 'change';\n }\n\n /**\n * Value validation is manually performed on the application side.\n */\n get validateTimingIsManual(): boolean {\n return this.validateTiming === 'manual';\n }\n\n /**\n * The list of all rules, including those specified in the properties under 'rules' and others calculated from values related to rule logic.\n */\n get rules(): VerifiableRule[] {\n return this._rules.value;\n }\n\n /**\n * Input is required\n *\n * @remarks\n * This checks whether there is at least one 'required' rule in the 'required' setting or within the specified 'rules'.\n */\n get isRequired(): boolean {\n return this._hasRequired.value;\n }\n\n /**\n * The component has been destroyed\n */\n get isDestroyed(): boolean {\n return this._isDestroyed;\n }\n\n /**\n * There is at least one node in an error state among the nodes directly under this node\n */\n get hasInvalidChild(): boolean {\n return this.invalidChildren.length > 0;\n }\n\n /**\n * Either this node or one of its descendants has an error\n */\n get invalid(): boolean {\n return this.hasMyError || this.hasInvalidChild;\n }\n\n /**\n * This node and none of its descendants have an error\n */\n get valid(): boolean {\n return !this.invalid;\n }\n\n /**\n * Tab index\n *\n * When the node is disabled, it is forcibly set to `-1`.\n */\n get tabindex(): number {\n return this._tabindex.value;\n }\n\n /**\n * Spell Check Settings\n *\n * @see https://developer.mozilla.org/docs/Web/HTML/Global_attributes/spellcheck\n */\n get spellcheck(): boolean {\n return this._props.spellcheck;\n }\n\n /**\n * The input value should be validated\n *\n * This varies based on the specified `validateTiming` and the user's interaction status.\n */\n get shouldValidate(): boolean {\n return this._shouldValidate.value;\n }\n\n /**\n * The form to which this node belongs is currently executing an asynchronous submission action\n *\n * @remarks\n * If the `detach` option is set, this will always be `false`.\n */\n get sending(): boolean {\n return (\n (!this.detached && !!this._parentForm && this._parentForm.sending) ||\n false\n );\n }\n\n /**\n * The current Vue instance initializing this node\n */\n get currentInstance(): ComponentInternalInstance | null {\n return this._cii;\n }\n\n /**\n * The host element of the current Vue instance initializing this node\n */\n get currentEl(): HTMLElement | null {\n const { currentInstance } = this;\n return currentInstance && (currentInstance.vnode.el as HTMLElement | null);\n }\n\n /**\n * Multiple input mode\n */\n get multiple(): boolean {\n return this.__multiple;\n }\n\n /**\n * Display error messages on this node itself\n */\n get showOwnErrors(): boolean {\n const { showOwnErrors } = this._props;\n if (showOwnErrors === false) return false;\n if (showOwnErrors) return true;\n if (this.parentFormGroup?.collectErrorMessages) {\n return false;\n }\n return !this.parentFormNodeWrapper?.collectErrorMessages;\n }\n\n /**\n * Whether to temporarily skip validation.\n * Useful for internal operations where validation is not needed.\n */\n get shouldSkipValidation(): boolean {\n return (\n (this._shouldSkipValidation ||\n this._resetGuard ||\n (!this.detached && this.parentNode?.shouldSkipValidation)) ??\n false\n );\n }\n\n readonly shallow: boolean;\n\n constructor(\n props: FormNodeProps,\n ctx: FormNodeContext<T, D>,\n options: FormNodeControlOptions<T, D, Required>,\n ) {\n markRaw(this);\n\n this.shallow = options?.shallow ?? false;\n this._value = options?.shallow ? shallowRef(null as any) : ref(null as any);\n this._initialValue = options?.shallow\n ? shallowRef(null as any)\n : ref(null as any);\n this._props = props;\n this._service = useVueForm();\n this._ctx = ctx;\n\n const { nodeType, stateExtensions } = options;\n\n this.__multiple = (props as any).multiple || false;\n this._name = computed(() => props.name);\n this.nodeType = nodeType;\n this._requiredFactory = options.requiredFactory || (() => requiredRule);\n this._validationValueGetter = options.validationValue;\n this._stateExtensions = stateExtensions || {};\n\n const parentNode = useParentFormNode();\n const parentFormNodeWrapper = useParentFormNodeWrapper();\n const parentFormGroup = useParentFormGroup();\n const parentForm = useParentForm();\n\n this._parentNode = parentNode;\n this._parentFormNodeWrapper = parentFormNodeWrapper;\n this._parentFormGroup = parentFormGroup;\n this._parentForm = parentForm;\n\n this._dirty = computed(\n () => !cheepDeepEqual(this.value, this.initialValue),\n );\n\n onMounted(() => {\n this._isMounted.value = true;\n this._mountedId.value = ++_mountedId;\n this._cii = getCurrentInstance();\n });\n\n provide(FormNodeInjectionKey, this);\n\n this._currentValue = computed<T | D>({\n get: () => this._value.value,\n set: (value) => {\n this.setValue(value as T | D);\n },\n });\n\n this._errorMessages = computed(() => {\n const { errorMessages = [] } = props;\n const messages = Array.isArray(errorMessages)\n ? errorMessages\n : [errorMessages];\n\n // @NOTE \"Without delay, the form node's state cannot be referenced by the options user.\"\n if (!this.booted) return messages;\n\n const moreMessages = options.errorMessages?.();\n if (moreMessages) {\n if (Array.isArray(moreMessages)) {\n messages.push(...moreMessages);\n } else {\n messages.push(moreMessages);\n }\n }\n return messages;\n });\n\n this._errors = computed(() => [\n ...this._errorMessages.value.map(toFormNodeError),\n ...this.validationErrors,\n ]);\n\n this._resolvedErrorMessages = computed(() =>\n this.showOwnErrors\n ? this.errors.map((error, index) =>\n this._createFormNodeErrorMessageSource(error, index),\n )\n : [],\n );\n\n this._invalidChildren = computed(() =>\n this.children.filter((node) => node.invalid),\n );\n\n this._errorCount = computed(() => {\n const baseCount = props.error ? 1 : 0;\n return this.errors.length + baseCount;\n });\n\n this._isDisabled = computed(() => {\n const isDisabled =\n props.disabled ||\n (!this.detached && !!parentNode && parentNode.isDisabled) ||\n this.sending;\n\n const { disabled } = this._stateExtensions;\n return disabled ? disabled(this, isDisabled) : isDisabled;\n });\n\n this._isReadonly = computed(() => {\n const isReadonly =\n props.readonly ||\n (!this.detached && !!parentNode && parentNode.isReadonly);\n const { readonly: readonlyFn } = this._stateExtensions;\n return readonlyFn ? readonlyFn(this, isReadonly) : isReadonly;\n });\n\n this._isViewonly = computed(() => {\n const isViewonly =\n props.viewonly ||\n (!this.detached && !!parentNode && parentNode.isViewonly);\n const { viewonly: viewonlyFn } = this._stateExtensions;\n return viewonlyFn ? viewonlyFn(this, isViewonly) : isViewonly;\n });\n\n this._canOperation = computed(() => {\n const canOperation =\n !this.isDisabled && !this.isReadonly && !this.isViewonly;\n\n const { canOperation: canOperationFn } = this._stateExtensions;\n return canOperationFn ? canOperationFn(this, canOperation) : canOperation;\n });\n\n this._rules = computed(() => this._resolveRules());\n\n this._hasRequired = computed(\n () => !!this._props.required || !!this.hasRequiredRule(),\n );\n\n this._tabindex = computed(() =>\n this.isDisabled ? -1 : toInt(props.tabindex),\n );\n\n (\n [\n '_syncValueFromProps',\n 'focus',\n 'blur',\n 'validateSelf',\n 'validate',\n ] as const\n ).forEach((fn) => {\n const _fn = this[fn];\n this[fn] = _fn.bind(this) as any;\n });\n\n this.setShouldValidate(this.validateTimingIsAlways);\n\n watch(() => props.modelValue, this._syncValueFromProps, {\n immediate: true,\n });\n this._initialValue.value = this.shallow\n ? this._value.value\n : (cheepClone(this._value.value) as any);\n\n watch(\n () => props.validateTiming,\n () => {\n if (\n this.validateTimingIsAlways ||\n (this.touched && this.validateTimingIsTouch) ||\n (this.dirty && this.validateTimingIsChange)\n ) {\n this.validateSelf();\n }\n },\n { immediate: true },\n );\n\n const onValidateValueChange = () => {\n this._lastValidateValueChanged = true;\n if (this.shouldSkipValidation) return;\n\n if (\n this.shouldValidate ||\n (this.isMounted && this.validateTimingIsChange) ||\n this.validateTimingIsAlways\n ) {\n this.validateSelf();\n }\n };\n\n watch(() => this.validationValue, onValidateValueChange, {\n immediate: true,\n });\n\n watch(\n () => this.value,\n (value) => {\n // onValidateValueChange();\n this._ctx.emit('change', value as any);\n },\n { immediate: true },\n );\n\n watch(\n () =>\n props.validationDeps && props.validationDeps(this as FormNodeControl),\n onValidateValueChange,\n // { deep: true },\n );\n\n watch(\n () => this.errors,\n (errors) => {\n ctx.emit('update:errors', errors);\n },\n { immediate: true },\n );\n\n parentFormNodeWrapper?.__joinFromNode(this);\n\n if (!this.detached) {\n parentFormGroup?.__joinFromNode(this);\n parentNode?._joinFromNode(this);\n }\n\n watch(\n () => props.detach,\n (detached) => {\n if (detached) {\n parentFormGroup?.__leaveFromNode(this);\n parentNode?._leaveFromNode(this);\n } else {\n parentFormGroup?.__joinFromNode(this);\n parentNode?._joinFromNode(this);\n }\n },\n );\n\n watch(\n () => this.rules,\n (currentRules, beforeRules) => {\n if (this.shouldValidate && rulesHasChanged(currentRules, beforeRules)) {\n this.validateSelf(true);\n }\n },\n );\n\n onBeforeMount(() => {\n this._booted.value = true;\n });\n\n onBeforeUnmount(() => {\n this.clearValidateResolvers();\n parentNode?._leaveFromNode(this);\n parentFormGroup?.__leaveFromNode(this);\n parentFormNodeWrapper?.__leaveFromNode(this);\n this._finalizePromise.value = null;\n this.resetSelfValidates();\n this._parentNode = null;\n this._parentFormNodeWrapper = null;\n this._parentFormGroup = null;\n this._parentForm = null;\n this._cii = null;\n this._isDestroyed = true;\n delete (this as any)._props;\n delete (this as any)._ctx;\n delete (this as any)._service;\n delete (this as any)._requiredFactory;\n });\n\n (['focusHandler', 'blurHandler'] as const).forEach((fn) => {\n this[fn] = this[fn].bind(this);\n });\n }\n\n /** @internal */\n _createFormNodeErrorMessageSource(\n error: FormNodeError,\n index: number,\n slotsOverrides?: FormNodeErrorSlotsSource,\n ): FormNodeErrorMessageSource {\n return {\n render: (_slotsOverrides) =>\n this.renderErrorSource(error, {\n ...slotsOverrides,\n ..._slotsOverrides,\n }),\n error,\n node: this,\n key: `${String(this.nodeType)}:${this.name}:${this.tag}:${\n error.name\n }:${index}`,\n };\n }\n\n protected hasRequiredRule() {\n return this.findRule(HAS_REQUIRED_RULE_RE);\n }\n\n /**\n * Recursively searches and retrieves the FormNode belonging to this node.\n *\n * @param predicate - Predicate executed recursively on descendant elements\n */\n findNodeRecursive(\n predicate: (node: FormNodeControl) => unknown,\n ): FormNodeControl | undefined {\n for (const child of this.children) {\n if (predicate(child)) return child;\n const hit = child.findNodeRecursive(predicate);\n if (hit) return hit;\n }\n }\n\n /**\n * Recursively retrieves all FormNodes belonging to this node.\n *\n * @param predicate - Predicate executed recursively on descendant elements\n */\n filterNodesRecursive(\n predicate: (node: FormNodeControl) => unknown,\n ): FormNodeControl[] {\n const hits: FormNodeControl[] = [];\n for (const child of this.children) {\n if (predicate(child)) hits.push(child);\n hits.push(...child.filterNodesRecursive(predicate));\n }\n return hits;\n }\n\n /**\n * Search for a node within this node that matches the specified name\n *\n * @param name Node name\n */\n findNodeByName(name: string): FormNodeControl | undefined {\n return this.findNodeRecursive((node) => node.name === name);\n }\n\n /**\n * Search for a node within this node that matches the specified tag\n *\n * @param tag Tag string\n */\n findNodeByTag(tag: string): FormNodeControl | undefined {\n return this.findNodeRecursive((node) => node.tag === tag);\n }\n\n protected _getContextOrDie() {\n const { _ctx } = this;\n if (!_ctx) throw new Error('missing form node context');\n return _ctx;\n }\n\n /**\n * Render the specified error source as a `VNodeArrayChildren`\n *\n * @param errorSource - string or FormNodeError\n */\n renderErrorSource(\n errorSource: string | FormNodeError,\n slotsOverrides?: FormNodeErrorSlotsSource,\n ): VNodeArrayChildren {\n const { slots } = this._getContextOrDie();\n const error =\n typeof errorSource === 'string'\n ? toFormNodeError(errorSource)\n : errorSource;\n if (slotsOverrides) {\n const slot =\n slotsOverrides[`error:${error.name}`] || slotsOverrides.error;\n const message = cleanupEmptyVNodeChild(slot?.(error));\n if (message) return message;\n }\n const slot = slots[`error:${error.name}`] || slots.error;\n if (slot) {\n const message = cleanupEmptyVNodeChild(slot?.(error));\n if (message) return message;\n }\n return (\n cleanupEmptyVNodeChild(this.service.resolveErrorMessage(error, this)) || [\n error.message,\n ]\n );\n }\n\n protected _finalize(): Promise<void> {\n return Promise.resolve();\n }\n\n /**\n * Finalize the input value\n */\n finalize(): Promise<void> {\n const getter = this._finalizePromise.value;\n if (getter) return getter();\n const promise = this._finalize().finally(() => {\n this._finalizePromise.value = null;\n });\n this._finalizePromise.value = () => promise;\n return promise;\n }\n\n /**\n * Ensure that the input value is finalized\n */\n ensureFinalized(): Promise<void> {\n const currentFinalize = this._finalizePromise.value?.();\n return currentFinalize || this.finalize();\n }\n\n /**\n * Ensure that the value of the self-node and all its child nodes is finalized\n */\n async finalizeAll(): Promise<void> {\n await Promise.all([\n this.ensureFinalized(),\n ...this.children.map((node) => node.ensureFinalized()),\n ]);\n }\n\n /**\n * Set the value\n *\n * @param value - value\n */\n setValue(value: T | D): boolean {\n if (this.shallow) {\n this._value.value = value;\n this._ctx.emit('update:modelValue', value);\n return true;\n }\n\n if (!cheepDeepEqual(this._value.value, value)) {\n const v = cheepClone(value);\n this._value.value = v as any;\n this._ctx.emit('update:modelValue', v as any);\n return true;\n }\n return false;\n }\n\n protected _syncValueFromProps(value: any) {\n const safeValue = this.safeModelValue(value);\n this._value.value = this.shallow\n ? safeValue\n : (cheepClone(safeValue) as any);\n }\n\n protected _resolveRules(): VerifiableRule[] {\n const { rules: propRules, required } = this._props;\n const rules = flattenRecursiveArray(propRules).map(resolveVerifiableRule);\n\n if (required) {\n const requiredRule = this._requiredFactory();\n requiredRule && rules.unshift(requiredRule);\n }\n\n rules.sort((a, b) => {\n const { $name: an } = a;\n const { $name: bn } = b;\n if (an === 'required') return -1;\n if (bn === 'required') return 1;\n return 0;\n });\n return rules;\n }\n\n /**\n * Set the validation execution necessity\n *\n * @param shouldValidate - The input value should be validated\n */\n setShouldValidate(shouldValidate: boolean): void {\n if (this.shouldValidate !== shouldValidate) {\n this._shouldValidate.value = shouldValidate;\n }\n }\n\n /**\n * Retrieve the default value when there is no input value\n */\n emptyValue(): T | D {\n return null as unknown as T | D;\n }\n\n /**\n * If the specified value is nullable, return `emptyValue`; otherwise, return the specified value as is\n *\n * @param value - Any value\n */\n safeModelValue(value: any): T | D {\n if (value == null) {\n return this.emptyValue();\n }\n return value;\n }\n\n /**\n * Find and retrieve a rule corresponding to the specified name or a name matching the regular expression\n *\n * @param ruleName - Name or regular expression\n */\n findRule(ruleName: string | RegExp): VerifiableRule | undefined {\n return this.rules.find((r) => {\n const { $name } = r;\n return typeof ruleName === 'string'\n ? ruleName === $name\n : ruleName.test($name);\n });\n }\n\n /**\n * Reset the input value of this node to the initial value or the value at the last commit, whichever is applicable\n *\n * @see {@link FormNodeControl.initialValue initialValue}\n *\n * @remarks\n * This method does not reset the validation state. Typically, consider using {@link FormNodeControl.resetSelf resetSelf}.\n */\n resetSelfValue(): void {\n this.value = cheepClone(this.initialValue);\n }\n\n /**\n * Reset the input value of this node and all its descendant nodes to the initial value or the value at the last commit, whichever is applicable\n *\n * @see {@link FormNodeControl.resetSelfValue resetSelfValue}\n *\n * @remarks\n * This method does not reset the validation state. Typically, consider using {@link FormNodeControl.resetSelf reset}.\n */\n resetValue(): void {\n this.resetSelfValue();\n this.children.forEach((child) => child.resetValue());\n }\n\n /**\n * Set the current input value as the initial value for this node.\n *\n * @remarks\n * This method does not reset the validation state. Typically, consider using {@link FormNodeControl.commitSelf commitSelf}.\n */\n commitSelfValue(): void {\n this._initialValue.value = cheepClone(this.value);\n }\n\n /**\n * Set the current input value as the initial value for this node and all its descendant nodes\n *\n * @remarks\n * This method does not reset the validation state. Typically, consider using {@link FormNodeControl.commit commit}.\n */\n commitValue(): void {\n this.commitSelfValue();\n this.children.forEach((child) => child.commitValue());\n }\n\n private _resetGuard: boolean = false;\n\n /**\n * Reset the validation state of this node\n *\n * @remarks\n * This method does not perform a reset on descendant nodes. Typically, consider using {@link FormNodeControl.resetValidates resetValidates}.\n */\n resetSelfValidates(): void {\n this._validationErrors.value = [];\n this._lastValidateValueChanged = true;\n this.touched = false;\n this.setShouldValidate(false);\n this._resetGuard = true;\n nextTick(() => {\n if (this.isDestroyed) return;\n this.setShouldValidate(this.validateTimingIsAlways);\n this._resetGuard = false;\n if (this.validateTimingIsAlways) {\n this.validateSelf();\n }\n });\n }\n\n /**\n * Reset the validation state of this node and all its descendant nodes\n */\n resetValidates(): void {\n this.resetSelfValidates();\n this.children.forEach((child) => child.resetValidates());\n }\n\n /**\n * Reset the input value of this node to the initial value and reset the validation state\n *\n * @remarks\n * This method does not reset the state of descendant nodes. Typically, consider using {@link FormNodeControl.reset reset}.\n */\n resetSelf(): void {\n this.resetSelfValue();\n this.resetSelfValidates();\n }\n\n /**\n * Execute any process while skipping ongoing asynchronous validation, if any\n *\n * @param fn - The function to be executed\n */\n skipValidation(fn: (...args: any) => any): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n try {\n this._shouldSkipValidation = true;\n fn();\n setTimeout(() => {\n this._shouldSkipValidation = false;