Skip to content

Commit

Permalink
tidy autocomplete and dependent wrappers
Browse files Browse the repository at this point in the history
  • Loading branch information
ttoomey committed Jan 12, 2025
1 parent a1dea86 commit 97d37aa
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 43 deletions.
56 changes: 27 additions & 29 deletions src/modules/form/components/AutofillFormItemWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,50 +11,48 @@ export interface Props {
handlers: FormDefinitionHandlers;
item: FormItem;
children: (value: any) => ReactNode;
getDependentLinkIds?: (item: FormItem) => string[];
}

/**
* A wrapper component that manages automatic field value population based on dependencies.
*
* This component handles the auto-population of form fields based on the values of other fields.
* It watches for changes in dependent fields and updates the wrapped field's value accordingly.
* The autofill behavior is skipped if the field has been manually edited by the user.
*
*/
const AutofillFormItemWrapper: React.FC<Props> = ({
handlers,
item,
children,
getDependentLinkIds,
}) => {
const linkId = getSafeLinkId(item.linkId);
// dependentLinkIds are referenced from the 'autofill_when' property of item
// example:
// link_id: item.id,
// autofill_values: [ { autofill_when: [{ question: dependentLinkIds[0] }] } ],
const { autofillInvertedDependencyMap, getAutofillValueForField } = handlers;
const dependentLinkIds = (
getDependentLinkIds
? getDependentLinkIds(item)
: autofillInvertedDependencyMap[item.linkId]
)?.map(getSafeLinkId);

// Listen for dependent field value changes
useWatch({
control: handlers.methods.control,
name: dependentLinkIds,
});
// Listen to see if this field is changed
const { isDirty } = useFormState({
control: handlers.methods.control,
name: item.linkId,
});
const dependentLinkIds =
autofillInvertedDependencyMap[item.linkId].map(getSafeLinkId);
const { setValue, getValues, control } = handlers.methods;

// subscribe to changes in the dependent field values
useWatch({ control: control, name: dependentLinkIds });
// not memoized, we rely on useWatch to re-render here when dependentLinkIds change
const autofillValue = getAutofillValueForField(item);

// Listen to see if this field has been edited by the user
const { isDirty } = useFormState({ control: control, name: linkId });

useEffect(() => {
// Don't autofill this field if it's been edited (i.e. is dirty)
if (isDirty) return;

const linkId = getSafeLinkId(item.linkId);
// Dont autofill if this is already the same value
if (
autofillValue === handlers.methods.getValues()[getSafeLinkId(item.linkId)]
)
return;

handlers.methods.setValue(getSafeLinkId(linkId), autofillValue, {
shouldDirty: false,
});
}, [autofillValue, item, handlers.methods, isDirty]);
// Don't autofill if this is already the same value
if (autofillValue === getValues(linkId)) return;

setValue(linkId, autofillValue, { shouldDirty: false });
}, [autofillValue, linkId, getValues, setValue, isDirty]);

return <>{children(autofillValue)}</>;
};
Expand Down
61 changes: 47 additions & 14 deletions src/modules/form/components/DependentFormItemWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { compact, flattenDeep, isEmpty } from 'lodash-es';
import { compact, flattenDeep, uniq } from 'lodash-es';
import React, { ReactNode, useMemo } from 'react';
import { useWatch } from 'react-hook-form';

Expand All @@ -15,6 +15,16 @@ export interface Props {
children: (isDisabled: boolean) => ReactNode;
}

/**
* A wrapper component that manages conditional display and disable states for form items
* and their children based on form dependencies.
*
* This component:
* - Tracks dependencies between form fields that affect enabled/disabled states
* - Listens for changes to dependent field values
* - Manages visibility based on disabled state and display rules
* - Handles nested item dependencies
*/
const DependentFormItemWrapper: React.FC<Props> = ({
handlers,
item,
Expand All @@ -32,6 +42,12 @@ const DependentFormItemWrapper: React.FC<Props> = ({
[item, itemMap]
);

/**
* Computes the list of all link IDs that affect the enabled/disabled state of this item
* and its children. This includes:
* 1. Direct dependencies of this item
* 2. Dependencies of all child items that have enableWhen conditions
*/
const dependentLinkIds = useMemo(() => {
const list: string[] = [
// All of this component's dependencies
Expand All @@ -44,30 +60,47 @@ const DependentFormItemWrapper: React.FC<Props> = ({
),
];

return list.map(getSafeLinkId);
return uniq(list.map(getSafeLinkId));
}, [item, childItems, disabledDependencyMap]);

// Listen for dependent field value changes
useWatch({
const dependantValues = useWatch({
control: handlers.methods.control,
name: dependentLinkIds,
});

const isDisabled = isItemDisabled(item);
const [isDisabled] = useMemo(
() => [isItemDisabled(item), dependantValues],
[isItemDisabled, item, dependantValues]
);

/**
* Determines if the item should be hidden based on:
* 1. If the item is disabled and configured to be hidden when disabled
* 2. If all child items are disabled and configured to be hidden when disabled
*/
const [hidden] = useMemo<[boolean, any]>(() => {
// Hide if this item should be disabled
if (isDisabled && item.disabledDisplay === DisabledDisplay.Hidden)
return [true, null];

// Hide if this item should be disabled
if (isDisabled && item.disabledDisplay === DisabledDisplay.Hidden)
return null;
// Hide if all of this item's children are disabled
if (
!isEmpty(childItems) &&
childItems.every(
// Hide if all of this item's children are disabled
if (childItems.length === 0) return [false, null];
const childrenHidden = childItems.every(
(child) =>
isItemDisabled(child) &&
child.disabledDisplay === DisabledDisplay.Hidden
)
)
return null;
);
return [childrenHidden, dependantValues];
}, [
isDisabled,
item.disabledDisplay,
isItemDisabled,
childItems,
dependantValues,
]);

if (hidden) return null;

return <>{children(isDisabled)}</>;
};
Expand Down

0 comments on commit 97d37aa

Please sign in to comment.