diff --git a/features/cv-page/forms/useResumeDataForm.ts b/features/cv-page/forms/useResumeDataForm.ts index 1802fddf..01c3fd06 100644 --- a/features/cv-page/forms/useResumeDataForm.ts +++ b/features/cv-page/forms/useResumeDataForm.ts @@ -1,12 +1,18 @@ import { useFormState } from 'aetherspace/forms/useFormState' -import { ResumeData } from 'cv-page/schemas/ResumeData' +import { GetResumeDataByUserSlugDataBridge } from '../schemas/GetResumeDataByUserSlugDataBridge' +import { DeepPartial } from 'aetherspace/types/typeHelpers' + +/* --- Types ----------------------------------------------------------------------------------- */ + +type InitialFormData = DeepPartial<(typeof GetResumeDataByUserSlugDataBridge)['RES']> /* --- useResumeDataForm() --------------------------------------------------------------------- */ -const useResumeDataForm = (initialValues) => { +const useResumeDataForm = (initialValues: InitialFormData) => { const formState = useFormState({ - stateSchema: ResumeData, + stateSchema: GetResumeDataByUserSlugDataBridge.responseSchema, initialValues, + applyDefaults: true, }) return formState } diff --git a/features/cv-page/screens/UpdateResumeScreen.tsx b/features/cv-page/screens/UpdateResumeScreen.tsx index 50e98289..d04b1955 100644 --- a/features/cv-page/screens/UpdateResumeScreen.tsx +++ b/features/cv-page/screens/UpdateResumeScreen.tsx @@ -1,11 +1,12 @@ import React from 'react' import { useAetherRoute } from 'aetherspace/navigation' import { z, AetherProps, createDataBridge } from 'aetherspace/schemas' -import { GetResumeDataByUserSlugDataBridge } from '..//schemas/GetResumeDataByUserSlugDataBridge.ts' +import { GetResumeDataByUserSlugDataBridge } from '../schemas/GetResumeDataByUserSlugDataBridge.ts' import { View } from 'aetherspace/primitives' import { H1, P } from 'aetherspace/html-elements' import useResumeDataForm from '../forms/useResumeDataForm.ts' import { dummyResumeData } from '../mocks/resumeData.mock.ts' +import { isEmpty } from 'aetherspace/utils/commonUtils/commonUtils.ts' /* --- Schemas & Types ------------------------------------------------------------------------- */ @@ -42,9 +43,18 @@ export const UpdateResumeScreen = (props: TUpdateResumeScreenProps) => { // -- Effects -- - React.useEffect(() => { - formState.handleChange('slug', 'codinsonnn') - }, []) + // React.useEffect(() => { + // formState.handleChange('slug', 'codinsonnn') + // }, []) + + // React.useEffect(() => { + // formState.validate() + // }, [formState.getValue('slug')]) + + // React.useEffect(() => { + // const { errors } = formState + // console.log('ResumeScreen', { formState }) + // }, [formState.hasErrors('awards')]) // -- Guards -- @@ -56,6 +66,14 @@ export const UpdateResumeScreen = (props: TUpdateResumeScreenProps) => { ) } + if (!isEmpty(formState.errors)) { + return ( + +

Form error: {JSON.stringify(formState.errors, null, 4)}

+
+ ) + } + // -- Render -- return ( diff --git a/packages/@aetherspace/forms/useFormState.ts b/packages/@aetherspace/forms/useFormState.ts index 4986c0fd..509f575f 100644 --- a/packages/@aetherspace/forms/useFormState.ts +++ b/packages/@aetherspace/forms/useFormState.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/ban-types */ import { useState, useMemo, useEffect } from 'react' +// Types +import { DeepPartial, HintedKeys } from '../types' // Schemas import { z } from '../schemas' // Utils @@ -18,29 +20,53 @@ export type AetherFormInputBaseProps = { } export type AetherFormState, K extends keyof T = keyof T> = { - errors: Record + /** -i- The current values of the form */ values: T + /** -i- A map of all errors in the form */ + errors: Record + /** -i- Whether the form is valid or not */ isValid: boolean + /** -i- Whether the form has been changed from its default state */ isUnsaved: boolean + /** -i- Whether the form is currently the default state */ isDefaultState: boolean + /** -i- Sets the form state to the provided values */ setValues: (values: T) => void + /** -i- Sets a single value in the form state */ handleChange: (key: K, value: T[K]) => void + /** -i- Returns a handler that sets a single value in the form state */ + getChangeHandler: (key: K) => (value: T[K]) => void + /** -i- Validates the form state, sets errors if not, and returns whether it is valid or not */ validate: (showErrors?: boolean) => boolean + /** -i- Updates the form errors */ updateErrors: (errors: Record) => void + /** -i- Resets the form state to its default values */ resetForm: () => void + /** -i- Spreadable function that returns the props to add to an input to hook it up to the form state */ getInputProps: (key: K) => AetherFormInputBaseProps - transformValues: (values: T) => Record + /** -i- Transform the values of the form (simply returns it if no transformer was provided) */ + transformValues: (values: T) => Record + /** -i- Returns a single value from the form state */ getValue: (key: K) => T[K] + /** -i- Returns the errors for a single value from the form state */ getErrors: (key: K) => string[] + /** -i- Returns whether a single value from the form state has errors */ hasErrors: (key: K) => boolean } export type AetherFormStateOptions>> = { + /** -i- Schema to validate the state with *and* provide types from */ stateSchema: z.ZodObject - initialValues?: Record + /** -i- Initial values to set the form state to, should be a partial of the possible formState */ + initialValues?: DeepPartial> + /** -i- Will trigger revalidation once a field loses focus */ validateOnBlur?: boolean + /** -i- Will trigger revalidation once a field changes */ validateOnChange?: boolean - transformValues?: (values: T) => Record + /** -i- Applies state schema defaults to the form state on every change */ + applyDefaults?: boolean + /** -i- If provided, will transform the values before returning them when calling formState.transformValues() */ + transformValues?: (values: T) => Record } /** --- useFormState() ------------------------------------------------------------------------- */ @@ -48,16 +74,22 @@ export type AetherFormStateOptions>>( options: AetherFormStateOptions ): AetherFormState => { - // Options - const { stateSchema, initialValues, validateOnBlur, validateOnChange } = options - const defaultValues = useMemo(() => stateSchema.applyDefaults(initialValues ?? {}) as T, []) - // Types type K = keyof T + // Options + const { stateSchema, initialValues, validateOnBlur, validateOnChange } = options // prettier-ignore + + // Defaults + const defaultValues = useMemo(() => { + if (options.applyDefaults) return initialValues as T + return stateSchema.applyDefaults(initialValues ?? {}) as T + }, []) + // State const [values, setValues] = useState(defaultValues) const [errors, updateErrors] = useState>({} as Record) + const valuesToReturn = options.applyDefaults ? stateSchema.applyDefaults(values) : values // -- Memos -- @@ -75,16 +107,25 @@ export const useFormState = !isEmpty(errors[key]) - const transformValues = () => (options.transformValues?.(values) ?? (values as Record)) // prettier-ignore + const transformValues = () => { + const transformedValues = options.transformValues?.(valuesToReturn) + return transformedValues ?? (valuesToReturn as Record) + } // -- Validation -- const validate = (showErrors = true) => { - const validationResult = stateSchema.safeParse(values) // @ts-ignore + const validationResult = stateSchema.safeParse(valuesToReturn) // @ts-ignore const validationError = validationResult.error as z.ZodError | undefined if (validationError && showErrors) { - const formErrors = validationError.flatten().fieldErrors as Record - updateErrors(formErrors) + const flattenedErrors = validationError.flatten() + const formErrors = flattenedErrors.fieldErrors as Record + const formErrorKeys = Object.keys(formErrors) as K[] + const updatedErrors = formErrorKeys.reduce((acc, key) => { + const dedupedErrors = Array.from(new Set(formErrors[key])) + return { ...acc, [key]: dedupedErrors } + }, {} as Record) // prettier-ignore + updateErrors(updatedErrors) } return validationResult.success } @@ -95,6 +136,8 @@ export const useFormState = ({ ...currentValues, [key]: value })) } + const getChangeHandler = (key: K) => (value: T[K]) => handleChange(key, value) + // -- Input Props -- const getInputProps = (key: K) => ({ @@ -120,13 +163,14 @@ export const useFormState = = T extends Promise ? R : T export type HintedKeys = string & {} // eslint-disable-line @typescript-eslint/ban-types +export type DeepPartial = { + [P in keyof T]?: T[P] extends Array + ? Array> + : T[P] extends ReadonlyArray + ? ReadonlyArray> + : T[P] extends object + ? DeepPartial + : T[P] +} + export type any$Todo = any