From b2b90b28c495422f30830a0be8664755e119b518 Mon Sep 17 00:00:00 2001 From: pidkopajo <61269871+pidkopajo@users.noreply.github.com> Date: Tue, 9 Feb 2021 18:08:33 +0100 Subject: [PATCH] feat: atom-based form state (#63) --- src/core/builders/create-form-schema.ts | 19 ++- src/core/helpers/branch-values.ts | 28 ++++ src/core/helpers/index.ts | 1 + src/core/helpers/resolve-is-valid.spec.ts | 3 + .../helpers/resolve-is-validating.spec.ts | 3 + src/core/hooks/use-field/use-field.ts | 103 ++++++++++-- .../use-form-controller/formts-dispatch.ts | 152 ++++++++++++++++++ .../use-form-controller/formts-methods.ts | 50 ++---- .../use-form-controller/formts-reducer.ts | 140 ---------------- .../use-form-controller.tsx | 11 +- .../hooks/use-form-handle/use-form-handle.ts | 37 +++-- .../hooks/use-form-values/use-form-values.ts | 5 +- src/core/types/field-descriptor.ts | 2 + src/core/types/formts-context.ts | 9 +- src/core/types/formts-state.ts | 23 ++- 15 files changed, 351 insertions(+), 235 deletions(-) create mode 100644 src/core/helpers/branch-values.ts create mode 100644 src/core/hooks/use-form-controller/formts-dispatch.ts delete mode 100644 src/core/hooks/use-form-controller/formts-reducer.ts diff --git a/src/core/builders/create-form-schema.ts b/src/core/builders/create-form-schema.ts index 9cce00f..709be62 100644 --- a/src/core/builders/create-form-schema.ts +++ b/src/core/builders/create-form-schema.ts @@ -1,4 +1,5 @@ import { assertNever, defineProperties, keys } from "../../utils"; +import { Lens } from "../../utils/lenses"; import * as Decoders from "../decoders"; import { FieldDecoder, _FieldDecoderImpl } from "../types/field-decoder"; import { _FieldDescriptorImpl } from "../types/field-descriptor"; @@ -33,16 +34,19 @@ type ErrorsMarker = (errors: () => Err) => Err; export const createFormSchema = ( fields: BuilderFn, _errors?: ErrorsMarker -): FormSchema => createObjectSchema(fields(Decoders)) as any; +): FormSchema => + createObjectSchema(fields(Decoders), Lens.identity()) as any; -const createObjectSchema = ( +const createObjectSchema = ( decodersMap: DecodersMap, + lens: Lens, path?: string ) => { return keys(decodersMap).reduce((schema, key) => { const decoder = decodersMap[key]; (schema as any)[key] = createFieldDescriptor( impl(decoder) as _FieldDecoderImpl, + Lens.compose(lens, Lens.prop(key as any)), path ? `${path}.${key}` : `${key}` ); return schema; @@ -51,6 +55,7 @@ const createObjectSchema = ( const createFieldDescriptor = ( decoder: _FieldDecoderImpl, + lens: Lens, path: string ): _FieldDescriptorImpl => { // these properties are hidden implementation details and thus should not be enumerable @@ -69,6 +74,12 @@ const createFieldDescriptor = ( writable: false, configurable: false, }, + __lens: { + value: lens, + enumerable: false, + writable: false, + configurable: false, + }, } ); @@ -84,6 +95,7 @@ const createFieldDescriptor = ( const nthHandler = (i: number) => createFieldDescriptor( decoder.inner as _FieldDecoderImpl, + Lens.compose(lens, Lens.index(i)), `${path}[${i}]` ); @@ -101,7 +113,8 @@ const createFieldDescriptor = ( case "object": { const props = createObjectSchema( - decoder.inner as DecodersMap, + decoder.inner as DecodersMap, + lens, path ); return Object.assign(rootDescriptor, props); diff --git a/src/core/helpers/branch-values.ts b/src/core/helpers/branch-values.ts new file mode 100644 index 0000000..3a5e8eb --- /dev/null +++ b/src/core/helpers/branch-values.ts @@ -0,0 +1,28 @@ +import { keys } from "../../utils"; +import { FieldDescriptor } from "../types/field-descriptor"; +import { FieldErrors, FieldValidatingState } from "../types/formts-state"; +import { impl } from "../types/type-mapper-util"; + +export const constructBranchErrorsString = ( + errors: FieldErrors, + field: FieldDescriptor +): string => { + const path = impl(field).__path; + + const childrenErrors = keys(errors).filter(key => key.startsWith(path)); + + return JSON.stringify(childrenErrors); +}; + +export const constructBranchValidatingString = ( + validating: FieldValidatingState, + field: FieldDescriptor +): string => { + const path = impl(field).__path; + + const childrenValidating = keys(validating).filter(key => + key.startsWith(path) + ); + + return JSON.stringify(childrenValidating); +}; diff --git a/src/core/helpers/index.ts b/src/core/helpers/index.ts index 33e4163..ea51b15 100644 --- a/src/core/helpers/index.ts +++ b/src/core/helpers/index.ts @@ -5,3 +5,4 @@ export * from "./make-validation-handlers"; export * from "./resolve-is-valid"; export * from "./resolve-is-validating"; export * from "./resolve-touched"; +export * from "./branch-values"; diff --git a/src/core/helpers/resolve-is-valid.spec.ts b/src/core/helpers/resolve-is-valid.spec.ts index 2ecc5de..89a748b 100644 --- a/src/core/helpers/resolve-is-valid.spec.ts +++ b/src/core/helpers/resolve-is-valid.spec.ts @@ -1,3 +1,4 @@ +import { Lens } from "../../utils/lenses"; import { FieldDescriptor } from "../types/field-descriptor"; import { FieldErrors } from "../types/formts-state"; import { opaque } from "../types/type-mapper-util"; @@ -8,12 +9,14 @@ const primitiveDescriptor = (path: string): FieldDescriptor => opaque({ __path: path, __decoder: { fieldType: "string" } as any, + __lens: Lens.prop(path), // not used, }); const complexDescriptor = (path: string): FieldDescriptor => opaque({ __path: path, __decoder: { fieldType: "object" } as any, + __lens: Lens.prop(path), // not used, }); describe("resolveIsValid", () => { diff --git a/src/core/helpers/resolve-is-validating.spec.ts b/src/core/helpers/resolve-is-validating.spec.ts index 0fb2360..f88000e 100644 --- a/src/core/helpers/resolve-is-validating.spec.ts +++ b/src/core/helpers/resolve-is-validating.spec.ts @@ -1,3 +1,4 @@ +import { Lens } from "../../utils/lenses"; import { FieldDescriptor } from "../types/field-descriptor"; import { FieldValidatingState } from "../types/formts-state"; import { opaque } from "../types/type-mapper-util"; @@ -8,12 +9,14 @@ const primitiveDescriptor = (path: string): FieldDescriptor => opaque({ __path: path, __decoder: { fieldType: "string" } as any, + __lens: Lens.prop(path), // not used, }); const complexDescriptor = (path: string): FieldDescriptor => opaque({ __path: path, __decoder: { fieldType: "object" } as any, + __lens: Lens.prop(path), // not used, }); describe("resolveIsValidating", () => { diff --git a/src/core/hooks/use-field/use-field.ts b/src/core/hooks/use-field/use-field.ts index 94c29a5..9a04ac1 100644 --- a/src/core/hooks/use-field/use-field.ts +++ b/src/core/hooks/use-field/use-field.ts @@ -1,5 +1,10 @@ +import { useMemo } from "react"; + import { keys, toIdentityDict } from "../../../utils"; +import { Atom } from "../../../utils/atoms"; +import { useSubscription } from "../../../utils/use-subscription"; import { useFormtsContext } from "../../context"; +import * as Helpers from "../../helpers"; import { isChoiceDecoder } from "../../types/field-decoder"; import { FieldDescriptor, @@ -10,6 +15,7 @@ import { import { FieldHandle, toFieldHandle } from "../../types/field-handle"; import { FormController } from "../../types/form-controller"; import { InternalFormtsMethods } from "../../types/formts-context"; +import { FormtsAtomState, TouchedValues } from "../../types/formts-state"; import { impl } from "../../types/type-mapper-util"; /** @@ -38,38 +44,85 @@ export const useField = ( fieldDescriptor: GenericFieldDescriptor, controller?: FormController ): FieldHandle => { - const { methods } = useFormtsContext(controller); + const { methods, state } = useFormtsContext(controller); + + const fieldState = useMemo(() => createFieldState(state, fieldDescriptor), [ + state, + impl(fieldDescriptor).__path, + ]); + const dependencies = useMemo( + () => createDependenciesState(state, fieldDescriptor), + [state, impl(fieldDescriptor).__path] + ); + + useSubscription(fieldState); + useSubscription(dependencies); + + return createFieldHandle(fieldDescriptor, methods, fieldState, state); +}; - return createFieldHandle(fieldDescriptor, methods); +type FieldState = Atom.Readonly<{ + value: T; + touched: TouchedValues; +}>; + +const createFieldState = ( + state: FormtsAtomState, + field: FieldDescriptor +): FieldState => { + const lens = impl(field).__lens; + + return Atom.fuse( + (value, touched) => ({ + value, + touched: touched as any, + }), + Atom.entangle(state.values, lens), + Atom.entangle(state.touched, lens) + ); +}; + +const createDependenciesState = ( + state: FormtsAtomState, + field: FieldDescriptor +): Atom.Readonly<{}> => { + return Atom.fuse( + (_branchErrors, _branchValidating) => ({}), + Atom.fuse(x => Helpers.constructBranchErrorsString(x, field), state.errors), + Atom.fuse( + x => Helpers.constructBranchValidatingString(x, field), + state.validating + ) + ); }; const createFieldHandle = ( descriptor: FieldDescriptor, - methods: InternalFormtsMethods + methods: InternalFormtsMethods, + fieldState: FieldState, + formState: FormtsAtomState ): FieldHandle => toFieldHandle({ descriptor, id: impl(descriptor).__path, - get value() { - return methods.getField(descriptor); - }, + value: fieldState.val.value, get isTouched() { - return methods.isFieldTouched(descriptor); + return Helpers.resolveTouched(fieldState.val.touched); }, get error() { - return methods.getFieldError(descriptor); + return formState.errors.val[impl(descriptor).__path] ?? null; }, get isValid() { - return methods.isFieldValid(descriptor); + return Helpers.resolveIsValid(formState.errors.val, descriptor); }, get isValidating() { - return methods.isFieldValidating(descriptor); + return Helpers.resolveIsValidating(formState.validating.val, descriptor); }, get children() { @@ -80,7 +133,16 @@ const createFieldHandle = ( enumerable: true, get: function () { const nestedDescriptor = descriptor[key]; - return createFieldHandle(nestedDescriptor, methods); + const childState = createFieldState( + formState, + nestedDescriptor + ); + return createFieldHandle( + nestedDescriptor, + methods, + childState, + formState + ); }, }), {} @@ -88,10 +150,17 @@ const createFieldHandle = ( } if (isArrayDescriptor(descriptor)) { - const value = methods.getField(descriptor) as unknown[]; - return value.map((_, i) => - createFieldHandle(descriptor.nth(i), methods) - ); + const value = (fieldState.val.value as unknown) as unknown[]; + return value.map((_, i) => { + const childDescriptor = descriptor.nth(i); + const childState = createFieldState(formState, childDescriptor); + return createFieldHandle( + descriptor.nth(i), + methods, + childState, + formState + ); + }); } return undefined; @@ -123,7 +192,7 @@ const createFieldHandle = ( addItem: item => { if (isArrayDescriptor(descriptor)) { - const array = methods.getField(descriptor) as unknown[]; + const array = (fieldState.val.value as unknown) as unknown[]; const updatedArray = [...array, item]; return methods.setFieldValue(descriptor, updatedArray); } @@ -133,7 +202,7 @@ const createFieldHandle = ( removeItem: index => { if (isArrayDescriptor(descriptor)) { - const array = methods.getField(descriptor) as unknown[]; + const array = (fieldState.val.value as unknown) as unknown[]; const updatedArray = array.filter((_, i) => i !== index); return methods.setFieldValue(descriptor, updatedArray); } diff --git a/src/core/hooks/use-form-controller/formts-dispatch.ts b/src/core/hooks/use-form-controller/formts-dispatch.ts new file mode 100644 index 0000000..40ceb4c --- /dev/null +++ b/src/core/hooks/use-form-controller/formts-dispatch.ts @@ -0,0 +1,152 @@ +import { filter, range } from "../../../utils"; +import { Atom } from "../../../utils/atoms"; +import { + createInitialValues, + makeTouchedValues, + makeUntouchedValues, +} from "../../helpers"; +import { FormtsOptions } from "../../types/formts-options"; +import { FormtsAction, FormtsAtomState } from "../../types/formts-state"; +import { impl } from "../../types/type-mapper-util"; + +export const getInitialState = ({ + Schema, + initialValues, +}: FormtsOptions): FormtsAtomState => { + const values = createInitialValues(Schema, initialValues); + const touched = makeUntouchedValues(values); + + return { + values: Atom.of(values), + touched: Atom.of(touched), + errors: Atom.of({}), + validating: Atom.of({}), + isSubmitting: Atom.of(false), + }; +}; + +export const createStateDispatch = ( + state: FormtsAtomState +) => (action: FormtsAction) => { + switch (action.type) { + case "reset": { + const { values } = action.payload; + const touched = makeUntouchedValues(values); + + state.values.set(values); + state.touched.set(touched); + state.errors.set({}); + state.validating.set({}); + state.isSubmitting.set(false); + break; + } + + case "touchValue": { + const lens = impl(action.payload.field).__lens; + + const value = lens.get(state.values.val); + const touched = lens.update(state.touched.val, () => + makeTouchedValues(value) + ); + + state.touched.set(touched); + break; + } + + case "setValue": { + const { field, value } = action.payload; + const lens = impl(field).__lens; + const path = impl(field).__path; + + const resolveErrors = () => { + if (!Array.isArray(value)) { + return state.errors.val; + } + + const currentValue = lens.get(state.values.val) as unknown[]; + + if (currentValue.length <= value.length) { + return state.errors.val; + } + + const hangingIndexes = range(value.length, currentValue.length - 1); + const errors = filter( + state.errors.val, + ({ key }) => + !hangingIndexes.some(i => key.startsWith(`${path}[${i}]`)) + ); + + return errors; + }; + + const values = lens.update(state.values.val, () => value); + const touched = lens.update(state.touched.val, () => + makeTouchedValues(value) + ); + + state.errors.set(resolveErrors()); + state.values.set(values); + state.touched.set(touched); + break; + } + + case "setErrors": { + const errors = action.payload.reduce( + (dict, { path, error }) => { + if (error != null) { + dict[path] = error; + } else { + delete dict[path]; + } + return dict; + }, + { ...state.errors.val } + ); + + state.errors.set(errors); + break; + } + + case "validatingStart": { + const { path, uuid } = action.payload; + + const validating = { + ...state.validating.val, + [path]: { ...state.validating.val[path], [uuid]: true as const }, + }; + + state.validating.set(validating); + break; + } + + case "validatingStop": { + const { path, uuid } = action.payload; + + const validating = (() => { + if (state.validating.val[path] == null) { + return state.validating.val; + } + + const validating = { ...state.validating.val }; + const uuids = { ...validating[path] }; + validating[path] = uuids; + + delete uuids[uuid]; + + if (Object.keys(uuids).length === 0) { + delete validating[path]; + } + + return validating; + })(); + + state.validating.set(validating); + break; + } + + case "setIsSubmitting": { + const { isSubmitting } = action.payload; + return state.isSubmitting.set(isSubmitting); + } + } +}; diff --git a/src/core/hooks/use-form-controller/formts-methods.ts b/src/core/hooks/use-form-controller/formts-methods.ts index 673d146..41b9e30 100644 --- a/src/core/hooks/use-form-controller/formts-methods.ts +++ b/src/core/hooks/use-form-controller/formts-methods.ts @@ -12,12 +12,12 @@ import { InternalFormtsMethods, } from "../../types/formts-context"; import { FormtsOptions } from "../../types/formts-options"; -import { FormtsAction, FormtsState } from "../../types/formts-state"; +import { FormtsAction, FormtsAtomState } from "../../types/formts-state"; import { impl } from "../../types/type-mapper-util"; type Input = { options: FormtsOptions; - state: FormtsState; + state: FormtsAtomState; dispatch: React.Dispatch>; }; @@ -27,24 +27,11 @@ export const createFormtsMethods = ({ dispatch, }: Input): InternalFormtsMethods => { const getField = (field: FieldDescriptor | string): T => { - const path = typeof field === "string" ? field : impl(field).__path; - return get(state.values, path) as any; + return typeof field === "string" + ? get(state.values.val, field) + : impl(field).__lens.get(state.values.val); }; - const getFieldError = (field: FieldDescriptor): Err | null => { - const error = state.errors[impl(field).__path]; - return error == null ? null : error; - }; - - const isFieldTouched = (field: FieldDescriptor) => - Helpers.resolveTouched(get(state.touched as object, impl(field).__path)); - - const isFieldValid = (field: FieldDescriptor) => - Helpers.resolveIsValid(state.errors, field); - - const isFieldValidating = (field: FieldDescriptor) => - Helpers.resolveIsValidating(state.validating, field); - const validateField = ( field: FieldDescriptor, trigger?: ValidationTrigger @@ -142,20 +129,6 @@ export const createFormtsMethods = ({ return Promise.resolve(); } - // TODO: getField is problematic when relaying on useReducer, should be solved when Atom based state is implemented - const modifiedGetField = ( - fieldToValidate: FieldDescriptor | string - ): T => { - const path = - typeof fieldToValidate === "string" - ? fieldToValidate - : impl(fieldToValidate).__path; - if (impl(field).__path === path) { - return value as any; - } - return getField(fieldToValidate); - }; - const { onFieldValidationStart, onFieldValidationEnd, @@ -164,7 +137,7 @@ export const createFormtsMethods = ({ return options.validator .validate({ fields: [field], - getValue: modifiedGetField, + getValue: getField, trigger: "change", onFieldValidationStart, onFieldValidationEnd, @@ -176,13 +149,13 @@ export const createFormtsMethods = ({ dispatch({ type: "setValue", - payload: { path: impl(field).__path, value }, + payload: { field, value }, }); return validateAfterChange(); }; const touchField = (field: FieldDescriptor) => - dispatch({ type: "touchValue", payload: { path: impl(field).__path } }); + dispatch({ type: "touchValue", payload: { field } }); const setFieldErrors = ( ...fields: Array<{ @@ -227,7 +200,7 @@ export const createFormtsMethods = ({ return { ok: false, errors } as const; } - return { ok: true, values: state.values } as const; + return { ok: true, values: state.values.val } as const; }) .then(result => { dispatch({ @@ -240,11 +213,6 @@ export const createFormtsMethods = ({ }; return { - getField, - getFieldError, - isFieldTouched, - isFieldValid, - isFieldValidating, validateField, validateForm, setFieldValue, diff --git a/src/core/hooks/use-form-controller/formts-reducer.ts b/src/core/hooks/use-form-controller/formts-reducer.ts deleted file mode 100644 index 18526ad..0000000 --- a/src/core/hooks/use-form-controller/formts-reducer.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Reducer } from "react"; - -import { filter, get, range, set } from "../../../utils"; -import { - createInitialValues, - makeTouchedValues, - makeUntouchedValues, -} from "../../helpers"; -import { FormtsOptions } from "../../types/formts-options"; -import { FormtsAction, FormtsState } from "../../types/formts-state"; - -export const createReducer = (): Reducer< - FormtsState, - FormtsAction -> => (state, action) => { - switch (action.type) { - case "reset": { - const { values } = action.payload; - const touched = makeUntouchedValues(values); - - return { - values, - touched, - errors: {}, - validating: {}, - isSubmitting: false, - }; - } - - case "touchValue": { - const { path } = action.payload; - - const value = get(state.values, path); - const touched = set(state.touched, path, makeTouchedValues(value)); - - return { ...state, touched }; - } - - case "setValue": { - const { path, value } = action.payload; - - const resolveErrors = () => { - if (!Array.isArray(value)) { - return state.errors; - } - - const currentValue = get(state.values, path) as unknown[]; - if (currentValue.length <= value.length) { - return state.errors; - } - - const hangingIndexes = range(value.length, currentValue.length - 1); - const errors = filter( - state.errors, - ({ key }) => - !hangingIndexes.some(i => key.startsWith(`${path}[${i}]`)) - ); - - return errors; - }; - - const values = set(state.values, path, value); - const touched = set(state.touched, path, makeTouchedValues(value)); - - return { ...state, values, touched, errors: resolveErrors() }; - } - - case "setErrors": { - const errors = action.payload.reduce( - (dict, { path, error }) => { - if (error != null) { - dict[path] = error; - } else { - delete dict[path]; - } - return dict; - }, - { ...state.errors } - ); - - return { ...state, errors }; - } - - case "validatingStart": { - const { path, uuid } = action.payload; - - const validating = { - ...state.validating, - [path]: { ...state.validating[path], [uuid]: true as const }, - }; - - return { ...state, validating }; - } - - case "validatingStop": { - const { path, uuid } = action.payload; - - const validating = (() => { - if (state.validating[path] == null) { - return state.validating; - } - - const validating = { ...state.validating }; - const uuids = { ...validating[path] }; - validating[path] = uuids; - - delete uuids[uuid]; - - if (Object.keys(uuids).length === 0) { - delete validating[path]; - } - - return validating; - })(); - - return { ...state, validating }; - } - - case "setIsSubmitting": { - const { isSubmitting } = action.payload; - return { ...state, isSubmitting }; - } - } -}; - -export const getInitialState = ({ - Schema, - initialValues, -}: FormtsOptions): FormtsState => { - const values = createInitialValues(Schema, initialValues); - const touched = makeUntouchedValues(values); - - return { - values, - touched, - errors: {}, - validating: {}, - isSubmitting: false, - }; -}; diff --git a/src/core/hooks/use-form-controller/use-form-controller.tsx b/src/core/hooks/use-form-controller/use-form-controller.tsx index b33129f..e4789b6 100644 --- a/src/core/hooks/use-form-controller/use-form-controller.tsx +++ b/src/core/hooks/use-form-controller/use-form-controller.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import { useCallback, useMemo } from "react"; import { FormController, @@ -7,8 +7,8 @@ import { import { FormtsOptions } from "../../types/formts-options"; import { opaque } from "../../types/type-mapper-util"; +import { createStateDispatch, getInitialState } from "./formts-dispatch"; import { createFormtsMethods } from "./formts-methods"; -import { createReducer, getInitialState } from "./formts-reducer"; /** * Hook that manages form state - should be used in main form component. @@ -34,11 +34,8 @@ import { createReducer, getInitialState } from "./formts-reducer"; export const useFormController = ( options: FormtsOptions ): FormController => { - const [state, dispatch] = React.useReducer( - createReducer(), - options, - getInitialState - ); + const state = useMemo(() => getInitialState(options), []); + const dispatch = useCallback(createStateDispatch(state), [state]); const methods = createFormtsMethods({ options, state, dispatch }); diff --git a/src/core/hooks/use-form-handle/use-form-handle.ts b/src/core/hooks/use-form-handle/use-form-handle.ts index 1495d5e..b115956 100644 --- a/src/core/hooks/use-form-handle/use-form-handle.ts +++ b/src/core/hooks/use-form-handle/use-form-handle.ts @@ -1,4 +1,8 @@ +import { useMemo } from "react"; + import { keys, values } from "../../../utils"; +import { Atom } from "../../../utils/atoms"; +import { useSubscription } from "../../../utils/use-subscription"; import { useFormtsContext } from "../../context"; import { resolveTouched } from "../../helpers"; import { FormController } from "../../types/form-controller"; @@ -33,20 +37,33 @@ export const useFormHandle = ( ): FormHandle => { const { state, methods } = useFormtsContext(controller); + const stateAtom = useMemo( + () => + Atom.fuse( + (isTouched, isValid, isValidating, isSubmitting) => ({ + isTouched, + isValid, + isValidating, + isSubmitting, + }), + Atom.fuse(resolveTouched, state.touched), + Atom.fuse(x => values(x).every(err => err == null), state.errors), + Atom.fuse(x => keys(x).length > 0, state.validating), + state.isSubmitting + ), + [state] + ); + + useSubscription(stateAtom); + return { - isSubmitting: state.isSubmitting, + isSubmitting: stateAtom.val.isSubmitting, - get isTouched() { - return resolveTouched(state.touched); - }, + isTouched: stateAtom.val.isTouched, - get isValid() { - return values(state.errors).filter(err => err != null).length === 0; - }, + isValid: stateAtom.val.isValid, - get isValidating() { - return keys(state.validating).length > 0; - }, + isValidating: stateAtom.val.isValidating, validate: methods.validateForm, diff --git a/src/core/hooks/use-form-values/use-form-values.ts b/src/core/hooks/use-form-values/use-form-values.ts index 19df2b5..2f88c55 100644 --- a/src/core/hooks/use-form-values/use-form-values.ts +++ b/src/core/hooks/use-form-values/use-form-values.ts @@ -1,3 +1,4 @@ +import { useSubscription } from "../../../utils/use-subscription"; import { useFormtsContext } from "../../context"; import { FormController } from "../../types/form-controller"; import { FormSchema } from "../../types/form-schema"; @@ -29,6 +30,6 @@ export const useFormValues = ( controller?: FormController ): Values => { const { state } = useFormtsContext(controller); - - return state.values; + useSubscription(state.values); + return state.values.val; }; diff --git a/src/core/types/field-descriptor.ts b/src/core/types/field-descriptor.ts index 4cb42d7..bd0b820 100644 --- a/src/core/types/field-descriptor.ts +++ b/src/core/types/field-descriptor.ts @@ -1,4 +1,5 @@ import { Nominal, range, values } from "../../utils"; +import { Lens } from "../../utils/lenses"; import { _FieldDecoderImpl } from "./field-decoder"; import { impl } from "./type-mapper-util"; @@ -7,6 +8,7 @@ import { impl } from "./type-mapper-util"; export type _FieldDescriptorImpl = { __path: string; __decoder: _FieldDecoderImpl; + __lens: Lens; // TODO maybe add root typing Lens }; export type _NTHHandler = { diff --git a/src/core/types/formts-context.ts b/src/core/types/formts-context.ts index f506fa7..f5ad1e0 100644 --- a/src/core/types/formts-context.ts +++ b/src/core/types/formts-context.ts @@ -4,18 +4,13 @@ import { FieldDescriptor } from "./field-descriptor"; import { FieldError } from "./form-handle"; import { ValidationResult, ValidationTrigger } from "./form-validator"; import { FormtsOptions } from "./formts-options"; -import { FormtsState } from "./formts-state"; +import { FormtsAtomState } from "./formts-state"; export type FormSubmissionResult = | { ok: true; values: Values } | { ok: false; errors: Array> }; export type InternalFormtsMethods = { - getField: (field: FieldDescriptor) => T; - getFieldError: (field: FieldDescriptor) => Err | null; - isFieldTouched: (field: FieldDescriptor) => boolean; - isFieldValid: (field: FieldDescriptor) => boolean; - isFieldValidating: (field: FieldDescriptor) => boolean; validateField: ( field: FieldDescriptor, trigger?: ValidationTrigger @@ -35,6 +30,6 @@ export type InternalFormtsMethods = { // internal context consumed by hooks export type InternalFormtsContext = { options: FormtsOptions; - state: FormtsState; + state: FormtsAtomState; methods: InternalFormtsMethods; }; diff --git a/src/core/types/formts-state.ts b/src/core/types/formts-state.ts index d634416..c40bc3f 100644 --- a/src/core/types/formts-state.ts +++ b/src/core/types/formts-state.ts @@ -1,19 +1,26 @@ +import { Atom } from "../../utils/atoms"; + +import { FieldDescriptor } from "./field-descriptor"; + // internal state & actions export type FormtsAction = | { type: "reset"; payload: { values: Values } } - | { type: "touchValue"; payload: { path: string } } - | { type: "setValue"; payload: { path: string; value: any } } + | { type: "touchValue"; payload: { field: FieldDescriptor } } + | { + type: "setValue"; + payload: { field: FieldDescriptor; value: any }; + } | { type: "setErrors"; payload: Array<{ path: string; error: Err | null }> } | { type: "validatingStart"; payload: { path: string; uuid: string } } | { type: "validatingStop"; payload: { path: string; uuid: string } } | { type: "setIsSubmitting"; payload: { isSubmitting: boolean } }; -export type FormtsState = { - values: Values; - touched: TouchedValues; - errors: FieldErrors; - validating: FieldValidatingState; - isSubmitting: boolean; +export type FormtsAtomState = { + values: Atom; + touched: Atom>; + errors: Atom>; + validating: Atom; + isSubmitting: Atom; }; export type TouchedValues = [V] extends [Array]