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