From 69a0f03324c55ad20d3180c17d2aaef946aa828a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Lopes?= Date: Mon, 3 Feb 2025 14:13:32 -0300 Subject: [PATCH] feat(form): adding support for nested paths --- packages/core/src/types.ts | 4 +++ packages/react/package.json | 2 +- packages/react/src/useForm.ts | 56 +++++++++++++++++----------------- packages/svelte/src/useForm.ts | 51 +++++++++++++++---------------- packages/vue3/package.json | 3 +- packages/vue3/src/useForm.ts | 45 +++++++++++++-------------- 6 files changed, 80 insertions(+), 81 deletions(-) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8537b269b..04cfb6c68 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -21,6 +21,10 @@ export type FormDataConvertible = | null | undefined +export type FormDataKeys = { + [K in keyof T & string]: T[K] extends object ? (T[K] extends Array ? K : `${K}.${FormDataKeys}` | K) : K +}[keyof T & string] + export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete' export type RequestPayload = Record | FormData diff --git a/packages/react/package.json b/packages/react/package.json index 6ea2657d6..5bb059e81 100755 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -60,6 +60,6 @@ }, "dependencies": { "@inertiajs/core": "2.0.3", - "lodash.isequal": "^4.5.0" + "lodash": "^4.5.0" } } diff --git a/packages/react/src/useForm.ts b/packages/react/src/useForm.ts index 0e0188e60..689a00129 100644 --- a/packages/react/src/useForm.ts +++ b/packages/react/src/useForm.ts @@ -1,18 +1,18 @@ -import { FormDataConvertible, Method, Progress, router, VisitOptions } from '@inertiajs/core' -import isEqual from 'lodash.isequal' +import { FormDataConvertible, FormDataKeys, Method, Progress, router, VisitOptions } from '@inertiajs/core' +import { cloneDeep, get, has, isEqual, set } from 'lodash' import { useCallback, useEffect, useRef, useState } from 'react' import useRemember from './useRemember' type setDataByObject = (data: TForm) => void type setDataByMethod = (data: (previousData: TForm) => TForm) => void -type setDataByKeyValuePair = (key: K, value: TForm[K]) => void +type setDataByKeyValuePair = (key: FormDataKeys, value: any) => void type FormDataType = Record type FormOptions = Omit export interface InertiaFormProps { data: TForm isDirty: boolean - errors: Partial> + errors: Partial, string>> hasErrors: boolean processing: boolean progress: Progress | null @@ -21,12 +21,12 @@ export interface InertiaFormProps { setData: setDataByObject & setDataByMethod & setDataByKeyValuePair transform: (callback: (data: TForm) => object) => void setDefaults(): void - setDefaults(field: keyof TForm, value: FormDataConvertible): void + setDefaults(field: FormDataKeys, value: FormDataConvertible): void setDefaults(fields: Partial): void - reset: (...fields: (keyof TForm)[]) => void - clearErrors: (...fields: (keyof TForm)[]) => void - setError(field: keyof TForm, value: string): void - setError(errors: Record): void + reset: (...fields: FormDataKeys[]) => void + clearErrors: (...fields: FormDataKeys[]) => void + setError(field: FormDataKeys, value: string): void + setError(errors: Record, string>): void submit: (method: Method, url: string, options?: FormOptions) => void get: (url: string, options?: FormOptions) => void patch: (url: string, options?: FormOptions) => void @@ -53,8 +53,8 @@ export default function useForm( const recentlySuccessfulTimeoutId = useRef(null) const [data, setData] = rememberKey ? useRemember(defaults, `${rememberKey}:data`) : useState(defaults) const [errors, setErrors] = rememberKey - ? useRemember({} as Partial>, `${rememberKey}:errors`) - : useState({} as Partial>) + ? useRemember({} as Partial, string>>, `${rememberKey}:errors`) + : useState({} as Partial, string>>) const [hasErrors, setHasErrors] = useState(false) const [processing, setProcessing] = useState(false) const [progress, setProgress] = useState(null) @@ -168,9 +168,9 @@ export default function useForm( ) const setDataFunction = useCallback( - (keyOrData: keyof TForm | Function | TForm, maybeValue?: TForm[keyof TForm]) => { + (keyOrData: FormDataKeys | Function | TForm, maybeValue?: any) => { if (typeof keyOrData === 'string') { - setData((data) => ({ ...data, [keyOrData]: maybeValue })) + setData((data) => set(cloneDeep(data), keyOrData, maybeValue)) } else if (typeof keyOrData === 'function') { setData((data) => keyOrData(data)) } else { @@ -181,14 +181,15 @@ export default function useForm( ) const setDefaultsFunction = useCallback( - (fieldOrFields?: keyof TForm | Partial, maybeValue?: FormDataConvertible) => { + (fieldOrFields?: FormDataKeys | Partial, maybeValue?: FormDataConvertible) => { if (typeof fieldOrFields === 'undefined') { setDefaults(() => data) } else { - setDefaults((defaults) => ({ - ...defaults, - ...(typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : (fieldOrFields as TForm)), - })) + setDefaults((defaults) => { + return typeof fieldOrFields === 'string' + ? set(cloneDeep(defaults), fieldOrFields, maybeValue) + : Object.assign(cloneDeep(defaults), fieldOrFields) + }) } }, [data, setDefaults], @@ -200,14 +201,13 @@ export default function useForm( setData(defaults) } else { setData((data) => - (Object.keys(defaults) as Array) - .filter((key) => fields.includes(key)) + (fields as Array>) + .filter((key) => has(defaults, key)) .reduce( (carry, key) => { - carry[key] = defaults[key] - return carry + return set(carry, key, get(defaults, key)) }, - { ...data }, + { ...data } as TForm, ), ) } @@ -216,13 +216,13 @@ export default function useForm( ) const setError = useCallback( - (fieldOrFields: keyof TForm | Record, maybeValue?: string) => { + (fieldOrFields: FormDataKeys | Record, string>, maybeValue?: string) => { setErrors((errors) => { const newErrors = { ...errors, ...(typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } - : (fieldOrFields as Record)), + : (fieldOrFields as Record, string>)), } setHasErrors(Object.keys(newErrors).length > 0) return newErrors @@ -234,7 +234,7 @@ export default function useForm( const clearErrors = useCallback( (...fields) => { setErrors((errors) => { - const newErrors = (Object.keys(errors) as Array).reduce( + const newErrors = (Object.keys(errors) as Array>).reduce( (carry, field) => ({ ...carry, ...(fields.length > 0 && !fields.includes(field) ? { [field]: errors[field] } : {}), @@ -251,7 +251,7 @@ export default function useForm( const createSubmitMethod = (method) => (url, options) => { submit(method, url, options) } - const get = useCallback(createSubmitMethod('get'), [submit]) + const getMethod = useCallback(createSubmitMethod('get'), [submit]) const post = useCallback(createSubmitMethod('post'), [submit]) const put = useCallback(createSubmitMethod('put'), [submit]) const patch = useCallback(createSubmitMethod('patch'), [submit]) @@ -283,7 +283,7 @@ export default function useForm( setError, clearErrors, submit, - get, + get: getMethod, post, put, patch, diff --git a/packages/svelte/src/useForm.ts b/packages/svelte/src/useForm.ts index 8cbd4c715..2d9995487 100644 --- a/packages/svelte/src/useForm.ts +++ b/packages/svelte/src/useForm.ts @@ -2,6 +2,7 @@ import type { ActiveVisit, Errors, FormDataConvertible, + FormDataKeys, Method, Page, PendingVisit, @@ -11,8 +12,7 @@ import type { } from '@inertiajs/core' import { router } from '@inertiajs/core' import type { AxiosProgressEvent } from 'axios' -import cloneDeep from 'lodash/cloneDeep' -import isEqual from 'lodash/isEqual' +import { cloneDeep, get, has, isEqual, set } from 'lodash' import { writable, type Writable } from 'svelte/store' type FormDataType = Record @@ -20,22 +20,22 @@ type FormOptions = Omit export interface InertiaFormProps { isDirty: boolean - errors: Partial> + errors: Partial, string>> hasErrors: boolean progress: Progress | null wasSuccessful: boolean recentlySuccessful: boolean processing: boolean setStore(data: TForm): void - setStore(key: keyof TForm, value?: FormDataConvertible): void + setStore(key: FormDataKeys, value?: FormDataConvertible): void data(): TForm transform(callback: (data: TForm) => object): this defaults(): this defaults(fields: Partial): this - defaults(field?: keyof TForm, value?: FormDataConvertible): this - reset(...fields: (keyof TForm)[]): this - clearErrors(...fields: (keyof TForm)[]): this - setError(field: keyof TForm, value: string): this + defaults(field?: FormDataKeys, value?: FormDataConvertible): this + reset(...fields: FormDataKeys[]): this + clearErrors(...fields: FormDataKeys[]): this + setError(field: FormDataKeys, value: string): this setError(errors: Errors): this submit(method: Method, url: string, options?: FormOptions): void get(url: string, options?: FormOptions): void @@ -61,7 +61,7 @@ export default function useForm( const inputData = (typeof rememberKeyOrData === 'string' ? maybeData : rememberKeyOrData) ?? {} const data: TForm = typeof inputData === 'function' ? inputData() : (inputData as TForm) const restored = rememberKey - ? (router.restore(rememberKey) as { data: TForm; errors: Record } | null) + ? (router.restore(rememberKey) as { data: TForm; errors: Record, string> } | null) : null let defaults = cloneDeep(data) let cancelToken: { cancel: () => void } | null = null @@ -79,27 +79,27 @@ export default function useForm( processing: false, setStore(keyOrData, maybeValue = undefined) { store.update((store) => { - return Object.assign(store, typeof keyOrData === 'string' ? { [keyOrData]: maybeValue } : keyOrData) + return typeof keyOrData === 'string' ? set(store, keyOrData, maybeValue) : Object.assign(store, keyOrData) }) }, data() { return Object.keys(data).reduce((carry, key) => { - carry[key] = this[key] - return carry + return set(carry, key, get(this, key)) }, {} as FormDataType) as TForm }, transform(callback) { transform = callback return this }, - defaults(fieldOrFields?: keyof TForm | Partial, maybeValue?: FormDataConvertible) { - defaults = - typeof fieldOrFields === 'undefined' - ? cloneDeep(this.data()) - : Object.assign( - cloneDeep(defaults), - typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields, - ) + defaults(fieldOrFields?: FormDataKeys | Partial, maybeValue?: FormDataConvertible) { + if (typeof fieldOrFields === 'undefined') { + defaults = cloneDeep(this.data()) + } else { + defaults = + typeof fieldOrFields === 'string' + ? set(cloneDeep(defaults), fieldOrFields, maybeValue) + : Object.assign(cloneDeep(defaults), fieldOrFields) + } return this }, @@ -109,18 +109,17 @@ export default function useForm( this.setStore(clonedData) } else { this.setStore( - Object.keys(clonedData) - .filter((key) => fields.includes(key)) + (fields as Array>) + .filter((key) => has(clonedData, key)) .reduce((carry, key) => { - carry[key] = clonedData[key] - return carry + return set(carry, key, get(clonedData, key)) }, {} as FormDataType) as TForm, ) } return this }, - setError(fieldOrFields: keyof TForm | Errors, maybeValue?: string) { + setError(fieldOrFields: FormDataKeys | Errors, maybeValue?: string) { this.setStore('errors', { ...this.errors, ...((typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields) as Errors), @@ -131,7 +130,7 @@ export default function useForm( clearErrors(...fields) { this.setStore( 'errors', - Object.keys(this.errors).reduce( + (Object.keys(this.errors) as FormDataKeys[]).reduce( (carry, field) => ({ ...carry, ...(fields.length > 0 && !fields.includes(field) ? { [field]: this.errors[field] } : {}), diff --git a/packages/vue3/package.json b/packages/vue3/package.json index c5c835b7d..253aec5d5 100755 --- a/packages/vue3/package.json +++ b/packages/vue3/package.json @@ -59,7 +59,6 @@ }, "dependencies": { "@inertiajs/core": "2.0.3", - "lodash.clonedeep": "^4.5.0", - "lodash.isequal": "^4.5.0" + "lodash": "^4.5.0" } } diff --git a/packages/vue3/src/useForm.ts b/packages/vue3/src/useForm.ts index 786cbeadb..01ef6cf70 100644 --- a/packages/vue3/src/useForm.ts +++ b/packages/vue3/src/useForm.ts @@ -1,6 +1,5 @@ -import { FormDataConvertible, Method, Progress, router, VisitOptions } from '@inertiajs/core' -import cloneDeep from 'lodash.clonedeep' -import isEqual from 'lodash.isequal' +import { FormDataConvertible, FormDataKeys, Method, Progress, router, VisitOptions } from '@inertiajs/core' +import { cloneDeep, get, has, isEqual, set } from 'lodash' import { reactive, watch } from 'vue' type FormDataType = Record @@ -8,7 +7,7 @@ type FormOptions = Omit export interface InertiaFormProps { isDirty: boolean - errors: Partial> + errors: Partial, string>> hasErrors: boolean processing: boolean progress: Progress | null @@ -17,12 +16,12 @@ export interface InertiaFormProps { data(): TForm transform(callback: (data: TForm) => object): this defaults(): this - defaults(field: keyof TForm, value: FormDataConvertible): this + defaults(field: FormDataKeys, value: FormDataConvertible): this defaults(fields: Partial): this - reset(...fields: (keyof TForm)[]): this - clearErrors(...fields: (keyof TForm)[]): this - setError(field: keyof TForm, value: string): this - setError(errors: Record): this + reset(...fields: FormDataKeys[]): this + clearErrors(...fields: FormDataKeys[]): this + setError(field: FormDataKeys, value: string): this + setError(errors: Record, string>): this submit(method: Method, url: string, options?: FormOptions): void get(url: string, options?: FormOptions): void post(url: string, options?: FormOptions): void @@ -46,7 +45,7 @@ export default function useForm( const rememberKey = typeof rememberKeyOrData === 'string' ? rememberKeyOrData : null const data = (typeof rememberKeyOrData === 'string' ? maybeData : rememberKeyOrData) ?? {} const restored = rememberKey - ? (router.restore(rememberKey) as { data: TForm; errors: Record }) + ? (router.restore(rememberKey) as { data: TForm; errors: Record, string> }) : null let defaults = typeof data === 'function' ? cloneDeep(data()) : cloneDeep(data) let cancelToken = null @@ -63,9 +62,8 @@ export default function useForm( wasSuccessful: false, recentlySuccessful: false, data() { - return (Object.keys(defaults) as Array).reduce((carry, key) => { - carry[key] = this[key] - return carry + return (Object.keys(defaults) as Array>).reduce((carry, key) => { + return set(carry, key, get(this, key)) }, {} as Partial) as TForm }, transform(callback) { @@ -73,7 +71,7 @@ export default function useForm( return this }, - defaults(fieldOrFields?: keyof TForm | Partial, maybeValue?: FormDataConvertible) { + defaults(fieldOrFields?: FormDataKeys | Partial, maybeValue?: FormDataConvertible) { if (typeof data === 'function') { throw new Error('You cannot call `defaults()` when using a function to define your form data.') } @@ -82,11 +80,10 @@ export default function useForm( defaults = this.data() this.isDirty = false } else { - defaults = Object.assign( - {}, - cloneDeep(defaults), - typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields, - ) + defaults = + typeof fieldOrFields === 'string' + ? set(cloneDeep(defaults), fieldOrFields, maybeValue) + : Object.assign({}, cloneDeep(defaults), fieldOrFields) } return this @@ -98,17 +95,17 @@ export default function useForm( defaults = clonedData Object.assign(this, resolvedData) } else { - Object.keys(resolvedData) - .filter((key) => fields.includes(key)) + (fields as Array>) + .filter((key) => has(clonedData, key)) .forEach((key) => { - defaults[key] = clonedData[key] - this[key] = resolvedData[key] + set(defaults, key, get(clonedData, key)) + set(this, key, get(resolvedData, key)) }) } return this }, - setError(fieldOrFields: keyof TForm | Record, maybeValue?: string) { + setError(fieldOrFields: FormDataKeys | Record, string>, maybeValue?: string) { Object.assign(this.errors, typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields) this.hasErrors = Object.keys(this.errors).length > 0