Skip to content

Commit

Permalink
feat: form stateSubcription option
Browse files Browse the repository at this point in the history
  • Loading branch information
HugoPerard committed Jan 9, 2024
1 parent 648560f commit 2af134f
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 50 deletions.
129 changes: 88 additions & 41 deletions packages/formiz-core/src/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
Store,
ResetOptions,
DefaultFormValues,
useFormProps,
} from "@/types";
import {
getFormIsValid,
Expand All @@ -20,12 +21,14 @@ import {
getStepIsPristine,
getStepIsValidating,
getFieldIsExternalProcessing,
isFormStateSubscribed,
} from "@/utils/form";

export const formInterfaceSelector = <
Values extends object = DefaultFormValues
>(
state: Store<Values>
state: Store<Values>,
stateSubscription?: useFormProps<Values>["stateSubscription"]
): FormInterface<Values> => {
const currentStep = state.steps.find(
(step) => step.name === state.form.currentStepName
Expand Down Expand Up @@ -63,31 +66,75 @@ export const formInterfaceSelector = <
remove: state.actions.removeCollectionValue(fieldName),
}),

id: state.form.id,
resetKey: state.form.resetKey,
isReady: state.ready,
isSubmitted: state.form.isSubmitted,
isValid: getFormIsValid(state.fields),
isValidating: getFormIsValidating(state.fields),
isPristine: getFormIsPristine(state.fields),
steps: state.steps
.filter((step) => step.isEnabled)
.map(stepInterfaceSelector(state)),
currentStep: currentStep
? stepInterfaceSelector(state)(currentStep)
: undefined,
isStepPristine: currentStep
? getStepIsPristine(currentStep.name, state.fields)
: true,
isStepValid: currentStep
? getStepIsValid(currentStep.name, state.fields)
: true,
isStepValidating: currentStep
? getStepIsValidating(currentStep.name, state.fields)
: false,
isStepSubmitted: currentStep?.isSubmitted ?? false,
isFirstStep: state.steps[0]?.name === currentStep?.name,
isLastStep: state.steps[state.steps.length - 1]?.name === currentStep?.name,
...isFormStateSubscribed("id", state.form.id, stateSubscription),
...isFormStateSubscribed(
"resetKey",
state.form.resetKey,
stateSubscription
),
...isFormStateSubscribed("isReady", state.ready, stateSubscription),
...isFormStateSubscribed(
"isSubmitted",
state.form.isSubmitted,
stateSubscription
),
...isFormStateSubscribed(
"isValid",
getFormIsValid(state.fields),
stateSubscription
),
...isFormStateSubscribed(
"isValidating",
getFormIsValidating(state.fields),
stateSubscription
),
...isFormStateSubscribed(
"isPristine",
getFormIsPristine(state.fields),
stateSubscription
),
...isFormStateSubscribed(
"steps",
state.steps
.filter((step) => step.isEnabled)
.map(stepInterfaceSelector(state)),
stateSubscription
),
...isFormStateSubscribed(
"currentStep",
currentStep ? stepInterfaceSelector(state)(currentStep) : undefined,
stateSubscription
),
...isFormStateSubscribed(
"isStepPristine",
currentStep ? getStepIsPristine(currentStep.name, state.fields) : true,
stateSubscription
),
...isFormStateSubscribed(
"isStepValid",
currentStep ? getStepIsValid(currentStep.name, state.fields) : true,
stateSubscription
),
...isFormStateSubscribed(
"isStepValidating",
currentStep ? getStepIsValidating(currentStep.name, state.fields) : true,
stateSubscription
),
...isFormStateSubscribed(
"isStepSubmitted",
currentStep?.isSubmitted ?? false,
stateSubscription
),
...isFormStateSubscribed(
"isFirstStep",
state.steps[0]?.name === currentStep?.name,
stateSubscription
),
...isFormStateSubscribed(
"isLastStep",
state.steps[state.steps.length - 1]?.name === currentStep?.name,
stateSubscription
),
};
};

Expand Down Expand Up @@ -117,21 +164,21 @@ export interface FormInterface<Values extends object = DefaultFormValues> {
remove: ReturnType<Store<any>["actions"]["removeCollectionValue"]>;
};

id: Store<Values>["form"]["id"];
resetKey: Store<Values>["form"]["resetKey"];
isReady: Store<Values>["ready"];
isSubmitted: Store<Values>["form"]["isSubmitted"];
isValid: boolean;
isValidating: boolean;
isPristine: boolean;
steps: StepInterface[];
currentStep: StepInterface | undefined;
isStepPristine: boolean;
isStepValid: boolean;
isStepValidating: boolean;
isStepSubmitted: boolean;
isFirstStep: boolean;
isLastStep: boolean;
id?: Store<Values>["form"]["id"];
resetKey?: Store<Values>["form"]["resetKey"];
isReady?: Store<Values>["ready"];
isSubmitted?: Store<Values>["form"]["isSubmitted"];
isValid?: boolean;
isValidating?: boolean;
isPristine?: boolean;
steps?: StepInterface[];
currentStep?: StepInterface | undefined;
isStepPristine?: boolean;
isStepValid?: boolean;
isStepValidating?: boolean;
isStepSubmitted?: boolean;
isFirstStep?: boolean;
isLastStep?: boolean;
}

