Skip to content

Commit bd2d9f3

Browse files
committed
feat: Implement inertia Form component support
1 parent 1bdcdf6 commit bd2d9f3

File tree

3 files changed

+170
-34
lines changed

3 files changed

+170
-34
lines changed

packages/vue-inertia/src/form.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { defineComponent, h, PropType, ref } from 'vue'
2+
import { Form as InertiaForm } from '@inertiajs/vue3'
3+
import type { FormComponentProps, FormComponentSlotProps, FormDataConvertible } from '@inertiajs/core'
4+
import { formDataToObject, Method } from '@inertiajs/core'
5+
import type { ValidationConfig } from 'laravel-precognition'
6+
import { createValidator, toSimpleValidationErrors } from 'laravel-precognition'
7+
8+
// Thin wrapper over Inertia's <Form> that wires Precognition live validation
9+
export const Form = defineComponent({
10+
name: 'Form',
11+
props: {
12+
// Pass-through Inertia Form props (loosely typed to avoid duplication)
13+
action: { type: [String, Object] as PropType<FormComponentProps['action']>, default: '' },
14+
method: { type: String as PropType<FormComponentProps['method']>, default: 'get' },
15+
16+
// Live validation extras
17+
precognitive: { type: [Boolean, Object] as PropType<boolean | ValidationConfig>, default: true },
18+
validateOn: { type: [String, Array] as PropType<'input' | 'change' | 'blur' | Array<'input' | 'change' | 'blur'>>, default: 'change' },
19+
validationTimeout: { type: Number, default: undefined },
20+
},
21+
setup(props, { slots, attrs }) {
22+
const formRef = ref<any>(null)
23+
let validator: ReturnType<typeof createValidator> | null = null
24+
25+
const getAction = () => (typeof props.action === 'object' ? props.action.url : (props.action as string))
26+
const getMethod = () => ((typeof props.action === 'object' ? props.action.method : props.method).toLowerCase() as Method)
27+
28+
const ensureValidator = (formEl: HTMLFormElement) => {
29+
if (validator) return validator
30+
const initial = formDataToObject(new FormData(formEl)) as Record<string, FormDataConvertible>
31+
validator = createValidator((client) => {
32+
const current = formDataToObject(new FormData(formEl)) as Record<string, FormDataConvertible>
33+
return client[getMethod()](getAction(), current, { precognitive: true })
34+
}, initial)
35+
.on('errorsChanged', () => {
36+
const simple = toSimpleValidationErrors(validator!.errors()) as Record<string, string>
37+
try {
38+
formRef.value?.clearErrors()
39+
formRef.value?.setError(simple)
40+
} catch {}
41+
})
42+
if (typeof props.validationTimeout === 'number') {
43+
validator.setTimeout(props.validationTimeout)
44+
}
45+
return validator
46+
}
47+
48+
const shouldValidateField = (target: EventTarget | null) => {
49+
const el = target as HTMLElement | null
50+
if (!el) return false
51+
return props.precognitive === true || typeof props.precognitive === 'object' || el.hasAttribute?.('precognitive') || el.getAttribute?.('data-precognitive') === 'true'
52+
}
53+
54+
const onMaybeValidate = (e: Event) => {
55+
if (!props.precognitive) return
56+
const evType = e.type as 'input' | 'change' | 'blur'
57+
const types = Array.isArray(props.validateOn) ? props.validateOn : [props.validateOn]
58+
if (!types.includes(evType)) return
59+
60+
const formEl = e.currentTarget as HTMLFormElement | null
61+
if (!formEl || !shouldValidateField(e.target)) return
62+
63+
const v = ensureValidator(formEl)
64+
const baseConfig = (typeof props.precognitive === 'object' ? props.precognitive : {}) as ValidationConfig
65+
66+
const target = e.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null
67+
const name = target ? target.name : undefined
68+
69+
try {
70+
if (name) {
71+
const data = formDataToObject(new FormData(formEl)) as Record<string, any>
72+
v.validate(name, data[name], { ...baseConfig, precognitive: true })
73+
} else {
74+
v.validate({ ...baseConfig, precognitive: true })
75+
}
76+
} catch {}
77+
}
78+
79+
return () =>
80+
h(
81+
InertiaForm as any,
82+
{
83+
...attrs,
84+
ref: formRef,
85+
action: getAction(),
86+
method: getMethod(),
87+
onInput: onMaybeValidate,
88+
onChange: onMaybeValidate,
89+
onBlur: onMaybeValidate,
90+
},
91+
{
92+
default: slots.default
93+
? (slotProps: FormComponentSlotProps) =>
94+
slots.default?.({ ...slotProps, validating: validator?.validating() ?? false })
95+
: undefined,
96+
},
97+
)
98+
},
99+
})

