@teamnovu/kit-vue-forms 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/PLAN.md +209 -0
  2. package/dist/composables/useField.d.ts +12 -0
  3. package/dist/composables/useFieldRegistry.d.ts +15 -0
  4. package/dist/composables/useForm.d.ts +10 -0
  5. package/dist/composables/useFormState.d.ts +7 -0
  6. package/dist/composables/useSubform.d.ts +5 -0
  7. package/dist/composables/useValidation.d.ts +30 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.mjs +414 -0
  10. package/dist/types/form.d.ts +41 -0
  11. package/dist/types/util.d.ts +26 -0
  12. package/dist/types/validation.d.ts +16 -0
  13. package/dist/utils/general.d.ts +2 -0
  14. package/dist/utils/path.d.ts +11 -0
  15. package/dist/utils/type-helpers.d.ts +3 -0
  16. package/dist/utils/validation.d.ts +3 -0
  17. package/dist/utils/zod.d.ts +3 -0
  18. package/package.json +41 -0
  19. package/src/composables/useField.ts +74 -0
  20. package/src/composables/useFieldRegistry.ts +53 -0
  21. package/src/composables/useForm.ts +54 -0
  22. package/src/composables/useFormData.ts +16 -0
  23. package/src/composables/useFormState.ts +21 -0
  24. package/src/composables/useSubform.ts +173 -0
  25. package/src/composables/useValidation.ts +227 -0
  26. package/src/index.ts +11 -0
  27. package/src/types/form.ts +58 -0
  28. package/src/types/util.ts +73 -0
  29. package/src/types/validation.ts +22 -0
  30. package/src/utils/general.ts +7 -0
  31. package/src/utils/path.ts +87 -0
  32. package/src/utils/type-helpers.ts +3 -0
  33. package/src/utils/validation.ts +66 -0
  34. package/src/utils/zod.ts +24 -0
  35. package/tests/formState.test.ts +138 -0
  36. package/tests/integration.test.ts +200 -0
  37. package/tests/nestedPath.test.ts +651 -0
  38. package/tests/path-utils.test.ts +159 -0
  39. package/tests/subform.test.ts +1348 -0
  40. package/tests/useField.test.ts +147 -0
  41. package/tests/useForm.test.ts +178 -0
  42. package/tests/useValidation.test.ts +216 -0
  43. package/tsconfig.json +18 -0
  44. package/vite.config.js +39 -0
  45. package/vitest.config.ts +14 -0
