Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions packages/vue-inertia/src/form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { defineComponent, h, PropType, ref } from 'vue'
import { Form as InertiaForm } from '@inertiajs/vue3'
import type { FormComponentProps, FormComponentSlotProps, FormDataConvertible } from '@inertiajs/core'
import { formDataToObject, Method } from '@inertiajs/core'
import type { ValidationConfig } from 'laravel-precognition'
import { createValidator, toSimpleValidationErrors } from 'laravel-precognition'

// Thin wrapper over Inertia's <Form> that wires Precognition live validation
export const Form = defineComponent({
name: 'Form',
props: {
// Pass-through Inertia Form props (loosely typed to avoid duplication)
action: { type: [String, Object] as PropType<FormComponentProps['action']>, default: '' },
method: { type: String as PropType<FormComponentProps['method']>, default: 'get' },

// Live validation extras
precognitive: { type: [Boolean, Object] as PropType<boolean | ValidationConfig>, default: true },
validateOn: { type: [String, Array] as PropType<'input' | 'change' | 'blur' | Array<'input' | 'change' | 'blur'>>, default: 'change' },
validationTimeout: { type: Number, default: undefined },
},
setup(props, { slots, attrs }) {
const formRef = ref<any>(null)
let validator: ReturnType<typeof createValidator> | null = null

const getAction = () => (typeof props.action === 'object' ? props.action.url : (props.action as string))
const getMethod = () => ((typeof props.action === 'object' ? props.action.method : props.method).toLowerCase() as Method)

const ensureValidator = (formEl: HTMLFormElement) => {
if (validator) return validator
const initial = formDataToObject(new FormData(formEl)) as Record<string, FormDataConvertible>
validator = createValidator((client) => {
const current = formDataToObject(new FormData(formEl)) as Record<string, FormDataConvertible>
return client[getMethod()](getAction(), current, { precognitive: true })
}, initial)
.on('errorsChanged', () => {
const simple = toSimpleValidationErrors(validator!.errors()) as Record<string, string>
try {
formRef.value?.clearErrors()
formRef.value?.setError(simple)
} catch {}
})
if (typeof props.validationTimeout === 'number') {
validator.setTimeout(props.validationTimeout)
}
return validator
}

const shouldValidateField = (target: EventTarget | null) => {
const el = target as HTMLElement | null
if (!el) return false
return (
props.precognitive === true ||
typeof props.precognitive === 'object' ||
el.hasAttribute?.('precognitive') ||
el.hasAttribute?.('data-precognitive') ||
el.getAttribute?.('data-precognitive') === 'true'
)
}

const onMaybeValidate = (e: Event) => {
const evType = e.type as 'input' | 'change' | 'blur'
const types = Array.isArray(props.validateOn) ? props.validateOn : [props.validateOn]
if (!types.includes(evType)) return

const formEl = e.currentTarget as HTMLFormElement | null
if (!formEl || !shouldValidateField(e.target)) return

const v = ensureValidator(formEl)
const baseConfig = (typeof props.precognitive === 'object' ? props.precognitive : {}) as ValidationConfig

const target = e.target as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null
const name = target ? target.name : undefined

try {
if (name) {
const data = formDataToObject(new FormData(formEl)) as Record<string, any>
v.validate(name, data[name], { ...baseConfig, precognitive: true })
} else {
v.validate({ ...baseConfig, precognitive: true })
}
} catch {}
}

return () =>
h(
InertiaForm as any,
{
...attrs,
ref: formRef,
action: getAction(),
method: getMethod(),
onInput: onMaybeValidate,
onChange: onMaybeValidate,
onBlur: onMaybeValidate,
},
{
default: slots.default
? (slotProps: FormComponentSlotProps) =>
slots.default?.({ ...slotProps, validating: validator?.validating() ?? false })
: undefined,
},
)
},
})
68 changes: 52 additions & 16 deletions packages/vue-inertia/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { NamedInputEvent, RequestMethod, SimpleValidationErrors, toSimpleValidationErrors, ValidationConfig, ValidationErrors, resolveUrl, resolveMethod } from 'laravel-precognition'
import { useForm as usePrecognitiveForm, client } from 'laravel-precognition-vue'
import { useForm as useInertiaForm } from '@inertiajs/vue3'
import { VisitOptions } from '@inertiajs/core'
import type { VisitOptions, FormDataKeys, FormDataType, FormDataErrors, ErrorValue } from '@inertiajs/core'
import { watchEffect } from 'vue'
import { Form, FormDataConvertible } from './types'
import type { Form, FormDataConvertible } from './types'

export { client, Form }
export { client }
export type { Form }
export { Form as PrecognitionForm } from "./form";

