Skip to content

Commit

Permalink
feat: atom-based form state (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
pidkopajo authored Feb 9, 2021
1 parent 40aa543 commit b2b90b2
Show file tree
Hide file tree
Showing 15 changed files with 351 additions and 235 deletions.
19 changes: 16 additions & 3 deletions src/core/builders/create-form-schema.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -33,16 +34,19 @@ type ErrorsMarker<Err> = (errors: <Err>() => Err) => Err;
export const createFormSchema = <Values extends object, Err = never>(
fields: BuilderFn<Values>,
_errors?: ErrorsMarker<Err>
): FormSchema<Values, Err> => createObjectSchema(fields(Decoders)) as any;
): FormSchema<Values, Err> =>
createObjectSchema(fields(Decoders), Lens.identity()) as any;

const createObjectSchema = <O extends object>(
const createObjectSchema = <O extends object, Root>(
decodersMap: DecodersMap<O>,
lens: Lens<Root, O>,
path?: string
) => {
return keys(decodersMap).reduce((schema, key) => {
const decoder = decodersMap[key];
(schema as any)[key] = createFieldDescriptor(
impl(decoder) as _FieldDecoderImpl<any>,
Lens.compose(lens, Lens.prop(key as any)),
path ? `${path}.${key}` : `${key}`
);
return schema;
Expand All @@ -51,6 +55,7 @@ const createObjectSchema = <O extends object>(

const createFieldDescriptor = (
decoder: _FieldDecoderImpl<any>,
lens: Lens<any, any>,
path: string
): _FieldDescriptorImpl<any> => {
// these properties are hidden implementation details and thus should not be enumerable
Expand All @@ -69,6 +74,12 @@ const createFieldDescriptor = (
writable: false,
configurable: false,
},
__lens: {
value: lens,
enumerable: false,
writable: false,
configurable: false,
},
}
);

Expand All @@ -84,6 +95,7 @@ const createFieldDescriptor = (
const nthHandler = (i: number) =>
createFieldDescriptor(
decoder.inner as _FieldDecoderImpl<any>,
Lens.compose(lens, Lens.index(i)),
`${path}[${i}]`
);

Expand All @@ -101,7 +113,8 @@ const createFieldDescriptor = (

case "object": {
const props = createObjectSchema(
decoder.inner as DecodersMap<object>,
decoder.inner as DecodersMap<unknown>,
lens,
path
);
return Object.assign(rootDescriptor, props);
Expand Down
28 changes: 28 additions & 0 deletions src/core/helpers/branch-values.ts
Original file line number Diff line number Diff line change
@@ -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 = <Err>(
errors: FieldErrors<Err>,
field: FieldDescriptor<unknown>
): 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<unknown>
): string => {
const path = impl(field).__path;

const childrenValidating = keys(validating).filter(key =>
key.startsWith(path)
);

return JSON.stringify(childrenValidating);
};
1 change: 1 addition & 0 deletions src/core/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
3 changes: 3 additions & 0 deletions src/core/helpers/resolve-is-valid.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -8,12 +9,14 @@ const primitiveDescriptor = (path: string): FieldDescriptor<unknown> =>
opaque({
__path: path,
__decoder: { fieldType: "string" } as any,
__lens: Lens.prop(path), // not used,
});

const complexDescriptor = (path: string): FieldDescriptor<unknown> =>
opaque({
__path: path,
__decoder: { fieldType: "object" } as any,
__lens: Lens.prop(path), // not used,
});

describe("resolveIsValid", () => {
Expand Down
3 changes: 3 additions & 0 deletions src/core/helpers/resolve-is-validating.spec.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -8,12 +9,14 @@ const primitiveDescriptor = (path: string): FieldDescriptor<unknown> =>
opaque({
__path: path,
__decoder: { fieldType: "string" } as any,
__lens: Lens.prop(path), // not used,
});

const complexDescriptor = (path: string): FieldDescriptor<unknown> =>
opaque({
__path: path,
__decoder: { fieldType: "object" } as any,
__lens: Lens.prop(path), // not used,
});

describe("resolveIsValidating", () => {
Expand Down
103 changes: 86 additions & 17 deletions src/core/hooks/use-field/use-field.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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";

/**
Expand Down Expand Up @@ -38,38 +44,85 @@ export const useField = <T, Err>(
fieldDescriptor: GenericFieldDescriptor<T, Err>,
controller?: FormController
): FieldHandle<T, Err> => {
const { methods } = useFormtsContext<object, Err>(controller);
const { methods, state } = useFormtsContext<object, Err>(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<T> = Atom.Readonly<{
value: T;
touched: TouchedValues<T>;
}>;

const createFieldState = <T, Err>(
state: FormtsAtomState<object, Err>,
field: FieldDescriptor<T, Err>
): FieldState<T> => {
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 = <T, Err>(
state: FormtsAtomState<object, Err>,
field: FieldDescriptor<T, Err>
): 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 = <T, Err>(
descriptor: FieldDescriptor<T, Err>,
methods: InternalFormtsMethods<object, Err>
methods: InternalFormtsMethods<object, Err>,
fieldState: FieldState<T>,
formState: FormtsAtomState<object, Err>
): FieldHandle<T, Err> =>
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() {
Expand All @@ -80,18 +133,34 @@ const createFieldHandle = <T, Err>(
enumerable: true,
get: function () {
const nestedDescriptor = descriptor[key];
return createFieldHandle(nestedDescriptor, methods);
const childState = createFieldState(
formState,
nestedDescriptor
);
return createFieldHandle(
nestedDescriptor,
methods,
childState,
formState
);
},
}),
{}
);
}

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;
Expand Down Expand Up @@ -123,7 +192,7 @@ const createFieldHandle = <T, Err>(

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);
}
Expand All @@ -133,7 +202,7 @@ const createFieldHandle = <T, Err>(

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);
}
Expand Down
Loading

0 comments on commit b2b90b2

Please sign in to comment.