@openmrs/esm-form-engine-lib 4.2.2-pre.2546 → 4.2.2-pre.2550

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/components/processor-factory/form-processor-factory.component.js +111 -39
  2. package/dist/components/renderer/custom-hooks-renderer.component.js +2 -2
  3. package/dist/hooks/useFormFieldValidators.js +22 -12
  4. package/dist/hooks/useFormFieldValueAdapters.js +22 -15
  5. package/dist/hooks/useFormFields.js +35 -42
  6. package/dist/hooks/useInitialValues.js +12 -5
  7. package/dist/hooks/useProcessorDependencies.js +2 -2
  8. package/dist/processors/encounter/encounter-form-processor.js +10 -20
  9. package/dist/processors/form-processor.js +4 -1
  10. package/dist/src/components/processor-factory/form-processor-factory.component.d.ts.map +1 -1
  11. package/dist/src/components/renderer/custom-hooks-renderer.component.d.ts +5 -8
  12. package/dist/src/components/renderer/custom-hooks-renderer.component.d.ts.map +1 -1
  13. package/dist/src/hooks/useFormFieldValidators.d.ts.map +1 -1
  14. package/dist/src/hooks/useFormFieldValueAdapters.d.ts.map +1 -1
  15. package/dist/src/hooks/useFormFields.d.ts.map +1 -1
  16. package/dist/src/hooks/useInitialValues.d.ts.map +1 -1
  17. package/dist/src/hooks/useProcessorDependencies.d.ts +2 -2
  18. package/dist/src/hooks/useProcessorDependencies.d.ts.map +1 -1
  19. package/dist/src/processors/encounter/encounter-form-processor.d.ts +6 -3
  20. package/dist/src/processors/encounter/encounter-form-processor.d.ts.map +1 -1
  21. package/dist/src/processors/form-processor.d.ts +21 -2
  22. package/dist/src/processors/form-processor.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/components/processor-factory/form-processor-factory.component.tsx +151 -35
  25. package/src/components/renderer/custom-hooks-renderer.component.tsx +6 -8
  26. package/src/hooks/useFormFieldValidators.ts +23 -13
  27. package/src/hooks/useFormFieldValueAdapters.ts +23 -16
  28. package/src/hooks/useFormFields.ts +28 -41
  29. package/src/hooks/useInitialValues.ts +15 -6
  30. package/src/hooks/useProcessorDependencies.ts +4 -3
  31. package/src/processors/encounter/encounter-form-processor.ts +12 -21
  32. package/src/processors/form-processor.ts +27 -1
@@ -2,12 +2,27 @@ import { type OpenmrsResource } from '@openmrs/esm-framework';
2
2
  import { type FormContextProps } from '../provider/form-provider';
3
3
  import { type ValueAndDisplay, type FormField, type FormSchema, type FormProcessorContextProps } from '../types';
4
4
  export type FormProcessorConstructor = new (...args: ConstructorParameters<typeof FormProcessor>) => FormProcessor;
5
+ /**
6
+ * The setters a processor uses to feed its results back into the processor context. These are the
7
+ * only three properties it can change; the rest of {@link FormProcessorContextProps} is derived by
8
+ * the FormProcessorFactory. They supersede the `setContext` passed alongside them, which is shaped
9
+ * like a `setState` over the whole context but silently discards everything else.
10
+ */
11
+ export interface FormProcessorContextSetters {
12
+ setDomainObjectValue: (value: OpenmrsResource) => void;
13
+ setPreviousDomainObjectValue: (value: OpenmrsResource) => void;
14
+ setCustomDependencies: (dependencies: Record<string, any> | ((previous: Record<string, any>) => Record<string, any>)) => void;
15
+ }
5
16
  export type GetCustomHooksResponse = {
6
17
  useCustomHooks: (context: Partial<FormProcessorContextProps>) => {
7
18
  data: any;
8
19
  isLoading: boolean;
9
20
  error: any;
10
- updateContext: (setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>) => void;
21
+ /**
22
+ * Called once the hook has finished loading, to merge its results into the processor context.
23
+ * `setContext` is deprecated; use `setters`.
24
+ */
25
+ updateContext: (setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>, setters: FormProcessorContextSetters) => void;
11
26
  };
12
27
  };
