Skip to content

Commit

Permalink
refactor: Create useSchema hook to make validation logic and `Input…
Browse files Browse the repository at this point in the history
…Field` components shareable (#3526)
  • Loading branch information
DafyddLlyr authored Aug 22, 2024
1 parent 900e30b commit 66241e4
Show file tree
Hide file tree
Showing 36 changed files with 758 additions and 653 deletions.
2 changes: 1 addition & 1 deletion doc/how-to/how-to-add-a-list-component-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The ideal maintainers of these schemas are still the services team though, rathe

2. **GitHub** - In the `.ts` file, ensure the schema has this basic structure:
```ts
import { Schema } from "@planx/components/List/model";
import { Schema } from "@planx/component/shared/Schema/model";

export const YourSchemasName: Schema = {
type: "Title (singular if no max, plural if max = 1)",
Expand Down
135 changes: 63 additions & 72 deletions editor.planx.uk/src/@planx/components/List/Public/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { useSchema } from "@planx/components/shared/Schema/hook";
import {
Schema,
SchemaUserData,
SchemaUserResponse,
} from "@planx/components/shared/Schema/model";
import {
getPreviouslySubmittedData,
makeData,
Expand All @@ -11,14 +17,7 @@ import React, {
useState,
} from "react";

import {
generateInitialValues,
generateValidationSchema,
List,
Schema,
UserData,
UserResponse,
} from "../model";
import { List } from "../model";
import {
flatten,
sumIdenticalUnits,
Expand All @@ -33,7 +32,7 @@ interface ListContextValue {
removeItem: (index: number) => void;
editItem: (index: number) => void;
cancelEditItem: () => void;
formik: FormikProps<UserData>;
formik: FormikProps<SchemaUserData>;
validateAndSubmitForm: () => void;
listProps: PublicProps<List>;
/**
Expand All @@ -58,13 +57,57 @@ const ListContext = createContext<ListContextValue | undefined>(undefined);

export const ListProvider: React.FC<ListProviderProps> = (props) => {
const { schema, children, handleSubmit } = props;
const { formikConfig, initialValues } = useSchema({
schema,
previousValues: getPreviouslySubmittedData(props),
});

const formik = useFormik<SchemaUserData>({
...formikConfig,
onSubmit: (values) => {
// defaultPassportData (array) is used when coming "back"
const defaultPassportData = makeData(props, values.schemaData)?.["data"];

// flattenedPassportData makes individual list items compatible with Calculate components
const flattenedPassportData = flatten(defaultPassportData, { depth: 2 });

// basic example of general summary stats we can add onSubmit:
// 1. count of items/responses
// 2. if the schema includes a field that sets fn = "identicalUnits", sum of total units
// 3. if the schema includes a field that sets fn = "development" & fn = "identicalUnits", sum of total units by development "val"
const totalUnits = sumIdenticalUnits(props.fn, defaultPassportData);
const totalUnitsByDevelopmentType = sumIdenticalUnitsByDevelopmentType(
props.fn,
defaultPassportData,
);

const summaries = {
[`${props.fn}.total.listItems`]:
defaultPassportData[`${props.fn}`].length,
...(totalUnits > 0 && {
[`${props.fn}.total.units`]: totalUnits,
}),
...(totalUnits > 0 &&
Object.keys(totalUnitsByDevelopmentType).length > 0 &&
totalUnitsByDevelopmentType),
};

handleSubmit?.({
data: {
...defaultPassportData,
...flattenedPassportData,
...summaries,
},
});
},
});

const [activeIndex, setActiveIndex] = useState<number>(
props.previouslySubmittedData ? -1 : 0,
);

const [activeItemInitialState, setActiveItemInitialState] = useState<
UserResponse | undefined
SchemaUserResponse | undefined
>(undefined);

const [addItemError, setAddItemError] = useState<boolean>(false);
Expand All @@ -85,21 +128,21 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
if (activeIndex !== -1) return setAddItemError(true);

// Do not allow new item to be added if it will exceed max
if (schema.max && formik.values.userData.length === schema.max) {
if (schema.max && formik.values.schemaData.length === schema.max) {
return setMaxError(true);
}

// Add new item, and set to active
setAddItemError(false);
formik.values.userData.push(generateInitialValues(schema));
setActiveIndex(formik.values.userData.length - 1);
formik.values.schemaData.push(initialValues);
setActiveIndex(formik.values.schemaData.length - 1);
};

const saveItem = async () => {
resetErrors();

const errors = await formik.validateForm();
const isValid = !errors.userData?.length;
const isValid = !errors.schemaData?.length;
if (isValid) {
exitEditMode();
setAddItemError(false);
Expand All @@ -114,10 +157,10 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
setActiveIndex((prev) => (prev === -1 ? 0 : prev - 1));
}

// Remove item from userData
// Remove item from schemaData
formik.setFieldValue(
"userData",
formik.values.userData.filter((_, i) => i !== index),
"schemaData",
formik.values.schemaData.filter((_, i) => i !== index),
);
};

Expand All @@ -126,7 +169,7 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
if (activeIndex !== -1) return setUnsavedItemError(true);

// Manually validate minimum number of items
if (formik.values.userData.length < schema.min) {
if (formik.values.schemaData.length < schema.min) {
return setMinError(true);
}

Expand All @@ -144,69 +187,17 @@ export const ListProvider: React.FC<ListProviderProps> = (props) => {
};

const editItem = (index: number) => {
setActiveItemInitialState(formik.values.userData[index]);
setActiveItemInitialState(formik.values.schemaData[index]);
setActiveIndex(index);
};

const getInitialValues = () => {
const previousValues = getPreviouslySubmittedData(props);
if (previousValues) return previousValues;

return schema.min ? [generateInitialValues(schema)] : [];
};

const exitEditMode = () => setActiveIndex(-1);

const resetItemToPreviousState = () =>
formik.setFieldValue(`userData[${activeIndex}]`, activeItemInitialState);
formik.setFieldValue(`schemaData[${activeIndex}]`, activeItemInitialState);

const isPageComponent = schema.max === 1;

const formik = useFormik<UserData>({
initialValues: {
userData: getInitialValues(),
},
onSubmit: (values) => {
// defaultPassportData (array) is used when coming "back"
const defaultPassportData = makeData(props, values.userData)?.["data"];

// flattenedPassportData makes individual list items compatible with Calculate components
const flattenedPassportData = flatten(defaultPassportData, { depth: 2 });

// basic example of general summary stats we can add onSubmit:
// 1. count of items/responses
// 2. if the schema includes a field that sets fn = "identicalUnits", sum of total units
// 3. if the schema includes a field that sets fn = "development" & fn = "identicalUnits", sum of total units by development "val"
const totalUnits = sumIdenticalUnits(props.fn, defaultPassportData);
const totalUnitsByDevelopmentType = sumIdenticalUnitsByDevelopmentType(
props.fn,
defaultPassportData,
);

const summaries = {
[`${props.fn}.total.listItems`]:
defaultPassportData[`${props.fn}`].length,
...(totalUnits > 0 && {
[`${props.fn}.total.units`]: totalUnits,
}),
...(totalUnits > 0 &&
Object.keys(totalUnitsByDevelopmentType).length > 0 &&
totalUnitsByDevelopmentType),
};

handleSubmit?.({
data: {
...defaultPassportData,
...flattenedPassportData,
...summaries,
},
});
},
validateOnBlur: false,
validateOnChange: false,
validationSchema: generateValidationSchema(schema),
});

return (
<ListContext.Provider
value={{
Expand Down
Loading

0 comments on commit 66241e4

Please sign in to comment.