export const stepInterfaceSelector =
Expand Down
2 changes: 1 addition & 1 deletion packages/formiz-core/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ export const createStore = <Values extends object = DefaultFormValues>(
});
},

reset: (resetOptions = {}) => {
reset: (resetOptions) => {
set((state) => {
let initialValues = cloneDeep(
state.formConfigRef.current?.initialValues
Expand Down
29 changes: 28 additions & 1 deletion packages/formiz-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,9 @@ export type ResetElement =
| "visited"
| "values";

export type ResetOptions = { only?: ResetElement[]; exclude?: ResetElement[] };
export type ResetOptions =
| { only: ResetElement[]; exclude?: never }
| { exclude: ResetElement[]; only?: never };

export type StoreInitialState<Values extends object = DefaultFormValues> = {
ready?: boolean;
Expand Down Expand Up @@ -430,6 +432,25 @@ export interface Store<Values extends object = DefaultFormValues> {
};
}

export type FormStateElement = Extract<
keyof FormInterface,
| "id"
| "resetKey"
| "isReady"
| "isSubmitted"
| "isValid"
| "isValidating"
| "isPristine"
| "steps"
| "currentStep"
| "isStepPristine"
| "isStepValid"
| "isStepValidating"
| "isStepSubmitted"
| "isFirstStep"
| "isLastStep"
>;

export interface useFormProps<Values extends object = DefaultFormValues> {
/**
* Id of the form.
Expand Down Expand Up @@ -471,6 +492,12 @@ export interface useFormProps<Values extends object = DefaultFormValues> {
* Function triggered when form becomes invalid.
*/
onInvalid?(form: FormInterface<any>): void;
/**
* States to select or to exclude from form subscription
*/
stateSubscription?:
| { only: Array<FormStateElement>; exclude?: never }
| { exclude: Array<FormStateElement>; only?: never };
}

export interface FormizProps {
Expand Down
2 changes: 1 addition & 1 deletion packages/formiz-core/src/useForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const useForm = <Values extends object = any>(

const formState = useStore(
(state) => ({
...formInterfaceSelector(state),
...formInterfaceSelector(state, formConfig?.stateSubscription),
__connect: useStore,
}),
isDeepEqual
Expand Down
12 changes: 9 additions & 3 deletions packages/formiz-core/src/useFormContext.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { useFormStore } from "@/Formiz";
import { formInterfaceSelector } from "@/selectors";
import { ERROR_USE_FORM_CONTEXT_MISSING_CONTEXT } from "@/errors";
import { Store } from "@/types";
import { Store, useFormProps } from "@/types";
import { isDeepEqual } from "@/utils/global";

export const useFormContext = <Values extends object = any>() => {
export const useFormContext = <Values extends object = any>(
options?: Pick<useFormProps<Values>, "stateSubscription">
) => {
const { useStore } = useFormStore() ?? {};
if (!useStore) {
throw new Error(ERROR_USE_FORM_CONTEXT_MISSING_CONTEXT);
}
const formState = useStore?.(
(state) => formInterfaceSelector<Values>(state as Store<Values>),
(state) =>
formInterfaceSelector<Values>(
state as Store<Values>,
options?.stateSubscription
),
isDeepEqual
);
return { ...formState };
Expand Down
20 changes: 17 additions & 3 deletions packages/formiz-core/src/utils/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import type {
DefaultFormValues,
Field,
Fields,
FormStateElement,
PartialField,
ResetElement,
ResetOptions,
useFormProps,
} from "@/types";
import { isObject } from "@/utils/global";

Expand Down Expand Up @@ -213,7 +215,19 @@ export const generateField = <Value>(

export const isResetAllowed = (
resetElement: ResetElement,
resetOptions: ResetOptions
resetOptions?: ResetOptions
) =>
(!resetOptions.only || resetOptions.only.includes(resetElement)) &&
(!resetOptions.exclude || !resetOptions.exclude.includes(resetElement));
!resetOptions ||
((!resetOptions.only || resetOptions.only.includes(resetElement)) &&
(!resetOptions.exclude || !resetOptions.exclude.includes(resetElement)));

export const isFormStateSubscribed = (
state: FormStateElement,
value: unknown,
stateSubscription: useFormProps["stateSubscription"]
) =>
!stateSubscription ||
((!stateSubscription.only || stateSubscription.only.includes(state)) &&
(!stateSubscription.exclude || !stateSubscription.exclude.includes(state)))
? { [state]: value }
: {};

0 comments on commit 2af134f

Please sign in to comment.