export const useForm = <Data extends Record<string, FormDataConvertible>>(method: RequestMethod | (() => RequestMethod), url: string | (() => string), inputs: Data | (() => Data), config: ValidationConfig = {}): Form<Data> => {
export const useForm = <Data extends FormDataType<Data>>(method: RequestMethod | (() => RequestMethod), url: string | (() => string), inputs: Data | (() => Data), config: ValidationConfig = {}): Form<Data> => {
/**
* The Inertia form.
*/
Expand Down Expand Up @@ -66,7 +68,7 @@ export const useForm = <Data extends Record<string, FormDataConvertible>>(method
const form: Form<Data> = Object.assign(inertiaForm, {
validating: precognitiveForm.validating,
touched: precognitiveForm.touched,
touch(name: Array<string> | string | NamedInputEvent) {
touch(name: Array<FormDataKeys<Data>> | FormDataKeys<Data> | NamedInputEvent) {
precognitiveForm.touch(name)

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

return form
},
clearErrors(...names: string[]) {
clearErrors<K extends FormDataKeys<Data>>(...names: K[]) {
inertiaClearErrors(...names)

if (names.length === 0) {
precognitiveForm.setErrors({})
} else {
names.forEach(precognitiveForm.forgetError)
names.forEach((n) =>
precognitiveForm.forgetError(
String(n) as unknown as keyof Data,
),
)
}

return form
},
reset(...names: string[]) {
reset<K extends FormDataKeys<Data>>(...names: K[]) {
inertiaReset(...names)

precognitiveForm.reset(...names)
if (names.length === 0) {
precognitiveForm.reset();
} else {
const str = names.map((n) => String(n));
precognitiveForm.reset(
...(str as unknown as Array<keyof Data>),
);
}

return form;
},
setErrors(errors: SimpleValidationErrors | ValidationErrors) {
// @ts-expect-error
precognitiveForm.setErrors(errors)
const anyErr = errors as unknown as Record<string, unknown>
const firstVal = anyErr ? Object.values(anyErr)[0] : undefined
const simple: SimpleValidationErrors =
typeof firstVal === "string"
? (errors as SimpleValidationErrors)
: toSimpleValidationErrors(errors as ValidationErrors)

precognitiveForm.setErrors(
simple as unknown as Partial<
Record<keyof Data, string | string[]>
>,
)

return form
},
forgetError(name: string | NamedInputEvent) {
precognitiveForm.forgetError(name)
forgetError(name: FormDataKeys<Data> | NamedInputEvent) {
if (typeof name === "object" && "target" in name) {
precognitiveForm.forgetError(name)
} else {
precognitiveForm.forgetError(
String(name) as unknown as keyof Data,
)
}

return form
},
setError(key: (keyof Data) | Record<keyof Data, string>, value?: string) {
setError(key: FormDataKeys<Data> | FormDataErrors<Data>, value?: ErrorValue) {
let errors: SimpleValidationErrors

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

return form
},
validate(name?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) {
validate(name?: FormDataKeys<Data> | NamedInputEvent | ValidationConfig, config?: ValidationConfig) {
precognitiveForm.setData(transformer(inertiaForm.data()))

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

if (typeof name === 'undefined') {
precognitiveForm.validate(config)
} else {
} else if (typeof name === "object" && "target" in name) {
precognitiveForm.validate(name, config)
} else {
precognitiveForm.validate(
String(name) as unknown as keyof Data,
config,
)
}

return form
Expand Down
13 changes: 7 additions & 6 deletions packages/vue-inertia/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { NamedInputEvent, RequestMethod, SimpleValidationErrors, ValidationConfig, ValidationErrors } from 'laravel-precognition'
import { Form as PrecognitiveForm } from 'laravel-precognition-vue/dist/types'
import { InertiaForm } from '@inertiajs/vue3'
import { VisitOptions } from '@inertiajs/core'
import { VisitOptions, FormDataErrors, FormDataKeys } from '@inertiajs/core'

type RedefinedProperties = 'setErrors' | 'touch' | 'forgetError' | 'setValidationTimeout' | 'submit' | 'reset' | 'validateFiles' | 'setData' | 'validate'
type RedefinedProperties = 'setErrors' | 'touch' | 'forgetError' | 'setValidationTimeout' | 'submit' | 'reset' | 'validateFiles' | 'setData' | 'validate' | 'errors'

export type Form<Data extends Record<string, FormDataConvertible>> = Omit<PrecognitiveForm<Data>, RedefinedProperties> & InertiaForm<Data> & {
errors: FormDataErrors<Data>,
setErrors(errors: SimpleValidationErrors | ValidationErrors): Data & Form<Data>,
touch(name: Array<string> | string | NamedInputEvent): Data & Form<Data>,
forgetError(string: keyof Data | NamedInputEvent): Data & Form<Data>,
touch(name: Array<FormDataKeys<Data>> | FormDataKeys<Data> | NamedInputEvent): Data & Form<Data>,
forgetError(string: FormDataKeys<Data> | NamedInputEvent): Data & Form<Data>,
setValidationTimeout(duration: number): Data & Form<Data>,
submit(config?: Partial<VisitOptions>): void,
submit(method: RequestMethod, url: string, options?: Partial<VisitOptions>): void,
reset(...keys: (keyof Partial<Data>)[]): Data & Form<Data>,
reset(...keys: FormDataKeys<Data>[]): Data & Form<Data>,
validateFiles(): Data & Form<Data>,
setData(data: Record<string, FormDataConvertible>): Data & Form<Data>,
validate(name?: (keyof Data | NamedInputEvent) | ValidationConfig, config?: ValidationConfig): Data & Form<Data>,
validate(name?: (FormDataKeys<Data> | NamedInputEvent) | ValidationConfig, config?: ValidationConfig): Data & Form<Data>,
}

// This type has been duplicated from @inertiajs/core to
Expand Down