| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- import React, { createContext, useReducer } from "react";
- import {
- PorterFormData,
- PorterFormState,
- PorterFormAction,
- PorterFormVariableList,
- GenericInputField,
- } from "./types";
- import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
- interface Props {
- rawFormData: PorterFormData;
- initialVariables?: PorterFormVariableList;
- overrideVariables?: PorterFormVariableList;
- isReadOnly?: boolean;
- }
- interface ContextProps {
- formData: PorterFormData;
- formState: PorterFormState;
- dispatchAction: (event: PorterFormAction) => void;
- isReadOnly?: boolean;
- }
- export const PorterFormContext = createContext<ContextProps | undefined>(
- undefined!
- );
- const { Provider } = PorterFormContext;
- export const PorterFormContextProvider: React.FC<Props> = (props) => {
- const handleAction = (
- state: PorterFormState,
- action: PorterFormAction
- ): PorterFormState => {
- switch (action.type) {
- case "init-field":
- if (!(action.id in state.components)) {
- return {
- ...state,
- components: {
- ...state.components,
- [action.id]: {
- state: action.initValue,
- validation: {
- ...{
- error: false,
- loading: false,
- validated: false,
- touched: false,
- },
- ...action.initValidation,
- },
- },
- },
- };
- }
- break;
- case "update-field":
- return {
- ...state,
- components: {
- ...state.components,
- [action.id]: {
- ...state.components[action.id],
- state: action.updateFunc(state.components[action.id]),
- },
- },
- };
- case "mutate-vars":
- return {
- ...state,
- variables: {
- ...action.mutateFunc(state.variables),
- ...props.overrideVariables,
- },
- };
- }
- return state;
- };
- const [state, dispatch] = useReducer(handleAction, {
- components: {},
- variables: props.initialVariables || {},
- });
- const evalShowIf = (
- vals: ShowIf,
- variables: PorterFormVariableList
- ): boolean => {
- if (!vals) {
- return false;
- }
- if (typeof vals == "string") {
- return !!variables[vals];
- }
- if ((vals as ShowIfOr).or) {
- vals = vals as ShowIfOr;
- for (let i = 0; i < vals.or.length; i++) {
- if (evalShowIf(vals.or[i], variables)) {
- return true;
- }
- }
- return false;
- }
- if ((vals as ShowIfAnd).and) {
- vals = vals as ShowIfAnd;
- for (let i = 0; i < vals.and.length; i++) {
- if (!evalShowIf(vals.and[i], variables)) {
- return false;
- }
- }
- return true;
- }
- if ((vals as ShowIfNot).not) {
- vals = vals as ShowIfNot;
- return !evalShowIf(vals.not, variables);
- }
- return false;
- };
- /*
- We don't want to have the actual <PorterForm> component to do as little form
- logic as possible, so this structures the form object based on show_if statements
- and assigns a unique id to each field
- This computed structure also later lets us figure out which fields should be required
- */
- const computeFormStructure = (
- data: PorterFormData,
- variables: PorterFormVariableList
- ) => {
- return {
- ...data,
- tabs: data.tabs.map((tab, i) => {
- return {
- ...tab,
- sections: tab.sections
- .map((section, j) => {
- return {
- ...section,
- contents: section.contents.map((field, k) => {
- return {
- ...field,
- id: `${i}-${j}-${k}`,
- };
- }),
- };
- })
- .filter((section) => {
- return !section.show_if || evalShowIf(section.show_if, variables);
- }),
- };
- }),
- };
- };
- /*
- compute a list of field ids who's input is required and a map from a variable value
- to a list of fields that set it
- */
- const computeRequiredVariables = (
- data: PorterFormData
- ): [string[], Record<string, string[]>] => {
- const requiredIds: string[] = [];
- const mapping: Record<string, string[]> = {};
- data.tabs.map((tab) =>
- tab.sections.map((section) =>
- section.contents.map((field) => {
- if (field.type == "heading" || field.type == "subtitle") return;
- if (field.required) {
- requiredIds.push(field.id);
- if (!mapping[field.variable]) {
- mapping[field.variable] = [];
- }
- mapping[field.variable].push(field.id);
- }
- })
- )
- );
- return [requiredIds, mapping];
- };
- const formData = computeFormStructure(props.rawFormData, state.variables);
- const [requiredIds, varMapping] = computeRequiredVariables(formData);
- return (
- <Provider
- value={{
- formData: formData,
- formState: state,
- dispatchAction: dispatch,
- isReadOnly: props.isReadOnly,
- }}
- >
- {props.children}
- </Provider>
- );
- };
|