PorterFormContextProvider.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import React, { createContext, useReducer } from "react";
  2. import {
  3. PorterFormData,
  4. PorterFormState,
  5. PorterFormAction,
  6. PorterFormVariableList,
  7. GenericInputField,
  8. } from "./types";
  9. import { ShowIf, ShowIfAnd, ShowIfNot, ShowIfOr } from "../../shared/types";
  10. interface Props {
  11. rawFormData: PorterFormData;
  12. initialVariables?: PorterFormVariableList;
  13. overrideVariables?: PorterFormVariableList;
  14. isReadOnly?: boolean;
  15. }
  16. interface ContextProps {
  17. formData: PorterFormData;
  18. formState: PorterFormState;
  19. dispatchAction: (event: PorterFormAction) => void;
  20. isReadOnly?: boolean;
  21. }
  22. export const PorterFormContext = createContext<ContextProps | undefined>(
  23. undefined!
  24. );
  25. const { Provider } = PorterFormContext;
  26. export const PorterFormContextProvider: React.FC<Props> = (props) => {
  27. const handleAction = (
  28. state: PorterFormState,
  29. action: PorterFormAction
  30. ): PorterFormState => {
  31. switch (action.type) {
  32. case "init-field":
  33. if (!(action.id in state.components)) {
  34. return {
  35. ...state,
  36. components: {
  37. ...state.components,
  38. [action.id]: {
  39. state: action.initValue,
  40. validation: {
  41. ...{
  42. error: false,
  43. loading: false,
  44. validated: false,
  45. touched: false,
  46. },
  47. ...action.initValidation,
  48. },
  49. },
  50. },
  51. };
  52. }
  53. break;
  54. case "update-field":
  55. return {
  56. ...state,
  57. components: {
  58. ...state.components,
  59. [action.id]: {
  60. ...state.components[action.id],
  61. state: action.updateFunc(state.components[action.id]),
  62. },
  63. },
  64. };
  65. case "mutate-vars":
  66. return {
  67. ...state,
  68. variables: {
  69. ...action.mutateFunc(state.variables),
  70. ...props.overrideVariables,
  71. },
  72. };
  73. }
  74. return state;
  75. };
  76. const [state, dispatch] = useReducer(handleAction, {
  77. components: {},
  78. variables: props.initialVariables || {},
  79. });
  80. const evalShowIf = (
  81. vals: ShowIf,
  82. variables: PorterFormVariableList
  83. ): boolean => {
  84. if (!vals) {
  85. return false;
  86. }
  87. if (typeof vals == "string") {
  88. return !!variables[vals];
  89. }
  90. if ((vals as ShowIfOr).or) {
  91. vals = vals as ShowIfOr;
  92. for (let i = 0; i < vals.or.length; i++) {
  93. if (evalShowIf(vals.or[i], variables)) {
  94. return true;
  95. }
  96. }
  97. return false;
  98. }
  99. if ((vals as ShowIfAnd).and) {
  100. vals = vals as ShowIfAnd;
  101. for (let i = 0; i < vals.and.length; i++) {
  102. if (!evalShowIf(vals.and[i], variables)) {
  103. return false;
  104. }
  105. }
  106. return true;
  107. }
  108. if ((vals as ShowIfNot).not) {
  109. vals = vals as ShowIfNot;
  110. return !evalShowIf(vals.not, variables);
  111. }
  112. return false;
  113. };
  114. /*
  115. We don't want to have the actual <PorterForm> component to do as little form
  116. logic as possible, so this structures the form object based on show_if statements
  117. and assigns a unique id to each field
  118. This computed structure also later lets us figure out which fields should be required
  119. */
  120. const computeFormStructure = (
  121. data: PorterFormData,
  122. variables: PorterFormVariableList
  123. ) => {
  124. return {
  125. ...data,
  126. tabs: data.tabs.map((tab, i) => {
  127. return {
  128. ...tab,
  129. sections: tab.sections
  130. .map((section, j) => {
  131. return {
  132. ...section,
  133. contents: section.contents.map((field, k) => {
  134. return {
  135. ...field,
  136. id: `${i}-${j}-${k}`,
  137. };
  138. }),
  139. };
  140. })
  141. .filter((section) => {
  142. return !section.show_if || evalShowIf(section.show_if, variables);
  143. }),
  144. };
  145. }),
  146. };
  147. };
  148. /*
  149. compute a list of field ids who's input is required and a map from a variable value
  150. to a list of fields that set it
  151. */
  152. const computeRequiredVariables = (
  153. data: PorterFormData
  154. ): [string[], Record<string, string[]>] => {
  155. const requiredIds: string[] = [];
  156. const mapping: Record<string, string[]> = {};
  157. data.tabs.map((tab) =>
  158. tab.sections.map((section) =>
  159. section.contents.map((field) => {
  160. if (field.type == "heading" || field.type == "subtitle") return;
  161. if (field.required) {
  162. requiredIds.push(field.id);
  163. if (!mapping[field.variable]) {
  164. mapping[field.variable] = [];
  165. }
  166. mapping[field.variable].push(field.id);
  167. }
  168. })
  169. )
  170. );
  171. return [requiredIds, mapping];
  172. };
  173. const formData = computeFormStructure(props.rawFormData, state.variables);
  174. const [requiredIds, varMapping] = computeRequiredVariables(formData);
  175. return (
  176. <Provider
  177. value={{
  178. formData: formData,
  179. formState: state,
  180. dispatchAction: dispatch,
  181. isReadOnly: props.isReadOnly,
  182. }}
  183. >
  184. {props.children}
  185. </Provider>
  186. );
  187. };