13
28
  export declare abstract class FormProcessor {
@@ -15,7 +30,11 @@ export declare abstract class FormProcessor {
15
30
  domainObjectValue: OpenmrsResource;
16
31
  constructor(formJson: FormSchema);
17
32
  getDomainObject(): OpenmrsResource;
18
- loadDependencies(context: Partial<FormProcessorContextProps>, setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>): Promise<Record<string, any>>;
33
+ /**
34
+ * Loads whatever the processor needs before the form can render, merging the results into the
35
+ * processor context. `setContext` is deprecated; use `setters`.
36
+ */
37
+ loadDependencies(context: Partial<FormProcessorContextProps>, setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>, setters: FormProcessorContextSetters): Promise<Record<string, any>>;
19
38
  abstract getHistoricalValue(field: FormField, context: FormContextProps): Promise<ValueAndDisplay>;
20
39
  abstract processSubmission(context: FormContextProps, abortController: AbortController): Promise<OpenmrsResource>;
21
40
  abstract getInitialValues(context: FormProcessorContextProps): Promise<Record<string, any>>;
@@ -1 +1 @@
1
- {"version":3,"file":"form-processor.d.ts","sourceRoot":"","sources":["../../../src/processors/form-processor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAEjH,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,IAAI,EAAE,qBAAqB,CAAC,OAAO,aAAa,CAAC,KAAK,aAAa,CAAC;AAEnH,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,yBAAyB,CAAC,KAAK;QAC/D,IAAI,EAAE,GAAG,CAAC;QACV,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,GAAG,CAAC;QACX,aAAa,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,yBAAyB,CAAC,CAAC,KAAK,IAAI,CAAC;KACtG,CAAC;CACH,CAAC;AAEF,8BAAsB,aAAa;IACjC,QAAQ,EAAE,UAAU,CAAC;IACrB,iBAAiB,EAAE,eAAe,CAAC;gBAEvB,QAAQ,EAAE,UAAU;IAIhC,eAAe;IAIT,gBAAgB,CACpB,OAAO,EAAE,OAAO,CAAC,yBAAyB,CAAC,EAC3C,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,yBAAyB,CAAC,CAAC,GAC1E,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAI/B,QAAQ,CAAC,kBAAkB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAClG,QAAQ,CAAC,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IACjH,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3F,QAAQ,CAAC,cAAc,IAAI,sBAAsB;IACjD,QAAQ,CAAC,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU;CAC3D"}
1
+ {"version":3,"file":"form-processor.d.ts","sourceRoot":"","sources":["../../../src/processors/form-processor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAE,KAAK,yBAAyB,EAAE,MAAM,UAAU,CAAC;AAEjH,MAAM,MAAM,wBAAwB,GAAG,KAAK,GAAG,IAAI,EAAE,qBAAqB,CAAC,OAAO,aAAa,CAAC,KAAK,aAAa,CAAC;AAEnH;;;;;GAKG;AACH,MAAM,WAAW,2BAA2B;IAC1C,oBAAoB,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IACvD,4BAA4B,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;IAC/D,qBAAqB,EAAE,CACrB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,KACzF,IAAI,CAAC;CACX;AAED,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,yBAAyB,CAAC,KAAK;QAC/D,IAAI,EAAE,GAAG,CAAC;QACV,SAAS,EAAE,OAAO,CAAC;QACnB,KAAK,EAAE,GAAG,CAAC;QACX;;;WAGG;QACH,aAAa,EAAE,CACb,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,yBAAyB,CAAC,CAAC,EAC3E,OAAO,EAAE,2BAA2B,KACjC,IAAI,CAAC;KACX,CAAC;CACH,CAAC;AAEF,8BAAsB,aAAa;IACjC,QAAQ,EAAE,UAAU,CAAC;IACrB,iBAAiB,EAAE,eAAe,CAAC;gBAEvB,QAAQ,EAAE,UAAU;IAIhC,eAAe;IAIf;;;OAGG;IACG,gBAAgB,CACpB,OAAO,EAAE,OAAO,CAAC,yBAAyB,CAAC,EAC3C,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,cAAc,CAAC,yBAAyB,CAAC,CAAC,EAC3E,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAI/B,QAAQ,CAAC,kBAAkB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAClG,QAAQ,CAAC,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC;IACjH,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3F,QAAQ,CAAC,cAAc,IAAI,sBAAsB;IACjD,QAAQ,CAAC,iBAAiB,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU;CAC3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-form-engine-lib",
3
- "version": "4.2.2-pre.2546",
3
+ "version": "4.2.2-pre.2550",
4
4
  "description": "React Form Engine for O3",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
@@ -1,4 +1,5 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { type OpenmrsResource } from '@openmrs/esm-framework';
2
3
  import useProcessorDependencies from '../../hooks/useProcessorDependencies';
3
4
  import useInitialValues from '../../hooks/useInitialValues';
4
5
  import { FormRenderer } from '../renderer/form/form-renderer.component';
@@ -11,6 +12,7 @@ import { useFormFieldsMeta } from '../../hooks/useFormFieldsMeta';
11
12
  import { useFormFactory } from '../../provider/form-factory-provider';
12
13
  import { useFormFieldValueAdapters } from '../../hooks/useFormFieldValueAdapters';
13
14
  import { EncounterFormProcessor } from '../../processors/encounter/encounter-form-processor';
15
+ import { type FormProcessorContextSetters } from '../../processors/form-processor';
14
16
  import { reportError } from '../../utils/error-utils';
15
17
  import { useTranslation } from 'react-i18next';
16
18
  import Loader from '../loaders/loader.component';
@@ -22,12 +24,48 @@ interface FormProcessorFactoryProps {
22
24
  setIsLoadingFormDependencies: (isLoading: boolean) => void;
23
25
  }
24
26
 
27
+ // Mutable parts of the context that can be updated by processors/hooks
28
+ interface MutableContextState {
29
+ domainObjectValue?: OpenmrsResource;
30
+ previousDomainObjectValue?: OpenmrsResource;
31
+ customDependencies?: Record<string, any>;
32
+ }
33
+
34
+ /** Context properties this component derives; a processor that sets one of these is ignored. */
35
+ const factoryOwnedContextKeys = [
36
+ 'patient',
37
+ 'formJson',
38
+ 'visit',
39
+ 'sessionMode',
40
+ 'sessionDate',
41
+ 'location',
42
+ 'currentProvider',
43
+ 'layoutType',
44
+ 'processor',
45
+ 'formFields',
46
+ 'formFieldAdapters',
47
+ 'formFieldValidators',
48
+ ] as const satisfies ReadonlyArray<keyof FormProcessorContextProps>;
49
+
50
+ function warnAboutIgnoredContextKeys(previous: FormProcessorContextProps, next: FormProcessorContextProps) {
51
+ const ignored = factoryOwnedContextKeys.filter((key) => key in next && next[key] !== previous[key]);
52
+ if (ignored.length) {
53
+ console.warn(
54
+ `A form processor tried to set the context propert${ignored.length === 1 ? 'y' : 'ies'} ` +
55
+ `${ignored.join(', ')}, which the form engine derives itself. ${
56
+ ignored.length === 1 ? 'It was' : 'They were'
57
+ } ignored. Use the setters passed alongside setContext to update the domain object or custom dependencies.`,
58
+ );
59
+ }
60
+ }
61
+
25
62
  const FormProcessorFactory = ({
26
63
  formJson,
27
64
  isSubForm = false,
28
65
  setIsLoadingFormDependencies,
29
66
  }: FormProcessorFactoryProps) => {
30
67
  const { patient, sessionMode, formProcessors, layoutType, location, provider, sessionDate, visit } = useFormFactory();
68
+ const { t } = useTranslation();
31
69
 
32
70
  const processor = useMemo(() => {
33
71
  const ProcessorClass = formProcessors[formJson.processor];
@@ -42,58 +80,135 @@ const FormProcessorFactory = ({
42
80
  return processorInstance;
43
81
  }, [formProcessors, formJson.processor]);
44
82
 
45
- const [processorContext, setProcessorContext] = useState<FormProcessorContextProps>({
46
- patient,
47
- formJson,
48
- sessionMode,
49
- layoutType,
50
- location,
51
- currentProvider: provider,
52
- processor,
53
- sessionDate,
54
- visit,
55
- formFields: [],
56
- formFieldAdapters: {},
57
- formFieldValidators: {},
58
- });
59
- const { t } = useTranslation();
83
+ // Derive form fields and related data
60
84
  const { formFields: rawFormFields, conceptReferences } = useFormFields(formJson);
61
85
  const { concepts: formFieldsConcepts, isLoading: isLoadingConcepts } = useConcepts(Array.from(conceptReferences));
62
86
  const formFieldsWithMeta = useFormFieldsMeta(rawFormFields, formFieldsConcepts);
63
87
  const formFieldAdapters = useFormFieldValueAdapters(rawFormFields);
64
88
  const formFieldValidators = useFormFieldValidators(rawFormFields);
65
- const { isLoading: isLoadingCustomDeps } = useProcessorDependencies(processor, processorContext, setProcessorContext);
89
+
90
+ const formFields = useMemo(
91
+ () => (formFieldsWithMeta?.length ? formFieldsWithMeta : rawFormFields ?? []),
92
+ [formFieldsWithMeta, rawFormFields],
93
+ );
94
+
95
+ // We divide the context into the "mutable" parts, which can be changed by custom hooks
96
+ // and the "static" parts
97
+ const [mutableContext, setMutableContext] = useState<MutableContextState>({});
98
+
99
+ // Create the "static" part of the context
100
+ const baseContext = useMemo<Omit<FormProcessorContextProps, keyof MutableContextState>>(
101
+ () => ({
102
+ patient,
103
+ formJson,
104
+ sessionMode,
105
+ layoutType,
106
+ location,
107
+ currentProvider: provider,
108
+ processor,
109
+ sessionDate,
110
+ visit,
111
+ formFields,
112
+ formFieldAdapters: formFieldAdapters ?? {},
113
+ formFieldValidators: formFieldValidators ?? {},
114
+ }),
115
+ [
116
+ patient,
117
+ formJson,
118
+ sessionMode,
119
+ layoutType,
120
+ location,
121
+ provider,
122
+ processor,
123
+ sessionDate,
124
+ visit,
125
+ formFields,
126
+ formFieldAdapters,
127
+ formFieldValidators,
128
+ ],
129
+ );
130
+
131
+ // re-create the full processor context
132
+ const processorContext = useMemo<FormProcessorContextProps>(
133
+ () => ({
134
+ ...baseContext,
135
+ ...mutableContext,
136
+ }),
137
+ [baseContext, mutableContext],
138
+ );
139
+
140
+ // callback to update the mutable part of the context
141
+ const setProcessorContext = useCallback(
142
+ (updater: FormProcessorContextProps | ((prev: FormProcessorContextProps) => FormProcessorContextProps)) => {
143
+ setMutableContext((prevMutable) => {
144
+ // Build the "previous" full context to pass to the updater
145
+ const prevFull: FormProcessorContextProps = {
146
+ ...baseContext,
147
+ ...prevMutable,
148
+ };
149
+
150
+ const newFull = typeof updater === 'function' ? updater(prevFull) : updater;
151
+
152
+ // Only the updater form sees prevFull; a bare object may be a stale context, whose
153
+ // differences aren't attempted writes.
154
+ if (typeof updater === 'function') {
155
+ warnAboutIgnoredContextKeys(prevFull, newFull);
156
+ }
157
+
158
+ // Extract only the mutable parts from the result
159
+ return {
160
+ domainObjectValue: newFull.domainObjectValue,
161
+ previousDomainObjectValue: newFull.previousDomainObjectValue,
162
+ customDependencies: newFull.customDependencies,
163
+ };
164
+ });
165
+ },
166
+ [baseContext],
167
+ );
168
+
169
+ const processorSetters = useMemo<FormProcessorContextSetters>(
170
+ () => ({
171
+ setDomainObjectValue: (value) => setMutableContext((prev) => ({ ...prev, domainObjectValue: value })),
172
+ setPreviousDomainObjectValue: (value) =>
173
+ setMutableContext((prev) => ({ ...prev, previousDomainObjectValue: value })),
174
+ setCustomDependencies: (dependencies) =>
175
+ setMutableContext((prev) => ({
176
+ ...prev,
177
+ customDependencies:
178
+ typeof dependencies === 'function' ? dependencies(prev.customDependencies ?? {}) : dependencies,
179
+ })),
180
+ }),
181
+ [],
182
+ );
183
+
184
+ const { isLoading: isLoadingCustomDeps } = useProcessorDependencies(
185
+ processor,
186
+ processorContext,
187
+ setProcessorContext,
188
+ processorSetters,
189
+ );
66
190
  const useCustomHooks = processor.getCustomHooks().useCustomHooks;
67
191
  const [isLoadingCustomHooks, setIsLoadingCustomHooks] = useState(!!useCustomHooks);
68
- const [isLoadingProcessorDependencies, setIsLoadingProcessorDependencies] = useState(true);
69
192
  const {
70
193
  isLoadingInitialValues,
71
194
  initialValues,
72
195
  error: initialValuesError,
73
196
  } = useInitialValues(processor, isLoadingCustomDeps || isLoadingCustomHooks || isLoadingConcepts, processorContext);
74
197
 
75
- useEffect(() => {
76
- const isLoading = isLoadingCustomDeps || isLoadingCustomHooks || isLoadingConcepts || isLoadingInitialValues;
77
- setIsLoadingFormDependencies(isLoading);
78
- setIsLoadingProcessorDependencies(isLoading);
79
- }, [isLoadingCustomDeps, isLoadingCustomHooks, isLoadingConcepts, isLoadingInitialValues]);
198
+ // Derive loading state with useMemo to avoid effect-based state updates
199
+ const isLoadingProcessorDependencies = useMemo(
200
+ () => isLoadingCustomDeps || isLoadingCustomHooks || isLoadingConcepts || isLoadingInitialValues,
201
+ [isLoadingCustomDeps, isLoadingCustomHooks, isLoadingConcepts, isLoadingInitialValues],
202
+ );
80
203
 
204
+ // Notify parent of loading state changes
81
205
  useEffect(() => {
82
- setProcessorContext((prev) => ({
83
- ...prev,
84
- ...(formFieldAdapters && { formFieldAdapters }),
85
- ...(formFieldValidators && { formFieldValidators }),
86
- ...(formFieldsWithMeta?.length
87
- ? { formFields: formFieldsWithMeta }
88
- : rawFormFields?.length
89
- ? { formFields: rawFormFields }
90
- : {}),
91
- }));
92
- }, [formFieldAdapters, formFieldValidators, rawFormFields, formFieldsWithMeta]);
206
+ setIsLoadingFormDependencies(isLoadingProcessorDependencies);
207
+ }, [isLoadingProcessorDependencies, setIsLoadingFormDependencies]);
93
208
 
94
209
  useEffect(() => {
95
210
  reportError(initialValuesError, t('errorLoadingInitialValues', 'Error loading initial values'));
96
- }, [initialValuesError]);
211
+ }, [initialValuesError, t]);
97
212
 
98
213
  useEffect(() => {
99
214
  if (formFieldAdapters) {
@@ -107,6 +222,7 @@ const FormProcessorFactory = ({
107
222
  <CustomHooksRenderer
108
223
  context={processorContext}
109
224
  setContext={setProcessorContext}
225
+ setters={processorSetters}
110
226
  useCustomHooks={useCustomHooks}
111
227
  setIsLoadingCustomHooks={setIsLoadingCustomHooks}
112
228
  />
@@ -1,27 +1,25 @@
1
1
  import { useEffect } from 'react';
2
2
  import { type FormProcessorContextProps } from '../../types';
3
+ import { type FormProcessorContextSetters, type GetCustomHooksResponse } from '../../processors/form-processor';
3
4
 
4
5
  export const CustomHooksRenderer = ({
5
6
  context,
6
7
  setContext,
8
+ setters,
7
9
  useCustomHooks,
8
10
  setIsLoadingCustomHooks,
9
11
  }: {
10
12
  context: FormProcessorContextProps;
11
- setContext: (context: FormProcessorContextProps) => void;
12
- useCustomHooks: (context: Partial<FormProcessorContextProps>) => {
13
- data: any;
14
- isLoading: boolean;
15
- error: any;
16
- updateContext: (setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>) => void;
17
- };
13
+ setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>;
14
+ setters: FormProcessorContextSetters;
15
+ useCustomHooks: GetCustomHooksResponse['useCustomHooks'];
18
16
  setIsLoadingCustomHooks: (isLoading: boolean) => void;
19
17
  }) => {
20
18
  const { isLoading = false, error = null, data, updateContext } = useCustomHooks(context);
21
19
 
22
20
  useEffect(() => {
23
21
  if (!isLoading && updateContext) {
24
- updateContext(setContext);
22
+ updateContext(setContext, setters);
25
23
  setIsLoadingCustomHooks(false);
26
24
  }
27
25
  }, [isLoading]);
@@ -1,22 +1,32 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useMemo } from 'react';
2
+ import useSWRImmutable from 'swr/immutable';
2
3
  import { getRegisteredValidator } from '../registry/registry';
3
4
  import { type FormField, type FormFieldValidator } from '../types';
4
5
 
5
- export function useFormFieldValidators(fields: FormField[]) {
6
- const [validators, setValidators] = useState<Record<string, FormFieldValidator>>();
6
+ const EMPTY_VALIDATORS: Record<string, FormFieldValidator> = {};
7
7
 
8
- useEffect(() => {
9
- const supportedTypes = new Set<string>();
8
+ export function useFormFieldValidators(fields: FormField[]) {
9
+ const validatorTypesKey = useMemo(() => {
10
+ const uniqueTypes = new Set<string>();
10
11
  fields.forEach((field) => {
11
- field.validators?.forEach((validator) => supportedTypes.add(validator.type));
12
- });
13
- const supportedTypesArray = Array.from(supportedTypes);
14
- Promise.all(supportedTypesArray.map((type) => getRegisteredValidator(type))).then((validators) => {
15
- setValidators(
16
- Object.assign({}, ...validators.map((validator, index) => ({ [supportedTypesArray[index]]: validator }))),
17
- );
12
+ field.validators?.forEach((validator) => uniqueTypes.add(validator.type));
18
13
  });
14
+ return Array.from(uniqueTypes).sort().join(',');
19
15
  }, [fields]);
20
16
 
21
- return validators;
17
+ const { data: validators } = useSWRImmutable(
18
+ validatorTypesKey ? ['formFieldValidators', validatorTypesKey] : null,
19
+ async ([, key]) => {
20
+ const types = key.split(',');
21
+ const loadedValidators = await Promise.all(types.map((type) => getRegisteredValidator(type)));
22
+ const validatorsByType: Record<string, FormFieldValidator> = {};
23
+ types.forEach((type, index) => {
24
+ validatorsByType[type] = loadedValidators[index];
25
+ });
26
+ return validatorsByType;
27
+ },
28
+ { keepPreviousData: true },
29
+ );
30
+
31
+ return validators ?? EMPTY_VALIDATORS;
22
32
  }
@@ -1,23 +1,30 @@
1
- import { useState, useEffect } from 'react';
1
+ import { useMemo } from 'react';
2
+ import useSWRImmutable from 'swr/immutable';
2
3
  import { type FormField, type FormFieldValueAdapter } from '../types';
3
4
  import { getRegisteredFieldValueAdapter } from '../registry/registry';
4
5
 
5
- export const useFormFieldValueAdapters = (fields: FormField[]) => {
6
- const [adapters, setAdapters] = useState<Record<string, FormFieldValueAdapter>>({});
6
+ const EMPTY_ADAPTERS: Record<string, FormFieldValueAdapter> = {};
7
7
 
8
- useEffect(() => {
9
- const supportedTypes = new Set<string>();
10
- fields.forEach((field) => {
11
- supportedTypes.add(field.type);
12
- });
13
- const supportedTypesArray = Array.from(supportedTypes);
14
- Promise.all(supportedTypesArray.map((type) => getRegisteredFieldValueAdapter(type))).then((adapters) => {
15
- const adaptersByType = supportedTypesArray.map((type, index) => ({
16
- [type]: adapters[index],
17
- }));
18
- setAdapters(Object.assign({}, ...adaptersByType));
19
- });
8
+ export const useFormFieldValueAdapters = (fields: FormField[]) => {
9
+ const typesKey = useMemo(() => {
10
+ const uniqueTypes = new Set<string>();
11
+ fields.forEach((field) => uniqueTypes.add(field.type));
12
+ return Array.from(uniqueTypes).sort().join(',');
20
13
  }, [fields]);
21
14
 
22
- return adapters;
15
+ const { data: adapters } = useSWRImmutable(
16
+ typesKey ? ['formFieldValueAdapters', typesKey] : null,
17
+ async ([, key]) => {
18
+ const types = key.split(',');
19
+ const loadedAdapters = await Promise.all(types.map((type) => getRegisteredFieldValueAdapter(type)));
20
+ const adaptersByType: Record<string, FormFieldValueAdapter> = {};
21
+ types.forEach((type, index) => {
22
+ adaptersByType[type] = loadedAdapters[index];
23
+ });
24
+ return adaptersByType;
25
+ },
26
+ { keepPreviousData: true },
27
+ );
28
+
29
+ return adapters ?? EMPTY_ADAPTERS;
23
30
  };
@@ -2,64 +2,51 @@ import { useMemo } from 'react';
2
2
  import { type FormField, type FormSchema } from '../types';
3
3
 
4
4
  export function useFormFields(form: FormSchema): { formFields: FormField[]; conceptReferences: Set<string> } {
5
- const [flattenedFields, conceptReferences] = useMemo(() => {
5
+ const [flattenedFields, conceptReferencesKey] = useMemo(() => {
6
6
  const flattenedFieldsTemp: FormField[] = [];
7
7
  const conceptReferencesTemp = new Set<string>();
8
8
 
9
- const processFlattenedFields = (
10
- fields: FormField[],
11
- ): {
12
- flattenedFields: FormField[];
13
- conceptReferences: Set<string>;
14
- } => {
15
- const flattenedFields: FormField[] = [];
16
- const conceptReferences = new Set<string>();
9
+ const processField = (field: FormField, parentGroupId?: string) => {
10
+ // Add group ID to nested fields if applicable
11
+ const processedField = parentGroupId ? { ...field, meta: { ...field.meta, groupId: parentGroupId } } : field;
17
12
 
18
- const processField = (field: FormField, parentGroupId?: string) => {
19
- // Add group ID to nested fields if applicable
20
- const processedField = parentGroupId ? { ...field, meta: { ...field.meta, groupId: parentGroupId } } : field;
13
+ // Add field to flattened list
14
+ flattenedFieldsTemp.push(processedField);
21
15
 
22
- // Add field to flattened list
23
- flattenedFields.push(processedField);
16
+ // Collect concept references
17
+ if (processedField.questionOptions?.concept) {
18
+ conceptReferencesTemp.add(processedField.questionOptions.concept);
19
+ }
24
20
 
25
- // Collect concept references
26
- if (processedField.questionOptions?.concept) {
27
- conceptReferences.add(processedField.questionOptions.concept);
21
+ // Collect concept references from answers
22
+ processedField.questionOptions?.answers?.forEach((answer) => {
23
+ if (answer.concept) {
24
+ conceptReferencesTemp.add(answer.concept);
28
25
  }
26
+ });
29
27
 
30
- // Collect concept references from answers
31
- processedField.questionOptions?.answers?.forEach((answer) => {
32
- if (answer.concept) {
33
- conceptReferences.add(answer.concept);
34
- }
28
+ // Recursively process nested questions for obsGroup
29
+ if (processedField.type === 'obsGroup' && processedField.questions) {
30
+ processedField.questions.forEach((nestedField) => {
31
+ processField(nestedField, processedField.id);
35
32
  });
36
-
37
- // Recursively process nested questions for obsGroup
38
- if (processedField.type === 'obsGroup' && processedField.questions) {
39
- processedField.questions.forEach((nestedField) => {
40
- processField(nestedField, processedField.id);
41
- });
42
- }
43
- };
44
-
45
- // Process all input fields
46
- fields.forEach((field) => processField(field));
47
-
48
- return { flattenedFields, conceptReferences };
33
+ }
49
34
  };
50
35
 
36
+ // Process all input fields
51
37
  form.pages?.forEach((page) =>
52
38
  page.sections?.forEach((section) => {
53
- if (section.questions) {
54
- const { flattenedFields, conceptReferences } = processFlattenedFields(section.questions);
55
- flattenedFieldsTemp.push(...flattenedFields);
56
- conceptReferences.forEach((conceptReference) => conceptReferencesTemp.add(conceptReference));
57
- }
39
+ section.questions?.forEach((field) => processField(field));
58
40
  }),
59
41
  );
60
42
 
61
- return [flattenedFieldsTemp, conceptReferencesTemp];
43
+ // Serialised so the set below is rebuilt only when the references change. JSON rather than a
44
+ // delimiter because mapping codes may contain any character, commas included.
45
+ const sortedRefs = Array.from(conceptReferencesTemp).sort();
46
+ return [flattenedFieldsTemp, JSON.stringify(sortedRefs)];
62
47
  }, [form]);
63
48
 
49
+ const conceptReferences = useMemo(() => new Set<string>(JSON.parse(conceptReferencesKey)), [conceptReferencesKey]);
50
+
64
51
  return { formFields: flattenedFields, conceptReferences };
65
52
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useRef, useState } from 'react';
2
2
  import { type FormProcessorContextProps } from '../types';
3
3
  import { type FormProcessor } from '../processors/form-processor';
4
4
 
@@ -11,16 +11,25 @@ const useInitialValues = (
11
11
  const [initialValues, setInitialValues] = useState({});
12
12
  const [error, setError] = useState(null);
13
13
 
14
+ const contextRef = useRef(context);
15
+ contextRef.current = context;
16
+
17
+ const hasStartedLoading = useRef(false);
18
+ const hasFormFields = context.formFields?.length > 0;
19
+ const hasFormFieldAdapters = Object.keys(context.formFieldAdapters ?? {}).length > 0;
20
+
14
21
  useEffect(() => {
15
22
  if (
16
23
  formProcessor &&
17
24
  !isLoadingContextDependencies &&
18
- context.formFields?.length &&
19
- Object.keys(context.formFieldAdapters).length &&
20
- !Object.keys(initialValues).length
25
+ hasFormFields &&
26
+ hasFormFieldAdapters &&
27
+ !hasStartedLoading.current
21
28
  ) {
29
+ hasStartedLoading.current = true;
30
+ const currentContext = contextRef.current;
22
31
  formProcessor
23
- .getInitialValues(context)
32
+ .getInitialValues(currentContext)
24
33
  .then((values) => {
25
34
  setInitialValues(values);
26
35
  setIsLoadingInitialValues(false);
@@ -31,7 +40,7 @@ const useInitialValues = (
31
40
  setIsLoadingInitialValues(false);
32
41
  });
33
42
  }
34
- }, [formProcessor, isLoadingContextDependencies, context, initialValues]);
43
+ }, [formProcessor, isLoadingContextDependencies, hasFormFields, hasFormFieldAdapters]);
35
44
 
36
45
  return { isLoadingInitialValues, initialValues, error };
37
46
  };
@@ -1,12 +1,13 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { type FormProcessorContextProps } from '../types';
3
- import { type FormProcessor } from '../processors/form-processor';
3
+ import { type FormProcessor, type FormProcessorContextSetters } from '../processors/form-processor';
4
4
  import { reportError } from '../utils/error-utils';
5
5
 
6
6
  const useProcessorDependencies = (
7
7
  formProcessor: FormProcessor,
8
8
  context: Partial<FormProcessorContextProps>,
9
- setContext: (context: FormProcessorContextProps) => void,
9
+ setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>,
10
+ setters: FormProcessorContextSetters,
10
11
  ) => {
11
12
  const [isLoading, setIsLoading] = useState(false);
12
13
  const [error, setError] = useState('');
@@ -17,7 +18,7 @@ const useProcessorDependencies = (
17
18
 
18
19
  if (loadDependencies) {
19
20
  setIsLoading(true);
20
- loadDependencies(context, setContext)
21
+ loadDependencies(context, setContext, setters)
21
22
  .then(() => {
22
23
  if (!ignore) {
23
24
  setIsLoading(false);
@@ -24,7 +24,7 @@ import {
24
24
  import { evaluateAsyncExpression, type FormNode } from '../../utils/expression-runner';
25
25
  import { extractErrorMessagesFromResponse } from '../../utils/error-utils';
26
26
  import { extractObsValueAndDisplay } from '../../utils/form-helper';
27
- import { FormProcessor } from '../form-processor';
27
+ import { FormProcessor, type FormProcessorContextSetters } from '../form-processor';
28
28
  import { getPreviousEncounter, saveEncounter } from '../../api';
29
29
  import { hasRendering } from '../../utils/common-utils';
30
30
  import { isEmpty } from '../../validators/form-validator';
@@ -50,19 +50,14 @@ function useCustomHooks(context: Partial<FormProcessorContextProps>) {
50
50
  data: { encounter, patientPrograms, encounterRole },
51
51
  isLoading,
52
52
  error: null,
53
- updateContext: (setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>) => {
54
- setContext((context) => {
55
- context.processor.domainObjectValue = encounter as OpenmrsResource;
56
- return {
57
- ...context,
58
- domainObjectValue: encounter as OpenmrsResource,
59
- customDependencies: {
60
- ...context.customDependencies,
61
- patientPrograms: patientPrograms,
62
- defaultEncounterRole: encounterRole,
63
- },
64
- };
65
- });
53
+ updateContext: (_setContext, { setDomainObjectValue, setCustomDependencies }) => {
54
+ context.processor.domainObjectValue = encounter as OpenmrsResource;
55
+ setDomainObjectValue(encounter as OpenmrsResource);
56
+ setCustomDependencies((previous) => ({
57
+ ...previous,
58
+ patientPrograms: patientPrograms,
59
+ defaultEncounterRole: encounterRole,
60
+ }));
66
61
  },
67
62
  };
68
63
  }
@@ -344,16 +339,12 @@ export class EncounterFormProcessor extends FormProcessor {
344
339
 
345
340
  async loadDependencies(
346
341
  context: FormContextProps,
347
- setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>,
342
+ _setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>,
343
+ { setPreviousDomainObjectValue }: FormProcessorContextSetters,
348
344
  ) {
349
345
  const { patient, formJson } = context;
350
346
  const encounter = await getPreviousEncounter(patient?.id, formJson.encounterType);
351
- setContext((context) => {
352
- return {
353
- ...context,
354
- previousDomainObjectValue: encounter,
355
- };
356
- });
347
+ setPreviousDomainObjectValue(encounter);
357
348
  return context;
358
349
  }
359
350