packages/vue-inertia/src/index.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
import { NamedInputEvent, RequestMethod, SimpleValidationErrors, toSimpleValidationErrors, ValidationConfig, ValidationErrors, resolveUrl, resolveMethod } from 'laravel-precognition'
1+
import { NamedInputEvent, SimpleValidationErrors, toSimpleValidationErrors, ValidationConfig, ValidationErrors, resolveUrl, resolveMethod } from 'laravel-precognition'
22
import { useForm as usePrecognitiveForm, client } from 'laravel-precognition-vue'
33
import { useForm as useInertiaForm } from '@inertiajs/vue3'
4-
import { VisitOptions } from '@inertiajs/core'
4+
import { VisitOptions, Method, FormDataKeys, FormDataType, type FormDataErrors, type ErrorValue } from '@inertiajs/core'
55
import { watchEffect } from 'vue'
6-
import { Form, FormDataConvertible } from './types'
6+
import type { Form, FormDataConvertible } from './types'
77

8-
export { client, Form }
8+
export { client }
9+
export type { Form }
10+
export { Form as PrecognitionForm } from "./form";
911

10-
export const useForm = <Data extends Record<string, FormDataConvertible>>(method: RequestMethod | (() => RequestMethod), url: string | (() => string), inputs: Data | (() => Data), config: ValidationConfig = {}): Form<Data> => {
12+
export const useForm = <Data extends FormDataType<Data>>(method: Method | (() => Method), url: string | (() => string), inputs: Data | (() => Data), config: ValidationConfig = {}): Form<Data> => {
1113
/**
1214
* The Inertia form.
1315
*/
@@ -66,7 +68,7 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
6668
const form: Form<Data> = Object.assign(inertiaForm, {
6769
validating: precognitiveForm.validating,
6870
touched: precognitiveForm.touched,
69-
touch(name: Array<string> | string | NamedInputEvent) {
71+
touch(name: Array<FormDataKeys<Data>> | FormDataKeys<Data> | NamedInputEvent) {
7072
precognitiveForm.touch(name)
7173

7274
return form
@@ -81,34 +83,63 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
8183

8284
return form
8385
},
84-
clearErrors(...names: string[]) {
86+
clearErrors<K extends FormDataKeys<Data>>(...names: K[]) {
8587
inertiaClearErrors(...names)
8688

8789
if (names.length === 0) {
8890
precognitiveForm.setErrors({})
8991
} else {
90-
names.forEach(precognitiveForm.forgetError)
92+
names.forEach((n) =>
93+
precognitiveForm.forgetError(
94+
String(n) as unknown as keyof Data,
95+
),
96+
)
9197
}
9298

9399
return form
94100
},
95-
reset(...names: string[]) {
101+
reset<K extends FormDataKeys<Data>>(...names: K[]) {
96102
inertiaReset(...names)
97103

98-
precognitiveForm.reset(...names)
104+
if (names.length === 0) {
105+
precognitiveForm.reset();
106+
} else {
107+
const str = names.map((n) => String(n));
108+
precognitiveForm.reset(
109+
...(str as unknown as Array<keyof Data>),
110+
);
111+
}
112+
113+
return form;
99114
},
100115
setErrors(errors: SimpleValidationErrors | ValidationErrors) {
101-
// @ts-expect-error
102-
precognitiveForm.setErrors(errors)
116+
const anyErr = errors as unknown as Record<string, unknown>
117+
const firstVal = anyErr ? Object.values(anyErr)[0] : undefined
118+
const simple: SimpleValidationErrors =
119+
typeof firstVal === "string"
120+
? (errors as SimpleValidationErrors)
121+
: toSimpleValidationErrors(errors as ValidationErrors)
122+
123+
precognitiveForm.setErrors(
124+
simple as unknown as Partial<
125+
Record<keyof Data, string | string[]>
126+
>,
127+
)
103128

104129
return form
105130
},
106-
forgetError(name: string | NamedInputEvent) {
107-
precognitiveForm.forgetError(name)
131+
forgetError(name: FormDataKeys<Data> | NamedInputEvent) {
132+
if (typeof name === "object" && "target" in name) {
133+
precognitiveForm.forgetError(name)
134+
} else {
135+
precognitiveForm.forgetError(
136+
String(name) as unknown as keyof Data,
137+
)
138+
}
108139

109140
return form
110141
},
111-
setError(key: (keyof Data) | Record<keyof Data, string>, value?: string) {
142+
setError(key: FormDataKeys<Data> | FormDataErrors<Data>, value?: ErrorValue) {
112143
let errors: SimpleValidationErrors
113144

114145
if (typeof key !== 'object') {
@@ -135,7 +166,7 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
135166

136167
return form
137168
},
138-
validate(name?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) {
169+
validate(name?: FormDataKeys<Data> | NamedInputEvent | ValidationConfig, config?: ValidationConfig) {
139170
precognitiveForm.setData(transformer(inertiaForm.data()))
140171

141172
if (typeof name === 'object' && !('target' in name)) {
@@ -150,8 +181,13 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
150181

151182
if (typeof name === 'undefined') {
152183
precognitiveForm.validate(config)
153-
} else {
184+
} else if (typeof name === "object" && "target" in name) {
154185
precognitiveForm.validate(name, config)
186+
} else {
187+
precognitiveForm.validate(
188+
String(name) as unknown as keyof Data,
189+
config,
190+
)
155191
}
156192

157193
return form
@@ -166,7 +202,7 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
166202

167203
return form
168204
},
169-
submit(submitMethod: RequestMethod | Partial<VisitOptions> = {}, submitUrl?: string, submitOptions?: Partial<VisitOptions>): void {
205+
submit(submitMethod: Method | Partial<VisitOptions> = {}, submitUrl?: string, submitOptions?: Partial<VisitOptions>): void {
170206
if (typeof submitMethod !== 'string') {
171207
submitOptions = submitMethod
172208
submitUrl = resolveUrl(url)

packages/vue-inertia/src/types.ts

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
1-
import { NamedInputEvent, RequestMethod, SimpleValidationErrors, ValidationConfig, ValidationErrors } from 'laravel-precognition'
2-
import { Form as PrecognitiveForm } from 'laravel-precognition-vue/dist/types'
3-
import { InertiaForm } from '@inertiajs/vue3'
4-
import { VisitOptions } from '@inertiajs/core'
1+
import { NamedInputEvent, SimpleValidationErrors, ValidationConfig, ValidationErrors } from 'laravel-precognition';
2+
import { Form as PrecognitiveForm } from 'laravel-precognition-vue/dist/types';
3+
import { InertiaForm } from '@inertiajs/vue3';
4+
import { VisitOptions, FormDataErrors, FormDataKeys, Method } from '@inertiajs/core';
55

6-
type RedefinedProperties = 'setErrors' | 'touch' | 'forgetError' | 'setValidationTimeout' | 'submit' | 'reset' | 'validateFiles' | 'setData' | 'validate'
6+
type RedefinedProperties = 'setErrors' | 'touch' | 'forgetError' | 'setValidationTimeout' | 'submit' | 'reset' | 'validateFiles' | 'setData' | 'validate' | 'errors';
77

88
export type Form<Data extends Record<string, FormDataConvertible>> = Omit<PrecognitiveForm<Data>, RedefinedProperties> & InertiaForm<Data> & {
9-
setErrors(errors: SimpleValidationErrors | ValidationErrors): Data & Form<Data>,
10-
touch(name: Array<string> | string | NamedInputEvent): Data & Form<Data>,
11-
forgetError(string: keyof Data | NamedInputEvent): Data & Form<Data>,
12-
setValidationTimeout(duration: number): Data & Form<Data>,
13-
submit(config?: Partial<VisitOptions>): void,
14-
submit(method: RequestMethod, url: string, options?: Partial<VisitOptions>): void,
15-
reset(...keys: (keyof Partial<Data>)[]): Data & Form<Data>,
16-
validateFiles(): Data & Form<Data>,
17-
setData(data: Record<string, FormDataConvertible>): Data & Form<Data>,
18-
validate(name?: (keyof Data | NamedInputEvent) | ValidationConfig, config?: ValidationConfig): Data & Form<Data>,
19-
}
9+
errors: FormDataErrors<Data>;
10+
setErrors(errors: SimpleValidationErrors | ValidationErrors): Data & Form<Data>;
11+
touch(name: Array<FormDataKeys<Data>> | FormDataKeys<Data> | NamedInputEvent): Data & Form<Data>;
12+
forgetError(string: FormDataKeys<Data> | NamedInputEvent): Data & Form<Data>;
13+
setValidationTimeout(duration: number): Data & Form<Data>;
14+
submit(config?: Partial<VisitOptions>): void;
15+
submit(method: Method, url: string, options?: Partial<VisitOptions>): void;
16+
reset(...keys: FormDataKeys<Data>[]): Data & Form<Data>;
17+
validateFiles(): Data & Form<Data>;
18+
setData(data: Record<string, FormDataConvertible>): Data & Form<Data>;
19+
validate(name?: (FormDataKeys<Data> | NamedInputEvent) | ValidationConfig, config?: ValidationConfig): Data & Form<Data>;
20+
};
2021

2122
// This type has been duplicated from @inertiajs/core to
2223
// continue supporting Inertia 1. When we drop version 1

0 commit comments

Comments
 (0)