@@ -0,0 +1,54 @@
1
+ import { computed, reactive, ref, toRef, type MaybeRef, type MaybeRefOrGetter, type Ref } from 'vue'
2
+ import type { Form, FormDataDefault } from '../types/form'
3
+ import type { EntityPaths, PickEntity } from '../types/util'
4
+ import type { ValidationStrategy } from '../types/validation'
5
+ import { cloneRefValue } from '../utils/general'
6
+ import { useFieldRegistry } from './useFieldRegistry'
7
+ import { useFormState } from './useFormState'
8
+ import { createSubformInterface, type SubformOptions } from './useSubform'
9
+ import { useValidation, type ValidationOptions } from './useValidation'
10
+
11
+ export interface UseFormOptions<T extends FormDataDefault> extends ValidationOptions<T> {
12
+ initialData: MaybeRefOrGetter<T>
13
+ validationStrategy?: MaybeRef<ValidationStrategy>
14
+ keepValuesOnUnmount?: MaybeRef<boolean>
15
+ }
16
+
17
+ export function useForm<T extends FormDataDefault>(options: UseFormOptions<T>) {
18
+ const initialData = computed(() => Object.freeze(cloneRefValue(options.initialData)))
19
+
20
+ const formData = ref<T>(cloneRefValue(initialData)) as Ref<T>
21
+
22
+ const state = reactive({
23
+ initialData: initialData,
24
+ formData,
25
+ })
26
+
27
+ const fields = useFieldRegistry(state)
28
+ const validationState = useValidation(state, options)
29
+ const formState = useFormState(state, fields)
30
+
31
+ const reset = () => {
32
+ formData.value = cloneRefValue(initialData)
33
+ fields.getFields().forEach(field => field.reset())
34
+ }
35
+
36
+ function getSubForm<K extends EntityPaths<T>>(
37
+ path: K,
38
+ options?: SubformOptions<PickEntity<T, K>>,
39
+ ): Form<PickEntity<T, K>> {
40
+ return createSubformInterface(formInterface, path, options)
41
+ }
42
+
43
+ const formInterface: Form<T> = {
44
+ ...fields,
45
+ ...validationState,
46
+ ...formState,
47
+ reset,
48
+ getSubForm,
49
+ initialData: toRef(state, 'initialData') as Form<T>['initialData'],
50
+ formData: toRef(state, 'formData') as Form<T>['formData'],
51
+ }
52
+
53
+ return formInterface
54
+ }
@@ -0,0 +1,16 @@
1
+ import { ref, type Ref, unref, watch } from 'vue'
2
+ import type { FormDataDefault } from '../types/form'
3
+
4
+ export function useFormData<T extends FormDataDefault>(
5
+ initialData: Ref<T>,
6
+ ) {
7
+ const formData = ref(unref(initialData))
8
+
9
+ watch(initialData, (newData) => {
10
+ if (newData !== formData.value) {
11
+ formData.value = newData
12
+ }
13
+ })
14
+
15
+ return { formData }
16
+ }
@@ -0,0 +1,21 @@
1
+ import { computed, unref } from 'vue'
2
+ import type { FormDataDefault, FormState } from '../types/form'
3
+ import type { FieldRegistry } from './useFieldRegistry'
4
+
5
+ export function useFormState<T extends FormDataDefault>(
6
+ formState: FormState<T>,
7
+ formFieldRegistry: FieldRegistry<T>,
8
+ ) {
9
+ const isDirty = computed(() => {
10
+ return JSON.stringify(formState.formData) !== JSON.stringify(formState.initialData)
11
+ })
12
+
13
+ const isTouched = computed(() => {
14
+ return formFieldRegistry.getFields().some(field => unref(field.touched))
15
+ })
16
+
17
+ return {
18
+ isDirty,
19
+ isTouched,
20
+ }
21
+ }
@@ -0,0 +1,173 @@
1
+ import { computed, isRef, unref, type Ref } from 'vue'
2
+ import type { Form, FormDataDefault, FormField } from '../types/form'
3
+ import type { EntityPaths, Paths, PickEntity, PickProps } from '../types/util'
4
+ import type { ValidationResult, Validator } from '../types/validation'
5
+ import { filterErrorsForPath, getLens, getNestedValue, joinPath } from '../utils/path'
6
+ import type { DefineFieldOptions } from './useFieldRegistry'
7
+ import { createValidator, SuccessValidationResult, type ValidatorOptions } from './useValidation'
8
+
9
+ export interface SubformOptions<_T extends FormDataDefault> {
10
+ // Additional subform-specific options can be added here
11
+ }
12
+
13
+ class NestedValidator<T extends FormDataDefault, P extends Paths<T>> implements Validator<T> {
14
+ constructor(
15
+ private path: P,
16
+ private validator: Validator<PickEntity<T, P>> | undefined,
17
+ ) {
18
+ }
19
+
20
+ async validate(data: T): Promise<ValidationResult> {
21
+ const subFormData = getNestedValue(data, this.path) as PickEntity<T, P>
22
+
23
+ if (!this.validator) {
24
+ return SuccessValidationResult
25
+ }
26
+
27
+ const validationResult = await this.validator.validate(subFormData)
28
+
29
+ return {
30
+ isValid: validationResult.isValid,
31
+ errors: {
32
+ general: validationResult.errors.general || [],
33
+ propertyErrors: validationResult.errors.propertyErrors
34
+ ? Object.fromEntries(
35
+ Object.entries(validationResult.errors.propertyErrors).map(([key, errors]) => [
36
+ joinPath(this.path, key),
37
+ errors,
38
+ ]),
39
+ )
40
+ : {},
41
+ },
42
+ }
43
+ }
44
+ }
45
+
46
+ export function createSubformInterface<
47
+ T extends FormDataDefault,
48
+ K extends EntityPaths<T>,
49
+ >(
50
+ mainForm: Form<T>,
51
+ path: K,
52
+ _options?: SubformOptions<PickEntity<T, K>>,
53
+ ): Form<PickEntity<T, K>> {
54
+ type ST = PickEntity<T, K>
55
+ type SP = Paths<ST>
56
+ type MP<P extends SP> = `${K}.${P}`
57
+ type ScopedMainPaths = Paths<T> & MP<SP>
58
+
59
+ // Create reactive data scoped to subform path
60
+ const formData = getLens(mainForm.formData, path) as Ref<ST>
61
+
62
+ const initialData = computed(() => {
63
+ return getNestedValue(mainForm.initialData.value, path) as ST
64
+ })
65
+
66
+ const adaptMainFormField = <S extends SP>(
67
+ field: FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>,
68
+ ): FormField<PickProps<ST, S>, S> => {
69
+ // Where P ist the full path in the main form, we need to adapt it to the subform's path
70
+ return {
71
+ ...field,
72
+ path: computed(() => unref(field.path).replace(path + '.', '')),
73
+ setValue: (newValue: PickProps<ST, S>) => {
74
+ field.setValue(newValue as PickProps<T, ScopedMainPaths>)
75
+ },
76
+ } as unknown as FormField<PickProps<ST, S>, S>
77
+ }
78
+
79
+ const getField = <P extends SP>(fieldPath: P) => {
80
+ const fullPath = joinPath(path, fieldPath)
81
+ const mainFormField = mainForm.getField(fullPath as ScopedMainPaths)
82
+
83
+ if (!mainFormField) {
84
+ return
85
+ }
86
+
87
+ return adaptMainFormField<P>(mainFormField)
88
+ }
89
+
90
+ // Field operations with path transformation
91
+ const defineField = <P extends SP>(fieldOptions: DefineFieldOptions<ST, P>) => {
92
+ const fullPath = joinPath(path, fieldOptions.path)
93
+
94
+ const mainField = mainForm.defineField({
95
+ ...fieldOptions,
96
+ path: fullPath as ScopedMainPaths,
97
+ })
98
+
99
+ return adaptMainFormField<P>(mainField)
100
+ }
101
+
102
+ const getFields = <P extends SP>(): FormField<PickProps<ST, P>, P>[] => {
103
+ return (mainForm.getFields() as FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>[])
104
+ .filter((field) => {
105
+ const fieldPath = field.path.value
106
+ return fieldPath.startsWith(path + '.') || fieldPath === path
107
+ })
108
+ .map(field => adaptMainFormField(field))
109
+ }
110
+
111
+ // Helper function to get all fields without type parameter
112
+ const getAllSubformFields = () => {
113
+ return (mainForm.getFields() as FormField<PickProps<T, ScopedMainPaths>, ScopedMainPaths>[])
114
+ .filter((field) => {
115
+ const fieldPath = field.path.value
116
+ return fieldPath.startsWith(path + '.') || fieldPath === path
117
+ })
118
+ }
119
+
120
+ // State computed from main form with path filtering
121
+ const isDirty = computed(() => getAllSubformFields().some(field => field.dirty.value))
122
+ const isTouched = computed(() => getAllSubformFields().some(field => field.touched.value))
123
+
124
+ // Validation delegates to main form
125
+ const isValid = computed(() => mainForm.isValid.value)
126
+ const isValidated = computed(() => mainForm.isValidated.value)
127
+ const errors = computed(() => filterErrorsForPath(unref(mainForm.errors), path))
128
+
129
+ const validateForm = () => mainForm.validateForm()
130
+
131
+ // Nested subforms
132
+ const getSubForm = <P extends EntityPaths<ST>>(
133
+ subPath: P,
134
+ subOptions?: SubformOptions<PickEntity<ST, P>>,
135
+ ) => {
136
+ const fullPath = joinPath(path, subPath) as EntityPaths<T>
137
+ return mainForm.getSubForm(
138
+ fullPath,
139
+ subOptions as SubformOptions<PickEntity<T, typeof fullPath>>,
140
+ ) as Form<PickEntity<ST, P>>
141
+ }
142
+
143
+ // Reset scoped to this subform
144
+ const reset = () => getAllSubformFields().forEach(field => field.reset())
145
+
146
+ const defineValidator = (options: ValidatorOptions<ST> | Ref<Validator<ST>>) => {
147
+ const subValidator = isRef(options) ? options : createValidator(options)
148
+ const validator = computed(
149
+ () => new NestedValidator<T, K>(path, unref(subValidator)),
150
+ )
151
+
152
+ mainForm.defineValidator(validator)
153
+
154
+ return subValidator
155
+ }
156
+
157
+ return {
158
+ formData,
159
+ initialData,
160
+ defineField,
161
+ getField,
162
+ getFields,
163
+ isDirty,
164
+ isTouched,
165
+ isValid,
166
+ isValidated,
167
+ errors,
168
+ defineValidator,
169
+ reset,
170
+ validateForm,
171
+ getSubForm,
172
+ }
173
+ }
@@ -0,0 +1,227 @@
1
+ import { computed, getCurrentScope, isRef, onBeforeUnmount, reactive, ref, toRefs, unref, watch, type MaybeRef, type Ref } from 'vue'
2
+ import z from 'zod'
3
+ import type { FormDataDefault } from '../types/form'
4
+ import type { ErrorBag, ValidationFunction, ValidationResult, Validator } from '../types/validation'
5
+ import { hasErrors, mergeErrors } from '../utils/validation'
6
+ import { flattenError } from '../utils/zod'
7
+
8
+ export interface ValidatorOptions<T> {
9
+ schema?: MaybeRef<z.ZodType>
10
+ validateFn?: MaybeRef<ValidationFunction<T>>
11
+ }
12
+
13
+ export interface ValidationOptions<T> extends ValidatorOptions<T> {
14
+ errors?: MaybeRef<ErrorBag>
15
+ }
16
+
17
+ export const SuccessValidationResult: ValidationResult = {
18
+ isValid: true,
19
+ errors: {
20
+ general: [],
21
+ propertyErrors: {},
22
+ },
23
+ }
24
+
25
+ class ZodSchemaValidator<T extends FormDataDefault> implements Validator<T> {
26
+ constructor(private schema?: z.ZodType<T>) {}
27
+
28
+ async validate(data: T): Promise<ValidationResult> {
29
+ if (!this.schema) {
30
+ return SuccessValidationResult
31
+ }
32
+
33
+ const result = await this.schema.safeParseAsync(data)
34
+
35
+ if (result.success) {
36
+ return SuccessValidationResult
37
+ }
38
+
39
+ const zodErrors = flattenError(result.error)
40
+
41
+ return {
42
+ isValid: false,
43
+ errors: {
44
+ general: zodErrors.general ?? [],
45
+ propertyErrors: zodErrors.propertyErrors ?? {},
46
+ },
47
+ }
48
+ }
49
+ }
50
+
51
+ class FunctionValidator<T extends FormDataDefault> implements Validator<T> {
52
+ constructor(private validateFn?: ValidationFunction<T>) {}
53
+
54
+ async validate(data: T): Promise<ValidationResult> {
55
+ if (!this.validateFn) {
56
+ return SuccessValidationResult
57
+ }
58
+
59
+ try {
60
+ const result = await this.validateFn(data)
61
+
62
+ if (result.isValid) {
63
+ return SuccessValidationResult
64
+ }
65
+
66
+ return result
67
+ } catch (error) {
68
+ return {
69
+ isValid: false,
70
+ errors: {
71
+ general: [(error as Error).message || 'Validation error'],
72
+ propertyErrors: {},
73
+ },
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ class CombinedValidator<T extends FormDataDefault> implements Validator<T> {
80
+ private schemaValidator: ZodSchemaValidator<T>
81
+ private functionValidator: FunctionValidator<T>
82
+
83
+ constructor(
84
+ private schema: z.ZodType<T>,
85
+ private validateFn: ValidationFunction<T>,
86
+ ) {
87
+ this.schemaValidator = new ZodSchemaValidator(this.schema)
88
+ this.functionValidator = new FunctionValidator(this.validateFn)
89
+ }
90
+
91
+ async validate(data: T): Promise<ValidationResult> {
92
+ const [schemaResult, functionResult] = await Promise.all([
93
+ this.schemaValidator.validate(data),
94
+ this.functionValidator.validate(data),
95
+ ])
96
+
97
+ const isValid = schemaResult.isValid && functionResult.isValid
98
+
99
+ return {
100
+ isValid,
101
+ errors: mergeErrors(schemaResult.errors, functionResult.errors),
102
+ }
103
+ }
104
+ }
105
+
106
+ export function createValidator<T extends FormDataDefault>(
107
+ options: ValidatorOptions<T>,
108
+ ): Ref<Validator<T> | undefined> {
109
+ return computed(() => new CombinedValidator(
110
+ unref(options.schema) as z.ZodType<T>,
111
+ unref(options.validateFn) as ValidationFunction<T>,
112
+ ))
113
+ }
114
+
115
+ export function useValidation<T extends FormDataDefault>(
116
+ formState: { formData: T },
117
+ options: ValidationOptions<T>,
118
+ ) {
119
+ const validationState = reactive({
120
+ validators: ref<Ref<Validator<T> | undefined>[]>([createValidator(options)]),
121
+ isValidated: false,
122
+ errors: unref(options.errors) ?? {
123
+ general: [],
124
+ propertyErrors: {},
125
+ },
126
+ })
127
+
128
+ // Watch for changes in the error bag and update validation state
129
+ watch(() => unref(options.errors), async () => {
130
+ const validationResults = await getValidationResults()
131
+
132
+ updateErrors(validationResults.errors)
133
+ }, { immediate: true })
134
+
135
+ // Watch for changes in validation function or schema
136
+ // to trigger validation. Only run if validation is already validated.
137
+ watch(
138
+ [() => validationState.validators],
139
+ async (validators) => {
140
+ if (!validationState.isValidated) {
141
+ return
142
+ }
143
+
144
+ if (validators) {
145
+ const validationResults = await getValidationResults()
146
+ validationState.errors = validationResults.errors
147
+ } else {
148
+ validationState.errors = SuccessValidationResult.errors
149
+ }
150
+ },
151
+ { immediate: true },
152
+ )
153
+
154
+ // Watch for changes in form data to trigger validation
155
+ watch(() => formState.formData, () => {
156
+ if (validationState.isValidated) {
157
+ validateForm()
158
+ }
159
+ })
160
+
161
+ const defineValidator = (options: ValidatorOptions<T> | Ref<Validator<T>>) => {
162
+ const validator = isRef(options) ? options : createValidator(options)
163
+
164
+ validationState.validators.push(validator)
165
+
166
+ if (getCurrentScope()) {
167
+ onBeforeUnmount(() => {
168
+ validationState.validators = validationState.validators.filter(
169
+ v => v !== validator,
170
+ )
171
+ })
172
+ }
173
+
174
+ return validator
175
+ }
176
+
177
+ async function getValidationResults() {
178
+ const validationResults = await Promise.all(
179
+ validationState.validators
180
+ .filter(validator => unref(validator) !== undefined)
181
+ .map(validator => unref(validator)!.validate(formState.formData)),
182
+ )
183
+
184
+ const isValid = validationResults.every(result => result.isValid)
185
+
186
+ let { errors } = SuccessValidationResult
187
+
188
+ if (!isValid) {
189
+ const validationErrors = validationResults.map(result => result.errors)
190
+
191
+ errors = mergeErrors(...validationErrors)
192
+ }
193
+
194
+ return {
195
+ errors,
196
+ isValid,
197
+ }
198
+ }
199
+
200
+ const updateErrors = (newErrors: ErrorBag) => {
201
+ validationState.errors = mergeErrors(unref(options.errors) ?? SuccessValidationResult.errors, newErrors)
202
+ }
203
+
204
+ const validateForm = async (): Promise<ValidationResult> => {
205
+ const validationResults = await getValidationResults()
206
+
207
+ updateErrors(validationResults.errors)
208
+
209
+ validationState.isValidated = true
210
+
211
+ return {
212
+ isValid: !hasErrors(validationResults.errors),
213
+ errors: validationState.errors,
214
+ }
215
+ }
216
+
217
+ const isValid = computed(() => !hasErrors(validationState.errors))
218
+
219
+ return {
220
+ ...toRefs(validationState),
221
+ validateForm,
222
+ defineValidator,
223
+ isValid,
224
+ }
225
+ }
226
+
227
+ export type ValidationState<T extends FormDataDefault> = ReturnType<typeof useValidation<T>>
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ // Main composable
2
+ export { useForm } from './composables/useForm'
3
+ export type { UseFormOptions } from './composables/useForm'
4
+
5
+ // Field composable
6
+ export { useField } from './composables/useField'
7
+ export type { UseFieldOptions } from './composables/useField'
8
+
9
+ // Types
10
+ export type { ValidationStrategy, ValidationErrorMessage as ErrorMessage, ValidationResult, ErrorBag } from './types/validation'
11
+ export type { DeepPartial, FormData } from './utils/type-helpers'
@@ -0,0 +1,58 @@
1
+ import type { Ref } from 'vue'
2
+ import type { DefineFieldOptions } from '../composables/useFieldRegistry'
3
+ import type { SubformOptions } from '../composables/useSubform'
4
+ import type { EntityPaths, Paths, PickEntity, PickProps } from './util'
5
+ import type { ErrorBag, ValidationErrorMessage, ValidationErrors, ValidationResult, Validator } from './validation'
6
+ import type { ValidatorOptions } from '../composables/useValidation'
7
+
8
+ export type FormDataDefault = object
9
+
10
+ export interface FormState<T extends FormDataDefault, TIn extends FormDataDefault = T> {
11
+ formData: T
12
+ initialData: TIn
13
+ }
14
+
15
+ export interface FormField<T, P extends string> {
16
+ value: Ref<T>
17
+ path: Ref<P>
18
+ initialValue: Readonly<Ref<T>>
19
+ errors: Ref<ValidationErrors>
20
+ touched: Ref<boolean>
21
+ dirty: Ref<boolean>
22
+ setValue: (newValue: T) => void
23
+ onBlur: () => void
24
+ onFocus: () => void
25
+ reset: () => void
26
+ setErrors: (newErrors: ValidationErrorMessage[]) => void
27
+ clearErrors: () => void
28
+ }
29
+
30
+ export interface Form<T extends FormDataDefault> {
31
+ // Data properties
32
+ formData: Ref<T>
33
+ initialData: Readonly<Ref<T>>
34
+
35
+ // Field operations
36
+ defineField: <P extends Paths<T>>(options: DefineFieldOptions<PickProps<T, P>, P>) => FormField<PickProps<T, P>, P>
37
+ getField: <P extends Paths<T>>(path: P) => FormField<PickProps<T, P>, P> | undefined
38
+ getFields: () => FormField<PickProps<T, Paths<T>>, Paths<T>>[]
39
+
40
+ // State properties
41
+ isDirty: Ref<boolean>
42
+ isTouched: Ref<boolean>
43
+ isValid: Ref<boolean>
44
+ isValidated: Ref<boolean>
45
+ errors: Ref<ErrorBag>
46
+
47
+ defineValidator: (options: ValidatorOptions<T> | Ref<Validator<T>>) => Ref<Validator<T> | undefined>
48
+
49
+ // Operations
50
+ reset: () => void
51
+ validateForm: () => Promise<ValidationResult>
52
+
53
+ // Nested subforms
54
+ getSubForm: <P extends EntityPaths<T>>(
55
+ path: P,
56
+ options?: SubformOptions<PickEntity<T, P>>,
57
+ ) => Form<PickEntity<T, P>>
58
+ }
@@ -0,0 +1,73 @@
1
+ import type { FormDataDefault } from './form'
2
+
3
+ /**
4
+ * Takes a dot-connected path and returns a tuple of its parts.
5
+ */
6
+ export type SplitPath<TPath extends string> =
7
+ TPath extends `${infer T1}.${infer T2}`
8
+ ? [T1, ...SplitPath<T2>]
9
+ : [TPath]
10
+
11
+ /**
12
+ * Picks the exact type of the Entity at the nested PropertyKeys path.
13
+ */
14
+ export type PickProps<Entity, PropertyKeys extends string> =
15
+ PropertyKeys extends `${infer TRoot}.${infer TRest}`
16
+ ? TRoot extends keyof Entity
17
+ ? TRest extends string
18
+ ? Entity[TRoot] extends object
19
+ ? PickProps<Entity[TRoot], TRest>
20
+ : never
21
+ : never
22
+ // We might have an array at hand but the key is a string with a number in it
23
+ : TRoot extends `${number}`
24
+ ? Entity extends unknown[]
25
+ ? TRest extends string
26
+ ? Entity[number] extends object
27
+ ? PickProps<Entity[number], TRest>
28
+ : never
29
+ : never
30
+ : never
31
+ : never
32
+ // We might have an array at hand but the key is a string with a number in it
33
+ : PropertyKeys extends keyof Entity
34
+ ? Entity[PropertyKeys]
35
+ : PropertyKeys extends `${number}`
36
+ ? Entity extends unknown[]
37
+ ? Entity[number]
38
+ : never
39
+ : never
40
+
41
+ /**
42
+ * Resolves to a union of dot-connected paths of all nested properties of T.
43
+ */
44
+ export type Paths<T, Seen = never> =
45
+ T extends Seen ? never :
46
+ T extends Array<infer ArrayType> ? `${number}` | `${number}.${Paths<ArrayType, Seen | T>}` :
47
+ T extends object
48
+ ? {
49
+ [K in keyof T]-?: `${Exclude<K, symbol>}${'' | `.${Paths<T[K], Seen | T>}`}`
50
+ }[keyof T]
51
+ : never
52
+
53
+ /**
54
+ * Removes the last part of a dot-connected path.
55
+ */
56
+ export type ButLast<T extends string> =
57
+ T extends `${infer Rest}.${infer Last}`
58
+ ? ButLast<Last> extends ''
59
+ ? Rest
60
+ : `${Rest}.${ButLast<Last>}`
61
+ : never
62
+
63
+ /**
64
+ * Combines Paths<T> with ButLast<Paths<T>> to include all paths except the last part.
65
+ * The & Paths<T> ensures that there are no entity paths that are not also available in Paths<T>.
66
+ */
67
+ export type EntityPaths<T> = ButLast<Paths<T>> & Paths<T>
68
+
69
+ export type PickEntity<Entity, PropertyKeys extends string> =
70
+ PropertyKeys extends unknown ? PickProps<Entity, EntityPaths<Entity> & PropertyKeys> & FormDataDefault : never
71
+
72
+ export type RestPath<T extends string, P extends string> =
73
+ P extends `${T}.${infer Rest}` ? Rest : never
@@ -0,0 +1,22 @@
1
+ import type { FormDataDefault } from './form'
2
+
3
+ export type ValidationStrategy = 'onTouch' | 'onFormOpen' | 'none' | 'preSubmit'
4
+
5
+ export type ValidationErrorMessage = string
6
+ export type ValidationErrors = ValidationErrorMessage[] | undefined
7
+
8
+ export interface ErrorBag {
9
+ general: ValidationErrors
10
+ propertyErrors: Record<string, ValidationErrors>
11
+ }
12
+
13
+ export interface ValidationResult {
14
+ isValid: boolean
15
+ errors: ErrorBag
16
+ }
17
+
18
+ export interface Validator<T extends FormDataDefault = FormDataDefault> {
19
+ validate: (data: T) => Promise<ValidationResult>
20
+ }
21
+
22
+ export type ValidationFunction<T> = (data: T) => Promise<ValidationResult>
@@ -0,0 +1,7 @@
1
+ import { toRaw, toValue, type MaybeRefOrGetter } from 'vue'
2
+
3
+ export function cloneRefValue<T>(ref: MaybeRefOrGetter<T> | MaybeRefOrGetter<Readonly<T>>): T {
4
+ const unreffed = toValue(ref)
5
+ const raw = toRaw(unreffed)
6
+ return structuredClone(raw) as T
7
+ }