UNPKG

vue-formic-dynamic-forms

Version:

Powerful library for creating dynamic, validated forms in Vue 3 based on JSON schemas

1 lines 167 kB
{"version":3,"file":"index.mjs","sources":["../src/utils/conditions.ts","../src/composables/useDynamicField.ts","../src/validators/builtin.ts","../src/validators/adapters.ts","../src/validators/engine.ts","../src/composables/useDynamicForm.ts","../src/components/BaseField.vue","../src/components/SelectField.vue","../src/components/RadioField.vue","../src/components/CheckboxField.vue","../src/components/DynamicField.vue","../src/components/DynamicForm.vue","../src/index.ts"],"sourcesContent":["import type { FieldCondition, ConditionalLogic } from '@/types';\r\n\r\n/**\r\n * Операторы сравнения для условий\r\n */\r\nconst OPERATORS = {\r\n equals: (fieldValue: any, conditionValue: any): boolean => {\r\n return fieldValue === conditionValue;\r\n },\r\n \r\n notEquals: (fieldValue: any, conditionValue: any): boolean => {\r\n return fieldValue !== conditionValue;\r\n },\r\n \r\n contains: (fieldValue: any, conditionValue: any): boolean => {\r\n if (typeof fieldValue === 'string' && typeof conditionValue === 'string') {\r\n return fieldValue.includes(conditionValue);\r\n }\r\n if (Array.isArray(fieldValue)) {\r\n return fieldValue.includes(conditionValue);\r\n }\r\n return false;\r\n },\r\n \r\n notContains: (fieldValue: any, conditionValue: any): boolean => {\r\n return !OPERATORS.contains(fieldValue, conditionValue);\r\n },\r\n \r\n gt: (fieldValue: any, conditionValue: any): boolean => {\r\n const numValue = Number(fieldValue);\r\n const numCondition = Number(conditionValue);\r\n return !isNaN(numValue) && !isNaN(numCondition) && numValue > numCondition;\r\n },\r\n \r\n gte: (fieldValue: any, conditionValue: any): boolean => {\r\n const numValue = Number(fieldValue);\r\n const numCondition = Number(conditionValue);\r\n return !isNaN(numValue) && !isNaN(numCondition) && numValue >= numCondition;\r\n },\r\n \r\n lt: (fieldValue: any, conditionValue: any): boolean => {\r\n const numValue = Number(fieldValue);\r\n const numCondition = Number(conditionValue);\r\n return !isNaN(numValue) && !isNaN(numCondition) && numValue < numCondition;\r\n },\r\n \r\n lte: (fieldValue: any, conditionValue: any): boolean => {\r\n const numValue = Number(fieldValue);\r\n const numCondition = Number(conditionValue);\r\n return !isNaN(numValue) && !isNaN(numCondition) && numValue <= numCondition;\r\n },\r\n \r\n empty: (fieldValue: any): boolean => {\r\n if (fieldValue === null || fieldValue === undefined) return true;\r\n if (typeof fieldValue === 'string') return fieldValue.trim() === '';\r\n if (Array.isArray(fieldValue)) return fieldValue.length === 0;\r\n if (typeof fieldValue === 'object') return Object.keys(fieldValue).length === 0;\r\n return false;\r\n },\r\n \r\n notEmpty: (fieldValue: any): boolean => {\r\n return !OPERATORS.empty(fieldValue);\r\n },\r\n \r\n in: (fieldValue: any, conditionValues: any[]): boolean => {\r\n return Array.isArray(conditionValues) && conditionValues.includes(fieldValue);\r\n },\r\n \r\n notIn: (fieldValue: any, conditionValues: any[]): boolean => {\r\n return !OPERATORS.in(fieldValue, conditionValues);\r\n }\r\n} as const;\r\n\r\n/**\r\n * Оценивает одно условие\r\n */\r\nexport function evaluateCondition(\r\n condition: FieldCondition, \r\n formData: Record<string, any>\r\n): boolean {\r\n const fieldValue = getNestedValue(formData, condition.field);\r\n const operator = OPERATORS[condition.operator];\r\n \r\n if (!operator) {\r\n console.warn(`Unknown operator: ${condition.operator}`);\r\n return false;\r\n }\r\n \r\n // Специальная обработка для операторов in/notIn\r\n if (condition.operator === 'in' || condition.operator === 'notIn') {\r\n return operator(fieldValue, condition.values || []);\r\n }\r\n \r\n // Специальная обработка для empty/notEmpty\r\n if (condition.operator === 'empty' || condition.operator === 'notEmpty') {\r\n return (operator as (value: any) => boolean)(fieldValue);\r\n }\r\n \r\n return operator(fieldValue, condition.value);\r\n}\r\n\r\n/**\r\n * Оценивает массив условий с логикой AND/OR\r\n */\r\nexport function evaluateConditions(\r\n conditions: FieldCondition[],\r\n formData: Record<string, any>,\r\n logic: 'and' | 'or' = 'and'\r\n): boolean {\r\n if (!conditions || conditions.length === 0) {\r\n return true;\r\n }\r\n \r\n const results = conditions.map(condition => evaluateCondition(condition, formData));\r\n \r\n return logic === 'and' \r\n ? results.every(result => result)\r\n : results.some(result => result);\r\n}\r\n\r\n/**\r\n * Оценивает условную логику поля\r\n */\r\nexport function evaluateFieldLogic(\r\n conditionalLogic: ConditionalLogic,\r\n formData: Record<string, any>\r\n): {\r\n visible: boolean;\r\n required: boolean;\r\n disabled: boolean;\r\n} {\r\n const logic = conditionalLogic.logic || 'and';\r\n \r\n let visible = true;\r\n let required = false;\r\n let disabled = false;\r\n \r\n // Оценка условий показа\r\n if (conditionalLogic.show) {\r\n visible = evaluateConditions(conditionalLogic.show, formData, logic);\r\n }\r\n \r\n // Оценка условий скрытия (более приоритетны чем show)\r\n if (conditionalLogic.hide) {\r\n const shouldHide = evaluateConditions(conditionalLogic.hide, formData, logic);\r\n if (shouldHide) {\r\n visible = false;\r\n }\r\n }\r\n \r\n // Оценка условий обязательности\r\n if (conditionalLogic.required) {\r\n required = evaluateConditions(conditionalLogic.required, formData, logic);\r\n }\r\n \r\n // Оценка условий отключения\r\n if (conditionalLogic.disabled) {\r\n disabled = evaluateConditions(conditionalLogic.disabled, formData, logic);\r\n }\r\n \r\n return { visible, required, disabled };\r\n}\r\n\r\n/**\r\n * Получает значение по вложенному пути (например, \"user.profile.name\")\r\n */\r\nfunction getNestedValue(obj: Record<string, any>, path: string): any {\r\n return path.split('.').reduce((current, key) => {\r\n return current && typeof current === 'object' ? current[key] : undefined;\r\n }, obj);\r\n}\r\n\r\n/**\r\n * Класс для управления условиями всей формы\r\n */\r\nexport class ConditionsEngine {\r\n private formData: Record<string, any> = {};\r\n private fieldConditions: Map<string, ConditionalLogic> = new Map();\r\n private cache: Map<string, any> = new Map();\r\n \r\n constructor(formData: Record<string, any> = {}) {\r\n this.formData = { ...formData };\r\n }\r\n \r\n /**\r\n * Обновляет данные формы\r\n */\r\n updateFormData(data: Record<string, any>): void {\r\n this.formData = { ...data };\r\n this.cache.clear(); // Очищаем кеш при изменении данных\r\n }\r\n \r\n /**\r\n * Регистрирует условия для поля\r\n */\r\n registerFieldConditions(fieldName: string, conditions: ConditionalLogic): void {\r\n this.fieldConditions.set(fieldName, conditions);\r\n this.cache.delete(fieldName); // Удаляем из кеша при изменении условий\r\n }\r\n \r\n /**\r\n * Получает состояние поля на основе условий\r\n */\r\n getFieldState(fieldName: string): {\r\n visible: boolean;\r\n required: boolean;\r\n disabled: boolean;\r\n } {\r\n const cacheKey = fieldName;\r\n if (this.cache.has(cacheKey)) {\r\n return this.cache.get(cacheKey);\r\n }\r\n \r\n const conditions = this.fieldConditions.get(fieldName);\r\n let state = { visible: true, required: false, disabled: false };\r\n \r\n if (conditions) {\r\n state = evaluateFieldLogic(conditions, this.formData);\r\n }\r\n \r\n this.cache.set(cacheKey, state);\r\n return state;\r\n }\r\n \r\n /**\r\n * Получает состояния всех зарегистрированных полей\r\n */\r\n getAllFieldStates(): Record<string, { visible: boolean; required: boolean; disabled: boolean }> {\r\n const states: Record<string, any> = {};\r\n \r\n for (const fieldName of this.fieldConditions.keys()) {\r\n states[fieldName] = this.getFieldState(fieldName);\r\n }\r\n \r\n return states;\r\n }\r\n \r\n /**\r\n * Проверяет, зависит ли поле от других полей\r\n */\r\n getFieldDependencies(fieldName: string): string[] {\r\n const conditions = this.fieldConditions.get(fieldName);\r\n if (!conditions) return [];\r\n \r\n const dependencies = new Set<string>();\r\n \r\n const extractDependencies = (conditionArray: FieldCondition[] | undefined) => {\r\n if (!conditionArray) return;\r\n conditionArray.forEach(condition => {\r\n dependencies.add(condition.field);\r\n });\r\n };\r\n \r\n extractDependencies(conditions.show);\r\n extractDependencies(conditions.hide);\r\n extractDependencies(conditions.required);\r\n extractDependencies(conditions.disabled);\r\n \r\n return Array.from(dependencies);\r\n }\r\n \r\n /**\r\n * Получает все поля, которые зависят от указанного поля\r\n */\r\n getDependentFields(fieldName: string): string[] {\r\n const dependents: string[] = [];\r\n \r\n for (const currentFieldName of this.fieldConditions.keys()) {\r\n const dependencies = this.getFieldDependencies(currentFieldName);\r\n if (dependencies.includes(fieldName)) {\r\n dependents.push(currentFieldName);\r\n }\r\n }\r\n \r\n return dependents;\r\n }\r\n}\r\n","import { ref, computed, watch, readonly, type Ref } from 'vue';\r\nimport type { \r\n FieldSchema, \r\n FieldState, \r\n FieldOption,\r\n AsyncValidationResult \r\n} from '@/types';\r\nimport { ValidationEngine } from '@/validators';\r\nimport { ConditionsEngine, evaluateFieldLogic } from '@/utils/conditions';\r\n\r\nexport interface UseDynamicFieldOptions {\r\n formData: Ref<Record<string, any>>;\r\n validationEngine: ValidationEngine;\r\n conditionsEngine: ConditionsEngine;\r\n validateOnChange?: boolean;\r\n validateOnBlur?: boolean;\r\n debounceValidation?: number;\r\n}\r\n\r\nexport function useDynamicField(\r\n fieldSchema: FieldSchema,\r\n options: UseDynamicFieldOptions\r\n) {\r\n const {\r\n formData,\r\n validationEngine,\r\n conditionsEngine,\r\n validateOnChange = true,\r\n validateOnBlur = true,\r\n debounceValidation = 300\r\n } = options;\r\n\r\n // Состояние поля\r\n const fieldState = ref<FieldState>({\r\n value: fieldSchema.defaultValue ?? '',\r\n error: null,\r\n touched: false,\r\n dirty: false,\r\n validating: false,\r\n valid: true,\r\n visible: true,\r\n disabled: false,\r\n required: fieldSchema.validation?.required ?? false\r\n });\r\n\r\n // Опции для селектов/радио\r\n const fieldOptions = ref<FieldOption[]>([]);\r\n const loadingOptions = ref(false);\r\n\r\n // Вычисляемые свойства\r\n const fieldValue = computed({\r\n get: () => fieldState.value.value,\r\n set: (newValue) => {\r\n fieldState.value.value = newValue;\r\n fieldState.value.dirty = true;\r\n \r\n // Обновляем данные формы\r\n formData.value[fieldSchema.name] = newValue;\r\n \r\n if (validateOnChange) {\r\n validateField();\r\n }\r\n }\r\n });\r\n\r\n const isVisible = computed(() => fieldState.value.visible);\r\n const isDisabled = computed(() => fieldState.value.disabled || fieldSchema.attributes?.disabled);\r\n const isRequired = computed(() => fieldState.value.required);\r\n const hasError = computed(() => !!fieldState.value.error);\r\n const isValidating = computed(() => fieldState.value.validating);\r\n\r\n // Вычисляем состояние поля на основе условий\r\n const conditionalState = computed(() => {\r\n if (fieldSchema.conditions) {\r\n return evaluateFieldLogic(fieldSchema.conditions, formData.value);\r\n }\r\n return {\r\n visible: true,\r\n required: fieldSchema.validation?.required ?? false,\r\n disabled: fieldSchema.attributes?.disabled ?? false\r\n };\r\n });\r\n\r\n // Обновляем состояние поля при изменении условий\r\n watch(conditionalState, (newState) => {\r\n fieldState.value.visible = newState.visible;\r\n fieldState.value.required = newState.required;\r\n fieldState.value.disabled = newState.disabled;\r\n }, { immediate: true });\r\n\r\n // Валидация поля\r\n async function validateField(): Promise<AsyncValidationResult> {\r\n fieldState.value.validating = true;\r\n \r\n try {\r\n let result: AsyncValidationResult;\r\n \r\n if (debounceValidation > 0) {\r\n result = await validationEngine.validateFieldDebounced(\r\n fieldSchema,\r\n fieldState.value.value,\r\n formData.value,\r\n debounceValidation\r\n );\r\n } else {\r\n result = await validationEngine.validateField(\r\n fieldSchema,\r\n fieldState.value.value,\r\n formData.value\r\n );\r\n }\r\n \r\n fieldState.value.error = result.error || null;\r\n fieldState.value.valid = result.isValid;\r\n \r\n return result;\r\n } finally {\r\n fieldState.value.validating = false;\r\n }\r\n }\r\n\r\n // Обработчики событий\r\n function handleFocus() {\r\n // Событие фокуса\r\n }\r\n\r\n function handleBlur() {\r\n fieldState.value.touched = true;\r\n \r\n if (validateOnBlur) {\r\n validateField();\r\n }\r\n }\r\n\r\n function handleInput(event: Event) {\r\n const target = event.target as HTMLInputElement;\r\n let value: any = target.value;\r\n \r\n // Обработка разных типов полей\r\n switch (fieldSchema.type) {\r\n case 'number':\r\n // Если поле пустое, оставляем пустую строку\r\n if (target.value === '') {\r\n value = '';\r\n } else {\r\n const numValue = target.valueAsNumber;\r\n // Если значение не число (NaN), оставляем как строку для валидации\r\n value = isNaN(numValue) ? target.value : numValue;\r\n }\r\n break;\r\n case 'checkbox':\r\n value = (target as HTMLInputElement).checked;\r\n break;\r\n case 'date':\r\n case 'datetime-local':\r\n value = target.valueAsDate || target.value;\r\n break;\r\n case 'file':\r\n value = (target as HTMLInputElement).files;\r\n break;\r\n default:\r\n value = target.value;\r\n }\r\n \r\n fieldValue.value = value;\r\n }\r\n\r\n function handleChange(event: Event) {\r\n handleInput(event);\r\n }\r\n\r\n // Загрузка опций для селектов\r\n async function loadOptions() {\r\n \r\n if (Array.isArray(fieldSchema.options)) {\r\n fieldOptions.value = fieldSchema.options;\r\n return;\r\n }\r\n \r\n if (typeof fieldSchema.options === 'function') {\r\n loadingOptions.value = true;\r\n try {\r\n fieldOptions.value = await fieldSchema.options();\r\n } catch (error) {\r\n fieldOptions.value = [];\r\n } finally {\r\n loadingOptions.value = false;\r\n }\r\n }\r\n }\r\n\r\n // Сброс поля\r\n function resetField() {\r\n fieldState.value.value = fieldSchema.defaultValue ?? '';\r\n fieldState.value.error = null;\r\n fieldState.value.touched = false;\r\n fieldState.value.dirty = false;\r\n fieldState.value.validating = false;\r\n fieldState.value.valid = true;\r\n \r\n formData.value[fieldSchema.name] = fieldState.value.value;\r\n }\r\n\r\n // Установка значения\r\n function setValue(value: any) {\r\n fieldValue.value = value;\r\n }\r\n\r\n // Установка ошибки\r\n function setError(error: string | null) {\r\n fieldState.value.error = error;\r\n fieldState.value.valid = !error;\r\n }\r\n\r\n // Очистка ошибки\r\n function clearError() {\r\n setError(null);\r\n }\r\n\r\n // Атрибуты для привязки к input\r\n const inputAttrs = computed(() => {\r\n const attrs: Record<string, any> = {\r\n id: `field-${fieldSchema.name}`,\r\n name: fieldSchema.name,\r\n type: fieldSchema.type,\r\n value: fieldValue.value,\r\n required: isRequired.value,\r\n disabled: isDisabled.value,\r\n readonly: fieldSchema.attributes?.readonly,\r\n placeholder: fieldSchema.attributes?.placeholder,\r\n autocomplete: fieldSchema.attributes?.autocomplete,\r\n pattern: fieldSchema.attributes?.pattern,\r\n min: fieldSchema.attributes?.min,\r\n max: fieldSchema.attributes?.max,\r\n step: fieldSchema.attributes?.step,\r\n multiple: fieldSchema.attributes?.multiple,\r\n accept: fieldSchema.attributes?.accept,\r\n rows: fieldSchema.attributes?.rows,\r\n cols: fieldSchema.attributes?.cols,\r\n maxlength: fieldSchema.attributes?.maxlength,\r\n minlength: fieldSchema.attributes?.minlength,\r\n 'aria-invalid': hasError.value ? 'true' : 'false',\r\n 'aria-describedby': hasError.value ? `field-${fieldSchema.name}-error` : undefined\r\n };\r\n\r\n // Убираем undefined значения\r\n return Object.fromEntries(\r\n Object.entries(attrs).filter(([, value]) => value !== undefined)\r\n );\r\n });\r\n\r\n // Классы CSS\r\n const inputClasses = computed(() => {\r\n const classes: string[] = [];\r\n \r\n if (fieldSchema.fieldClass) {\r\n classes.push(fieldSchema.fieldClass);\r\n }\r\n \r\n if (hasError.value && fieldSchema.errorClass) {\r\n classes.push(fieldSchema.errorClass);\r\n }\r\n \r\n return classes.join(' ');\r\n });\r\n\r\n // Инициализация\r\n function initialize() {\r\n // Устанавливаем начальное значение в форме\r\n formData.value[fieldSchema.name] = fieldState.value.value;\r\n \r\n // Загружаем опции если нужно\r\n if (fieldSchema.options) {\r\n loadOptions();\r\n }\r\n \r\n // Регистрируем условия в движке\r\n if (fieldSchema.conditions) {\r\n conditionsEngine.registerFieldConditions(fieldSchema.name, fieldSchema.conditions);\r\n }\r\n }\r\n\r\n // Инициализируем поле\r\n initialize();\r\n\r\n return {\r\n // Состояние\r\n fieldState: readonly(fieldState),\r\n fieldValue,\r\n fieldOptions: readonly(fieldOptions),\r\n loadingOptions: readonly(loadingOptions),\r\n \r\n // Вычисляемые свойства\r\n isVisible,\r\n isDisabled,\r\n isRequired,\r\n hasError,\r\n isValidating,\r\n \r\n // Методы\r\n validateField,\r\n resetField,\r\n setValue,\r\n setError,\r\n clearError,\r\n loadOptions,\r\n \r\n // Обработчики событий\r\n handleFocus,\r\n handleBlur,\r\n handleInput,\r\n handleChange,\r\n \r\n // Атрибуты для привязки\r\n inputAttrs,\r\n inputClasses,\r\n \r\n // Схема поля\r\n fieldSchema: readonly(fieldSchema)\r\n };\r\n}","import type { Validator, ValidatorFunction } from '@/types';\r\n\r\n/**\r\n * Создает валидатор\r\n */\r\nfunction createValidator(\r\n type: string,\r\n validate: ValidatorFunction,\r\n defaultMessage?: string\r\n): (message?: string) => Validator {\r\n return (message?: string) => ({\r\n type,\r\n validate,\r\n message: message || defaultMessage || `Validation failed for ${type}`\r\n });\r\n}\r\n\r\n/**\r\n * Валидатор обязательного поля\r\n */\r\nexport const required = createValidator(\r\n 'required',\r\n (value: any) => {\r\n if (value === null || value === undefined) return false;\r\n if (typeof value === 'string') return value.trim().length > 0;\r\n if (Array.isArray(value)) return value.length > 0;\r\n if (typeof value === 'boolean') return true;\r\n if (typeof value === 'number') return !isNaN(value);\r\n return true;\r\n },\r\n 'Это поле обязательно для заполнения'\r\n);\r\n\r\n/**\r\n * Валидатор минимальной длины\r\n */\r\nexport const minLength = (length: number, message?: string) =>\r\n createValidator(\r\n 'minLength',\r\n (value: any) => {\r\n if (value === null || value === undefined || value === '') return true;\r\n const str = String(value);\r\n return str.length >= length;\r\n },\r\n message || `Минимальная длина: ${length} символов`\r\n )();\r\n\r\n/**\r\n * Валидатор максимальной длины\r\n */\r\nexport const maxLength = (length: number, message?: string) =>\r\n createValidator(\r\n 'maxLength',\r\n (value: any) => {\r\n if (value === null || value === undefined || value === '') return true;\r\n const str = String(value);\r\n return str.length <= length;\r\n },\r\n message || `Максимальная длина: ${length} символов`\r\n )();\r\n\r\n/**\r\n * Валидатор минимального значения\r\n */\r\nexport const min = (minValue: number, message?: string) =>\r\n createValidator(\r\n 'min',\r\n (value: any) => {\r\n if (value === null || value === undefined || value === '') return true;\r\n const num = Number(value);\r\n return !isNaN(num) && num >= minValue;\r\n },\r\n message || `Минимальное значение: ${minValue}`\r\n )();\r\n\r\n/**\r\n * Валидатор максимального значения\r\n */\r\nexport const max = (maxValue: number, message?: string) =>\r\n createValidator(\r\n 'max',\r\n (value: any) => {\r\n if (value === null || value === undefined || value === '') return true;\r\n const num = Number(value);\r\n return !isNaN(num) && num <= maxValue;\r\n },\r\n message || `Максимальное значение: ${maxValue}`\r\n )();\r\n\r\n/**\r\n * Валидатор email\r\n */\r\nexport const email = createValidator(\r\n 'email',\r\n (value: any) => {\r\n if (!value) return true;\r\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\r\n return emailRegex.test(String(value));\r\n },\r\n 'Введите корректный email адрес'\r\n);\r\n\r\n/**\r\n * Валидатор URL\r\n */\r\nexport const url = createValidator(\r\n 'url',\r\n (value: any) => {\r\n if (!value) return true;\r\n try {\r\n new URL(String(value));\r\n return true;\r\n } catch {\r\n return false;\r\n }\r\n },\r\n 'Введите корректный URL'\r\n);\r\n\r\n/**\r\n * Валидатор регулярного выражения\r\n */\r\nexport const pattern = (regex: RegExp | string, message?: string) =>\r\n createValidator(\r\n 'pattern',\r\n (value: any) => {\r\n if (!value) return true;\r\n const pattern = typeof regex === 'string' ? new RegExp(regex) : regex;\r\n return pattern.test(String(value));\r\n },\r\n message || 'Значение не соответствует требуемому формату'\r\n )();\r\n\r\n/**\r\n * Валидатор соответствия одному из значений\r\n */\r\nexport const oneOf = (values: any[], message?: string) =>\r\n createValidator(\r\n 'oneOf',\r\n (value: any) => {\r\n if (value === null || value === undefined) return true;\r\n return values.includes(value);\r\n },\r\n message || `Значение должно быть одним из: ${values.join(', ')}`\r\n )();\r\n\r\n/**\r\n * Валидатор исключения значений\r\n */\r\nexport const notOneOf = (values: any[], message?: string) =>\r\n createValidator(\r\n 'notOneOf',\r\n (value: any) => {\r\n if (value === null || value === undefined) return true;\r\n return !values.includes(value);\r\n },\r\n message || `Значение не может быть одним из: ${values.join(', ')}`\r\n )();\r\n\r\n/**\r\n * Валидатор числового значения\r\n */\r\nexport const number = createValidator(\r\n 'number',\r\n (value: any) => {\r\n if (value === null || value === undefined || value === '') return true;\r\n return !isNaN(Number(value));\r\n },\r\n 'Значение должно быть числом'\r\n);\r\n\r\n/**\r\n * Валидатор целого числа\r\n */\r\nexport const integer = createValidator(\r\n 'integer',\r\n (value: any) => {\r\n if (value === null || value === undefined || value === '') return true;\r\n const num = Number(value);\r\n return !isNaN(num) && Number.isInteger(num);\r\n },\r\n 'Значение должно быть целым числом'\r\n);\r\n\r\n/**\r\n * Кастомный валидатор\r\n */\r\nexport const custom = (validator: ValidatorFunction, message?: string) =>\r\n createValidator(\r\n 'custom',\r\n validator,\r\n message || 'Кастомная валидация не прошла'\r\n )();\r\n\r\n/**\r\n * Валидатор сравнения с другим полем\r\n */\r\nexport const sameAs = (fieldName: string, message?: string) =>\r\n createValidator(\r\n 'sameAs',\r\n (value: any, formData: Record<string, any>) => {\r\n const otherValue = formData[fieldName];\r\n return value === otherValue;\r\n },\r\n message || `Значение должно совпадать с полем ${fieldName}`\r\n )();\r\n\r\n/**\r\n * Валидатор отличия от другого поля\r\n */\r\nexport const differentFrom = (fieldName: string, message?: string) =>\r\n createValidator(\r\n 'differentFrom',\r\n (value: any, formData: Record<string, any>) => {\r\n const otherValue = formData[fieldName];\r\n return value !== otherValue;\r\n },\r\n message || `Значение должно отличаться от поля ${fieldName}`\r\n )();\r\n\r\n/**\r\n * Собираем все встроенные валидаторы\r\n */\r\nexport const builtinValidators = {\r\n required,\r\n minLength,\r\n maxLength,\r\n min,\r\n max,\r\n email,\r\n url,\r\n pattern,\r\n oneOf,\r\n notOneOf,\r\n number,\r\n integer,\r\n custom,\r\n sameAs,\r\n differentFrom\r\n};","import type { ValidatorAdapter, ValidationError } from '@/types';\n\n/**\n * Безопасная проверка наличия библиотеки\n */\nfunction isLibraryAvailable(libraryName: string): boolean {\n if (typeof window !== 'undefined') {\n // В браузере проверяем глобальные переменные\n switch (libraryName) {\n case 'yup':\n return !!(window as any).yup;\n case 'zod':\n return !!(window as any).z;\n case 'vee-validate':\n return !!(window as any).VeeValidate;\n default:\n return false;\n }\n }\n // В Node.js просто возвращаем false для упрощения\n return false;\n}\n\n/**\n * Адаптер для Yup\n */\nexport const yupAdapter: ValidatorAdapter = {\n name: 'yup',\n \n isAvailable(): boolean {\n return isLibraryAvailable('yup');\n },\n \n async validate(schema: any, value: any, _formData: Record<string, any>): Promise<ValidationError[]> {\n if (!this.isAvailable()) {\n return [{\n field: 'unknown',\n message: 'Yup library is not available',\n type: 'error',\n value\n }];\n }\n \n try {\n await schema.validate(value, { \n context: _formData,\n abortEarly: false \n });\n return [];\n } catch (error: any) {\n if (error.inner) {\n return error.inner.map((err: any) => ({\n field: err.path || 'unknown',\n message: err.message,\n type: err.type || 'validation',\n value: err.value\n }));\n }\n \n return [{\n field: error.path || 'unknown',\n message: error.message,\n type: error.type || 'validation',\n value\n }];\n }\n }\n};\n\n/**\n * Адаптер для Zod\n */\nexport const zodAdapter: ValidatorAdapter = {\n name: 'zod',\n \n isAvailable(): boolean {\n return isLibraryAvailable('zod');\n },\n \n async validate(schema: any, value: any, _formData: Record<string, any>): Promise<ValidationError[]> {\n if (!this.isAvailable()) {\n return [{\n field: 'unknown',\n message: 'Zod library is not available',\n type: 'error',\n value\n }];\n }\n \n try {\n schema.parse(value);\n return [];\n } catch (error: any) {\n if (error.errors) {\n return error.errors.map((err: any) => ({\n field: err.path?.join('.') || 'unknown',\n message: err.message,\n type: err.code || 'validation',\n value\n }));\n }\n \n return [{\n field: 'unknown',\n message: error.message || 'Validation failed',\n type: 'validation',\n value\n }];\n }\n }\n};\n\n/**\n * Адаптер для VeeValidate правил\n */\nexport const veeValidateAdapter: ValidatorAdapter = {\n name: 'vee-validate',\n \n isAvailable(): boolean {\n return isLibraryAvailable('vee-validate');\n },\n \n async validate(rule: string | Function, value: any, formData: Record<string, any>): Promise<ValidationError[]> {\n if (!this.isAvailable()) {\n return [{\n field: 'unknown',\n message: 'VeeValidate library is not available',\n type: 'error',\n value\n }];\n }\n \n try {\n let validator: Function;\n \n if (typeof rule === 'string') {\n const VeeValidate = (window as any).VeeValidate;\n validator = VeeValidate[rule];\n if (!validator) {\n throw new Error(`VeeValidate rule \"${rule}\" not found`);\n }\n } else {\n validator = rule;\n }\n \n const result = await validator(value, [], { form: formData });\n \n if (result === true || result === undefined) {\n return [];\n }\n \n return [{\n field: 'unknown',\n message: typeof result === 'string' ? result : 'Validation failed',\n type: 'vee-validate',\n value\n }];\n } catch (error: any) {\n return [{\n field: 'unknown',\n message: error.message || 'Validation failed',\n type: 'vee-validate',\n value\n }];\n }\n }\n};\n\n/**\n * Реестр адаптеров\n */\nexport const validatorAdapters = new Map<string, ValidatorAdapter>([\n ['yup', yupAdapter],\n ['zod', zodAdapter],\n ['vee-validate', veeValidateAdapter]\n]);\n\n/**\n * Получить доступный адаптер\n */\nexport function getValidatorAdapter(name: string): ValidatorAdapter | null {\n const adapter = validatorAdapters.get(name);\n return adapter && adapter.isAvailable() ? adapter : null;\n}\n\n/**\n * Получить все доступные адаптеры\n */\nexport function getAvailableAdapters(): ValidatorAdapter[] {\n return Array.from(validatorAdapters.values()).filter(adapter => adapter.isAvailable());\n}","import type { \n FieldSchema, \n ValidationResult, \n ValidationError,\n Validator,\n AsyncValidationResult,\n ValidationContext\n} from '@/types';\nimport { builtinValidators } from './builtin';\nimport { getValidatorAdapter } from './adapters';\n\n/**\n * Движок валидации\n */\nexport class ValidationEngine {\n private abortControllers = new Map<string, AbortController>();\n private validationCache = new Map<string, ValidationResult>();\n private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\n /**\n * Валидирует одно поле\n */\n async validateField(\n fieldSchema: FieldSchema,\n value: any,\n formData: Record<string, any>,\n context?: Partial<ValidationContext>\n ): Promise<AsyncValidationResult> {\n const fieldName = fieldSchema.name;\n \n // Отменяем предыдущую валидацию если она есть\n this.cancelFieldValidation(fieldName);\n \n // Создаем новый контроллер отмены\n const abortController = new AbortController();\n this.abortControllers.set(fieldName, abortController);\n \n const validationContext: ValidationContext = {\n fieldName,\n fieldValue: value,\n formData,\n formSchema: context?.formSchema!,\n abortSignal: abortController.signal,\n ...context\n };\n\n try {\n // Валидация встроенными валидаторами\n const builtinErrors = await this.validateWithBuiltinValidators(\n fieldSchema,\n value,\n formData,\n validationContext\n );\n \n if (builtinErrors.length > 0) {\n return {\n field: fieldName,\n isValid: false,\n error: builtinErrors[0].message,\n pending: false\n };\n }\n \n // Валидация внешними валидаторами\n const externalErrors = await this.validateWithExternalValidators(\n fieldSchema,\n value,\n formData,\n validationContext\n );\n \n if (externalErrors.length > 0) {\n return {\n field: fieldName,\n isValid: false,\n error: externalErrors[0].message,\n pending: false\n };\n }\n \n return {\n field: fieldName,\n isValid: true,\n pending: false\n };\n \n } catch (error: any) {\n // Проверяем, была ли операция отменена\n if (error.name === 'AbortError') {\n return {\n field: fieldName,\n isValid: false,\n pending: true\n };\n }\n \n return {\n field: fieldName,\n isValid: false,\n error: error.message || 'Ошибка валидации',\n pending: false\n };\n } finally {\n this.abortControllers.delete(fieldName);\n }\n }\n\n /**\n * Валидирует всю форму\n */\n async validateForm(\n fields: FieldSchema[],\n formData: Record<string, any>\n ): Promise<ValidationResult> {\n const validationPromises = fields.map(field => \n this.validateField(field, formData[field.name], formData)\n );\n \n const results = await Promise.allSettled(validationPromises);\n const errors: Record<string, string> = {};\n const fieldErrors: ValidationError[] = [];\n \n results.forEach((result, index) => {\n const field = fields[index];\n \n if (result.status === 'fulfilled') {\n const validationResult = result.value;\n if (!validationResult.isValid && validationResult.error) {\n errors[field.name] = validationResult.error;\n fieldErrors.push({\n field: field.name,\n message: validationResult.error,\n type: 'validation',\n value: formData[field.name]\n });\n }\n } else {\n errors[field.name] = 'Ошибка валидации';\n fieldErrors.push({\n field: field.name,\n message: 'Ошибка валидации',\n type: 'error',\n value: formData[field.name]\n });\n }\n });\n \n return {\n isValid: Object.keys(errors).length === 0,\n errors,\n fieldErrors\n };\n }\n\n /**\n * Валидация с встроенными валидаторами\n */\n private async validateWithBuiltinValidators(\n fieldSchema: FieldSchema,\n value: any,\n formData: Record<string, any>,\n _context: ValidationContext\n ): Promise<ValidationError[]> {\n const errors: ValidationError[] = [];\n const validation = fieldSchema.validation;\n \n if (!validation) return errors;\n \n // Создаем валидаторы на основе правил валидации\n const validators: Validator[] = [];\n \n if (validation.required) {\n validators.push(builtinValidators.required(validation.message));\n }\n \n if (validation.minLength !== undefined) {\n validators.push(builtinValidators.minLength(validation.minLength, validation.message));\n }\n \n if (validation.maxLength !== undefined) {\n validators.push(builtinValidators.maxLength(validation.maxLength, validation.message));\n }\n \n if (validation.min !== undefined) {\n validators.push(builtinValidators.min(validation.min, validation.message));\n }\n \n if (validation.max !== undefined) {\n validators.push(builtinValidators.max(validation.max, validation.message));\n }\n \n if (validation.email) {\n validators.push(builtinValidators.email(validation.message));\n }\n \n if (validation.url) {\n validators.push(builtinValidators.url(validation.message));\n }\n \n if (validation.pattern) {\n validators.push(builtinValidators.pattern(validation.pattern, validation.message));\n }\n \n if (validation.custom) {\n validators.push(builtinValidators.custom(validation.custom, validation.message));\n }\n \n // Выполняем валидацию\n for (const validator of validators) {\n try {\n const result = await validator.validate(value, formData, fieldSchema.name);\n \n if (result !== true) {\n errors.push({\n field: fieldSchema.name,\n message: typeof result === 'string' ? result : validator.message || 'Ошибка валидации',\n type: validator.type,\n value\n });\n break; // Останавливаемся на первой ошибке\n }\n } catch (error: any) {\n errors.push({\n field: fieldSchema.name,\n message: error.message || 'Ошибка валидации',\n type: 'error',\n value\n });\n break;\n }\n }\n \n return errors;\n }\n\n /**\n * Валидация с внешними валидаторами\n */\n private async validateWithExternalValidators(\n fieldSchema: FieldSchema,\n value: any,\n formData: Record<string, any>,\n _context: ValidationContext\n ): Promise<ValidationError[]> {\n const externalValidation = fieldSchema.externalValidation;\n if (!externalValidation) return [];\n \n const errors: ValidationError[] = [];\n \n // Yup валидация\n if (externalValidation.yup) {\n const adapter = getValidatorAdapter('yup');\n if (adapter) {\n try {\n const yupErrors = await adapter.validate(externalValidation.yup, value, formData);\n errors.push(...yupErrors);\n } catch (error) {\n // Ошибки уже обработаны в адаптере\n }\n }\n }\n \n // Zod валидация\n if (externalValidation.zod) {\n const adapter = getValidatorAdapter('zod');\n if (adapter) {\n try {\n const zodErrors = await adapter.validate(externalValidation.zod, value, formData);\n errors.push(...zodErrors);\n } catch (error) {\n // Ошибки уже обработаны в адаптере\n }\n }\n }\n \n // VeeValidate валидация\n if (externalValidation.veeValidate) {\n const adapter = getValidatorAdapter('vee-validate');\n if (adapter) {\n try {\n const veeErrors = await adapter.validate(externalValidation.veeValidate, value, formData);\n errors.push(...veeErrors);\n } catch (error) {\n // Ошибки уже обработаны в адаптере\n }\n }\n }\n \n // Кастомная валидация\n if (externalValidation.custom) {\n try {\n const result = await externalValidation.custom(value, formData);\n if (result !== true) {\n errors.push({\n field: fieldSchema.name,\n message: typeof result === 'string' ? result : 'Кастомная валидация не прошла',\n type: 'custom',\n value\n });\n }\n } catch (error: any) {\n errors.push({\n field: fieldSchema.name,\n message: error.message || 'Ошибка кастомной валидации',\n type: 'custom-error',\n value\n });\n }\n }\n \n return errors;\n }\n\n /**\n * Отменяет валидацию поля\n */\n cancelFieldValidation(fieldName: string): void {\n const controller = this.abortControllers.get(fieldName);\n if (controller) {\n controller.abort();\n this.abortControllers.delete(fieldName);\n }\n \n const timer = this.debounceTimers.get(fieldName);\n if (timer) {\n clearTimeout(timer);\n this.debounceTimers.delete(fieldName);\n }\n }\n\n /**\n * Валидация с debounce\n */\n validateFieldDebounced(\n fieldSchema: FieldSchema,\n value: any,\n formData: Record<string, any>,\n delay: number = 300\n ): Promise<AsyncValidationResult> {\n return new Promise((resolve) => {\n const fieldName = fieldSchema.name;\n \n // Отменяем предыдущий таймер\n const existingTimer = this.debounceTimers.get(fieldName);\n if (existingTimer) {\n clearTimeout(existingTimer);\n }\n \n // Устанавливаем новый таймер\n const timer = setTimeout(() => {\n this.debounceTimers.delete(fieldName);\n this.validateField(fieldSchema, value, formData).then(resolve);\n }, delay);\n \n this.debounceTimers.set(fieldName, timer);\n });\n }\n\n /**\n * Очистка ресурсов\n */\n dispose(): void {\n // Отменяем все активные валидации\n for (const controller of this.abortControllers.values()) {\n controller.abort();\n }\n this.abortControllers.clear();\n \n // Очищаем таймеры\n for (const timer of this.debounceTimers.values()) {\n clearTimeout(timer);\n }\n this.debounceTimers.clear();\n \n // Очищаем кеш\n this.validationCache.clear();\n }\n}","import { ref, computed, watch, reactive, readonly } from 'vue';\r\nimport type { \r\n FormSchema, \r\n FormState, \r\n FormEvents,\r\n ValidationResult\r\n} from '@/types';\r\nimport { ValidationEngine } from '@/validators';\r\nimport { ConditionsEngine } from '@/utils/conditions';\r\nimport { useDynamicField } from './useDynamicField';\r\n\r\nexport interface UseDynamicFormOptions {\r\n schema: FormSchema;\r\n initialData?: Record<string, any>;\r\n validateOnChange?: boolean;\r\n validateOnBlur?: boolean;\r\n validateOnSubmit?: boolean;\r\n resetOnSubmit?: boolean;\r\n debounceValidation?: number;\r\n}\r\n\r\nexport interface UseDynamicFormEvents extends Partial<FormEvents> {}\r\n\r\nexport function useDynamicForm(\r\n options: UseDynamicFormOptions,\r\n events?: UseDynamicFormEvents\r\n) {\r\n const {\r\n schema,\r\n initialData = {},\r\n validateOnChange = schema.config?.validateOnChange ?? true,\r\n validateOnBlur = schema.config?.validateOnBlur ?? true,\r\n validateOnSubmit = schema.config?.validateOnSubmit ?? true,\r\n resetOnSubmit = schema.config?.resetOnSubmit ?? false,\r\n debounceValidation = 300\r\n } = options;\r\n\r\n // Инициализируем движки\r\n const validationEngine = new ValidationEngine();\r\n const conditionsEngine = new ConditionsEngine(initialData);\r\n\r\n // Данные формы\r\n const formData = ref<Record<string, any>>({});\r\n \r\n // Состояние формы\r\n const formState = reactive<FormState>({\r\n values: {},\r\n errors: {},\r\n touched: {},\r\n dirty: {},\r\n isSubmitting: false,\r\n isValidating: false,\r\n isValid: true,\r\n isDirty: false,\r\n submitCount: 0,\r\n fields: {}\r\n });\r\n\r\n // Поля формы\r\n const fieldComposables = new Map<string, ReturnType<typeof useDynamicField>>();\r\n\r\n // Инициализация полей\r\n function initializeFields() {\r\n // Устанавливаем начальные значения\r\n const initialValues: Record<string, any> = {};\r\n \r\n schema.fields.forEach(fieldSchema => {\r\n const initialValue = initialData[fieldSchema.name] ?? fieldSchema.defaultValue ?? getDefaultValueForType(fieldSchema.type);\r\n initialValues[fieldSchema.name] = initialValue;\r\n \r\n // Создаем composable для поля\r\n const fieldComposable = useDynamicField(fieldSchema, {\r\n formData,\r\n validationEngine,\r\n conditionsEngine,\r\n validateOnChange,\r\n validateOnBlur,\r\n debounceValidation\r\n });\r\n \r\n fieldComposables.set(fieldSchema.name, fieldComposable);\r\n \r\n // Инициализируем состояние поля в форме\r\n formState.fields[fieldSchema.name] = {\r\n value: initialValue,\r\n error: null,\r\n touched: false,\r\n dirty: false,\r\n validating: false,\r\n valid: true,\r\n visible: true,\r\n disabled: false,\r\n required: fieldSchema.validation?.required ?? false\r\n };\r\n });\r\n \r\n formData.value = { ...initialValues };\r\n formState.values = { ...initialValues };\r\n \r\n // Обновляем движок условий\r\n conditionsEngine.updateFormData(formData.value);\r\n }\r\n\r\n // Получение значений по умолчанию для разных типов полей\r\n function getDefaultValueForType(fieldType: string): any {\r\n switch (fieldType) {\r\n case 'checkbox':\r\n return false;\r\n case 'number':\r\n case 'range':\r\n return 0;\r\n case 'multiselect':\r\n return [];\r\n case 'file':\r\n return null;\r\n default:\r\n return '';\r\n }\r\n }\r\n\r\n // Вычисляемые свойства\r\n const visibleFields = computed(() => {\r\n return schema.fields.filter(field => {\r\n const fieldComposable = fieldComposables.get(field.name);\r\n return fieldComposable?.isVisible.value ?? true;\r\n });\r\n });\r\n\r\n const isFormValid = computed(() => {\r\n return Object.values(formState.errors).every(error => !error);\r\n });\r\n\r\n const isFormDirty = computed(() => {\r\n return Object.values(formState.dirty).some(dirty => dirty);\r\n });\r\n\r\n const fieldErrors = computed(() => {\r\n return Object.entries(formState.errors)\r\n .filter(([, error]) => error)\r\n .map(([field, message]) => ({\r\n field,\r\n message: message!,\r\n type: 'validation',\r\n value: formData.value[field]\r\n }));\r\n });\r\n\r\n // Слежение за изменениями данных формы\r\n watch(formData, (newData) => {\r\n formState.values = { ...newData };\r\n conditionsEngine.updateFormData(newData);\r\n \r\n // Обновляем состояние dirty для каждого поля\r\n Object.keys(newData).forEach(fieldName => {\r\n const initialValue = initialData[fieldName] ?? schema.fields.find(f => f.name === fieldName)?.defaultValue;\r\n formState.dirty[fieldName] = newData[fieldName] !== initialValue;\r\n });\r\n \r\n formState.isDirty = isFormDirty.value;\r\n \r\n // Вызываем событие изменения\r\n if (events?.change) {\r\n const changedField = Object.keys(newData).find(key => \r\n newData[key] !== formState.values[key]\r\n );\r\n if (changedField) {\r\n events.change(changedField, newData[changedField], newData);\r\n }\r\n }\r\n }, { deep: true });\r\n\r\n // Валидация формы\r\n async function validateForm(): Promise<ValidationResult> {\r\n formState.isValidating = true;\r\n \r\n try {\r\n const result = await validationEngine.validateForm(schema.fields, formData.value);\r\n \r\n // Обновляем состояние ошибок\r\n formState.errors = { ...result.errors };\r\n formState.isValid = result.isValid;\r\n \r\n // Обновляем состояние полей\r\n Object.keys(formState.fields).forEach(fieldName => {\r\n formState.fields[fieldName].error = result.errors[fieldName] || null;\r\n formState.fields[fieldName].valid = !result.errors[fieldName];\r\n });\r\n \r\n if (events?.validate) {\r\n events.validate(result.errors);\r\n }\r\n \r\n return result;\r\n } finally {\r\n formState.isValidating = false;\r\n }\r\n }\r\n\r\n // Валидация отдельного поля\r\n async function validateField(fieldName: string): Promise<boolean> {\r\n const fieldComposable = fieldComposables.get(fieldName);\r\n if (!fieldComposable) return false;\r\n \r\n const result = await fieldComposable.validateField();\r\n \r\n // Обновляем состояние поля в форме\r\n formState.errors[fieldName] = result.error || '';\r\n formState.fields[fieldName].error = result.error || null;\r\n formState.fields[fieldName].valid = result.isValid;\r\n formState.fields[fieldName].validating = result.pending || false;\r\n \r\n return result.isValid;\r\n }\r\n\r\n // Отправка формы\r\n async function submitForm(): Promise<void> {\r\n if (formState.isSubmitting) return;\r\n \r\n formState.isSubmitting = true;\r\n formState.submitCount++;\r\n \r\n try {\r\n // Отмечаем все поля как touched\r\n Object.keys(formState.fields).forEach(fieldName => {\r\n formState.touched[fieldName] = true;\r\n formState.fields[fieldName].touched = true;\r\n });\r\n \r\n // Валидация перед отправкой\r\n let isValid = true;\r\n if (validateOnSubmit) {\r\n const validationResult = await validateForm();\r\n isValid = validationResult.isValid;\r\n }\r\n \r\n // Вызываем событие отправки\r\n if (events?.submit) {\r\n await events.submit(formData.value, isValid);\r\n }\r\n \r\n // Сбрасываем форму если нужно\r\n if (resetOnSubmit && isValid) {\r\n resetForm();\r\n }\r\n } finally {\r\n formState.isSubmitting = false;\r\n }\r\n }\r\n\r\n // Сброс формы\r\n function resetForm(): void {\r\n // Сбрасываем все поля\r\n fieldComposables.forEach(fieldComposable => {\r\n fieldComposable.resetField();\r\n });\r\n \r\n // Сбрасываем состояние формы\r\n formState.errors = {};\r\n formState.touched = {};\r\n formState.dirty = {};\r\n formState.isDirty = false;\r\n formState.isValid = true;\r\n formState.submitCount = 0;\r\n \r\n // Сбрасываем состояние полей\r\n Object.keys(formState.fields).forEach(fieldName => {\r\n const field = schema.fields.find(f => f.name === fieldName);\r\n const defaultValue = field?.defaultValue ?? getDefaultValueForType(field?.type || 'text');\r\n \r\n formState.fields[fieldName] = {\r\n value: defaultValue,\r\n error: null,\r\n touched: false,\r\n dirty: false,\r\n validating: false,\r\n valid: true,\r\n visible: true,\r\n disabled: false,\r\n required: field?.validation?.required ?? false\r\n };\r\n });\r\n \r\n if (events?.reset) {\r\n events.reset();\r\n }\r\n }\r\n\r\n // Установка значений формы\r\n function setFormValues(values: Record<string, any>): void {\r\n Object.entries(values).forEach(([fieldName, value]) => {\r\n const fieldComposable = fieldComposables.get(fieldName);\r\n if (fieldComposable) {\r\n fieldComposable.setValue(value);\r\n }\r\n });\r\n }\r\n\r\n // Установка ошибок формы\r\n function setFormErrors(errors: Record<string, string>): void {\r\n Object.entries(errors).forEach(([fieldName, error]) => {\r\n const fieldComposable = fieldComposables.get(fieldName);\r\n