Skip to content

Commit b460d84

Browse files
Simplify restoring fieldsets visibility
1 parent 60e9f22 commit b460d84

File tree

4 files changed

+317
-65
lines changed

4 files changed

+317
-65
lines changed

v0/src/calculateConditionalProperties.js

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,23 @@ function isFieldRequired(node, field) {
2828
/**
2929
* Loops recursively through fieldset fields and returns an copy version of them
3030
* where the required property is updated.
31+
* Since rebuildFieldset is called within a closure, we pass the current fields as parameter
32+
* to restore the computed isVisible property.
3133
*
3234
* @param {Array} fields - list of fields of a fieldset
3335
* @param {Object} property - property that relates with the list of fields
3436
* @returns {Object}
3537
*/
36-
function rebuildFieldset(fields, property) {
38+
function rebuildFieldset(fields, currentFields, property) {
3739
if (property?.properties) {
38-
return fields.map((field) => {
40+
return fields.map((field, index) => {
3941
const propertyConditionals = property.properties[field.name];
42+
const isVisible = currentFields[index].isVisible;
4043
if (!propertyConditionals) {
41-
return field;
44+
return {
45+
...field,
46+
isVisible,
47+
};
4248
}
4349

4450
const newFieldParams = extractParametersFromNode(propertyConditionals);
@@ -47,19 +53,22 @@ function rebuildFieldset(fields, property) {
4753
return {
4854
...field,
4955
...newFieldParams,
50-
fields: rebuildFieldset(field.fields, propertyConditionals),
56+
isVisible,
57+
fields: rebuildFieldset(field.fields, currentFields[index].fields, propertyConditionals),
5158
};
5259
}
5360
return {
5461
...field,
5562
...newFieldParams,
63+
isVisible,
5664
required: isFieldRequired(property, field),
5765
};
5866
});
5967
}
6068

61-
return fields.map((field) => ({
69+
return fields.map((field, index) => ({
6270
...field,
71+
isVisible: currentFields[index].isVisible,
6372
required: isFieldRequired(property, field),
6473
}));
6574
}
@@ -92,7 +101,7 @@ export function calculateConditionalProperties({ fieldParams, customProperties,
92101
*
93102
* @returns {calculateConditionalPropertiesReturn}
94103
*/
95-
return ({ isRequired, conditionBranch, formValues }) => {
104+
return ({ isRequired, conditionBranch, formValues, currentField }) => {
96105
// Check if the current field is conditionally declared in the schema
97106
// console.log('::calc (closure original)', fieldParams.description);
98107
const conditionalProperty = conditionBranch?.properties?.[fieldParams.name];
@@ -110,7 +119,11 @@ export function calculateConditionalProperties({ fieldParams, customProperties,
110119
let fieldSetFields;
111120

112121
if (fieldParams.inputType === supportedTypes.FIELDSET) {
113-
fieldSetFields = rebuildFieldset(fieldParams.fields, conditionalProperty);
122+
fieldSetFields = rebuildFieldset(
123+
fieldParams.fields,
124+
currentField.fields,
125+
conditionalProperty
126+
);
114127
newFieldParams.fields = fieldSetFields;
115128
}
116129

v0/src/helpers.js

Lines changed: 5 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ function getPrefillSubFieldValues(field, defaultValues, parentFieldKeyPath) {
161161
initialValue[field.name] = subFieldValues;
162162
}
163163
} else {
164-
// getDefaultValues and getPrefillSubFieldValues have a circluar dependency, resulting in one having to be used before defined.
164+
// getDefaultValues and getPrefillSubFieldValues have a circular dependency, resulting in one having to be used before defined.
165165
// As function declarations are hoisted this should not be a problem.
166166
// eslint-disable-next-line no-use-before-define
167167

@@ -213,46 +213,6 @@ export function getPrefillValues(fields, initialValues = {}) {
213213
return initialValues;
214214
}
215215

216-
/**
217-
* Preserves the visibility of nested fields in a fieldset
218-
* @param {Object} field - field object
219-
* @param {String} parentPath - path to the parent field
220-
* @returns {Object} - object with a map of the visibility of the nested fields, e.g. { 'parent.child': true }
221-
*/
222-
function preserveNestedFieldsVisibility(field, parentPath = '') {
223-
return field.fields?.reduce?.((acc, f) => {
224-
const path = parentPath ? `${parentPath}.${f.name}` : f.name;
225-
if (!isNil(f.isVisible)) {
226-
acc[path] = f.isVisible;
227-
}
228-
229-
if (f.fields) {
230-
Object.assign(acc, preserveNestedFieldsVisibility(f, path));
231-
}
232-
return acc;
233-
}, {});
234-
}
235-
236-
/**
237-
* Restores the visibility of nested fields in a fieldset
238-
* @param {Object} field - field object
239-
* @param {Object} nestedFieldsVisibility - object with a map of the visibility of the nested fields, e.g. { 'parent.child': true }
240-
* @param {String} parentPath - path to the parent field
241-
*/
242-
function restoreNestedFieldsVisibility(field, nestedFieldsVisibility, parentPath = '') {
243-
field.fields.forEach((f) => {
244-
const path = parentPath ? `${parentPath}.${f.name}` : f.name;
245-
const visibility = get(nestedFieldsVisibility, path);
246-
if (!isNil(visibility)) {
247-
f.isVisible = visibility;
248-
}
249-
250-
if (f.fields) {
251-
restoreNestedFieldsVisibility(f, nestedFieldsVisibility, path);
252-
}
253-
});
254-
}
255-
256216
/**
257217
* Updates field properties based on the current JSON-schema node and the required fields
258218
*
@@ -283,22 +243,10 @@ function updateField(field, requiredFields, node, formValues, logic, config) {
283243
field.isVisible = true;
284244
}
285245

286-
// Store current visibility of fields within a fieldset before updating its attributes
287-
const nestedFieldsVisibility = preserveNestedFieldsVisibility(field);
288-
289246
const updateAttributes = (fieldAttrs) => {
290247
Object.entries(fieldAttrs).forEach(([key, value]) => {
291248
field[key] = value;
292249

293-
// If the field is a fieldset, restore the visibility of the fields within it.
294-
// If this is not in place, calling updateField for multiple conditionals touching
295-
// the same fieldset will unset previously calculated visibility for the nested fields.
296-
// This is because rebuildFieldset is called via the calculateConditionalProperties closure
297-
// created at the time of building the fields, and it returns a new fieldset.fields array
298-
if (key === 'fields' && !isNil(nestedFieldsVisibility)) {
299-
restoreNestedFieldsVisibility(field, nestedFieldsVisibility);
300-
}
301-
302250
if (key === 'schema' && typeof value === 'function') {
303251
// key "schema" refers to YupSchema that needs to be processed for validations.
304252
field[key] = value();
@@ -347,6 +295,7 @@ function updateField(field, requiredFields, node, formValues, logic, config) {
347295
isRequired: fieldIsRequired,
348296
conditionBranch: node,
349297
formValues,
298+
currentField: field,
350299
});
351300
updateAttributes(newAttributes);
352301
removeConditionalStaleAttributes(field, newAttributes, rootFieldAttrs);
@@ -416,7 +365,7 @@ export function processNode({
416365

417366
if (node.if !== undefined) {
418367
const matchesCondition = checkIfConditionMatchesProperties(node, formValues, formFields, logic);
419-
// BUG HERE (unreleated) - what if it matches but doesn't has a then,
368+
// BUG HERE (unrelated) - what if it matches but doesn't has a then,
420369
// it should do nothing, but instead it jumps to node.else when it shouldn't.
421370
if (matchesCondition && node.then) {
422371
const { required: branchRequired } = processNode({
@@ -500,14 +449,14 @@ export function processNode({
500449
function clearValuesIfNotVisible(fields, formValues) {
501450
fields.forEach(({ isVisible = true, name, inputType, fields: nestedFields }) => {
502451
if (!isVisible) {
503-
// TODO I (Sandrina) think this doesn't work. I didn't find any test covering this scenario. Revisit later.
504452
formValues[name] = null;
505453
}
506454
if (inputType === supportedTypes.FIELDSET && nestedFields && formValues[name]) {
507455
clearValuesIfNotVisible(nestedFields, formValues[name]);
508456
}
509457
});
510458
}
459+
511460
/**
512461
* Updates form fields properties based on the current form state and the JSON schema rules
513462
*
@@ -549,7 +498,7 @@ function getFieldOptions(node, presentation) {
549498
}));
550499
}
551500

552-
/** @deprecated - takes precendence in case a JSON Schema still has deprecated options */
501+
/** @deprecated - takes precedence in case a JSON Schema still has deprecated options */
553502
if (presentation.options) {
554503
return presentation.options;
555504
}

v0/src/tests/createHeadlessForm.test.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2307,6 +2307,85 @@ describe('createHeadlessForm', () => {
23072307
).toBeUndefined();
23082308
});
23092309
});
2310+
2311+
describe('supports computed values based on values from nested fieldsets', () => {
2312+
it("computed value for total_contributions is calculated correctly with defaults when user selects 'yes' for has_retirement_plan", () => {
2313+
const { fields, handleValidation } = createHeadlessForm(
2314+
schemaWithNestedFieldsetsConditionals,
2315+
{}
2316+
);
2317+
const validateForm = (vals) => friendlyError(handleValidation(vals));
2318+
2319+
expect(
2320+
validateForm({
2321+
perks: {
2322+
benefits_package: 'basic',
2323+
has_retirement_plan: 'yes',
2324+
declare_amount: 'no',
2325+
retirement_plan: {
2326+
plan_name: 'test',
2327+
create_plan: 'no',
2328+
},
2329+
},
2330+
})
2331+
).toEqual({ perks: { retirement_plan: { year: 'Required field' } } });
2332+
2333+
expect(getField(fields, 'total_contributions').isVisible).toBe(true);
2334+
expect(getField(fields, 'total_contributions').default).toBe(0);
2335+
expect(getField(fields, 'total_contributions').const).toBe(0);
2336+
});
2337+
2338+
it('computed value for total_contributions is calculated correctly based on the selected months', () => {
2339+
const { fields, handleValidation } = createHeadlessForm(
2340+
schemaWithNestedFieldsetsConditionals,
2341+
{}
2342+
);
2343+
const validateForm = (vals) => friendlyError(handleValidation(vals));
2344+
2345+
expect(
2346+
validateForm({
2347+
perks: {
2348+
benefits_package: 'basic',
2349+
has_retirement_plan: 'yes',
2350+
declare_amount: 'no',
2351+
retirement_plan: {
2352+
plan_name: 'test',
2353+
year: 2025,
2354+
create_plan: 'yes',
2355+
planned_contributions: {
2356+
months: ['january', 'february', 'march', 'april', 'may'],
2357+
},
2358+
},
2359+
},
2360+
})
2361+
).toBeUndefined();
2362+
2363+
expect(getField(fields, 'total_contributions').isVisible).toBe(true);
2364+
expect(getField(fields, 'total_contributions').default).toBe(5);
2365+
expect(getField(fields, 'total_contributions').const).toBe(5);
2366+
2367+
expect(
2368+
validateForm({
2369+
perks: {
2370+
benefits_package: 'basic',
2371+
has_retirement_plan: 'yes',
2372+
declare_amount: 'no',
2373+
retirement_plan: {
2374+
plan_name: 'test',
2375+
year: 2025,
2376+
create_plan: 'yes',
2377+
planned_contributions: {
2378+
months: ['january', 'february', 'march'],
2379+
},
2380+
},
2381+
},
2382+
})
2383+
).toBeUndefined();
2384+
2385+
expect(getField(fields, 'total_contributions').default).toBe(3);
2386+
expect(getField(fields, 'total_contributions').const).toBe(3);
2387+
});
2388+
});
23102389
});
23112390

23122391
it('support "email" field type', () => {

0 commit comments

Comments
 (0)