Skip to content

Commit

Permalink
✨ [#52] Implement defaultValue field in edit form
Browse files Browse the repository at this point in the history
When options are added, changed, removed or re-ordered, the defaultValue
configuration should reflect this. It may not include stale options, and
for the sake of explicitness, every option that's present should be
included with an explicit default value so that the form renderer
has a reliable data structure to work with.
  • Loading branch information
sergei-maertens committed Nov 16, 2023
1 parent 5b029c3 commit 201c878
Showing 1 changed file with 66 additions and 10 deletions.
76 changes: 66 additions & 10 deletions src/registry/selectboxes/edit.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {SelectboxesComponentSchema} from '@open-formulieren/types';
import {Option} from '@open-formulieren/types/lib/formio/common';
import {useFormikContext} from 'formik';
import isEqual from 'lodash.isequal';
import {useLayoutEffect} from 'react';
import {FormattedMessage, useIntl} from 'react-intl';

import {
Expand All @@ -21,7 +23,7 @@ import {
useDeriveComponentKey,
} from '@/components/builder';
import {LABELS} from '@/components/builder/messages';
import {TabList, TabPanel, Tabs} from '@/components/formio';
import {SelectBoxes, TabList, TabPanel, Tabs} from '@/components/formio';
import {getErrorNames} from '@/utils/errors';

import {EditFormDefinition} from '../types';
Expand All @@ -33,7 +35,11 @@ import {checkIsManualOptions} from './helpers';
const EditForm: EditFormDefinition<SelectboxesComponentSchema> = () => {
const intl = useIntl();
const [isKeyManuallySetRef, generatedKey] = useDeriveComponentKey();
const {values, errors} = useFormikContext<SelectboxesComponentSchema>();
const {values, errors, setFieldValue} = useFormikContext<SelectboxesComponentSchema>();
const {
openForms: {dataSrc},
defaultValue,
} = values;

const erroredFields = Object.keys(errors).length
? getErrorNames<SelectboxesComponentSchema>(errors)
Expand All @@ -48,7 +54,23 @@ const EditForm: EditFormDefinition<SelectboxesComponentSchema> = () => {

Validate.useManageValidatorsTranslations<SelectboxesComponentSchema>(['required']);

const options = checkIsManualOptions(values) ? values.values || [] : [];
const isManualOptions = checkIsManualOptions(values);
const options = isManualOptions ? values.values || [] : [];

// Ensure that form state is reset if the values source changes.
useLayoutEffect(() => {
switch (dataSrc) {
case 'variable': {
if (isEqual(defaultValue, {})) return;
setFieldValue('defaultValue', {});
break;
}
case 'manual': {
setFieldValue('values', [{value: '', label: '', openForms: {translations: {}}}]);
break;
}
}
}, [dataSrc]);

return (
<Tabs>
Expand Down Expand Up @@ -88,7 +110,7 @@ const EditForm: EditFormDefinition<SelectboxesComponentSchema> = () => {
<ClearOnHide />
<IsSensitiveData />
<ValuesConfig<SelectboxesComponentSchema> name="values" />
<DefaultValue options={options} />
{isManualOptions && <DefaultValue options={options} />}
</TabPanel>

{/* Advanced tab */}
Expand Down Expand Up @@ -165,17 +187,51 @@ interface DefaultValueProps {

const DefaultValue: React.FC<DefaultValueProps> = ({options}) => {
const intl = useIntl();
const label = <FormattedMessage {...LABELS.defaultValue} />;
const {getFieldProps, setFieldValue} = useFormikContext<SelectboxesComponentSchema>();
const {value = {}} = getFieldProps<SelectboxesComponentSchema['defaultValue'] | undefined>(
'defaultValue'
);

const tooltip = intl.formatMessage({
description: "Tooltip for 'defaultValue' builder field",
defaultMessage: 'This will be the initial value for this field before user interaction.',
});

// This layout effect uses a non-primitive dependency. It works *because of Formik*
// implementation details, wich uses refs internally and changes the identity of the
// options field only when mutations are made to it (add, change items, re-ordering...)
useLayoutEffect(() => {
const optionValues = options.map(opt => opt.value);
const defaultValueKeys = new Set(Object.keys(value));

// if all the option values are present in the default value map, there is nothing
// to do and we bail early to prevent further form state mutations.
if (defaultValueKeys === new Set(optionValues)) return;

// If no default value is present for an option, make it explicitly false.
// Checking/unchecking persist the state either way, so we only need to do this once
// if an option is present yet.
//
// Additionally, we start with an empty object so that we can drop/discard any default
// values for options that were removed.
const explicitDefaults: SelectboxesComponentSchema['defaultValue'] = {};
optionValues.forEach(optionValue => {
// if a value is specified already in the form state, use it, otherwise default to "unchecked".
const defaultForOption = value.hasOwnProperty(optionValue) ? value[optionValue] : false;
explicitDefaults[optionValue] = defaultForOption;
});

setFieldValue('defaultValue', explicitDefaults);
console.log('normalized defaultValue');
}, [options]);

return (
<div>
{label}
<pre>{JSON.stringify(options, null, 2)}</pre>
{tooltip}
</div>
<SelectBoxes
name="defaultValue"
options={options}
label={<FormattedMessage {...LABELS.defaultValue} />}
tooltip={tooltip}
/>
);
};

Expand Down

0 comments on commit 201c878

Please sign in to comment.