Skip to content

Commit 32f0a3a

Browse files
authored
feat(onboarding) - change how we render conditional fields (#793)
* feat(onboarding) - change how we render conditional fields * fix old cases
1 parent 7835812 commit 32f0a3a

7 files changed

Lines changed: 137 additions & 43 deletions

File tree

example/src/Onboarding.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ const OnboardingWithProps = ({
269269
DEU: {
270270
contract_details: 1,
271271
},
272+
CHN: {
273+
contract_details: 2,
274+
},
272275
},
273276
}}
274277
/>

src/components/form/JSONSchemaForm.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ function checkFieldHasForcedValue(field: any) {
2222
return (
2323
field.const !== undefined && // Only accepts a specific value
2424
field.const === field.default && // It can be prefilled, meaning it's not critical
25-
field.inputType !== 'checkbox' && // Because checkbox must always be visible
26-
field.inputType !== 'hidden' // Because hidden inputs shouldn't be visible
25+
field.type !== 'checkbox' && // Because checkbox must always be visible
26+
field.type !== 'hidden' // Because hidden inputs shouldn't be visible
2727
);
2828
}
2929

@@ -94,18 +94,23 @@ export const JSONSchemaFormFields = ({
9494
);
9595
}
9696

97-
let FieldComponent =
98-
fieldsMap[field.inputType as keyof typeof fieldsMap];
97+
// We use field.type on purpose here, not field.inputType, when using field.inputType
98+
// the conditionals didn't work as expected
99+
// I believe json-schema-form in the latest versions uses field.inputType correctly but
100+
// the version of json-schema-form is decided by remote-json-schema-form-kit
101+
// our product uses field.type instead of field.inputType and we probably should do the same
102+
const fieldType = field.type;
103+
let FieldComponent = fieldsMap[fieldType as keyof typeof fieldsMap];
99104

100105
if (!FieldComponent) {
101106
return (
102107
<p className='error'>
103-
Field type {field.inputType as string} not supported
108+
Field type {fieldType as string} not supported
104109
</p>
105110
);
106111
}
107112

108-
if (field.inputType === 'fieldset') {
113+
if (fieldType === 'fieldset') {
109114
return (
110115
<FieldComponent
111116
key={field.name}
@@ -115,7 +120,7 @@ export const JSONSchemaFormFields = ({
115120
);
116121
}
117122

118-
if (field.inputType === 'fieldset-flat') {
123+
if (fieldType === 'fieldset-flat') {
119124
return (
120125
<FieldComponent
121126
key={field.name}
@@ -127,7 +132,7 @@ export const JSONSchemaFormFields = ({
127132
}
128133

129134
// TODO: Have doubts about this, it seems we only support checkbox for multiple select
130-
if (field.inputType === 'select' && field.multiple) {
135+
if (fieldType === 'select' && field.multiple) {
131136
FieldComponent = fieldsMap['multi-select'];
132137
}
133138

@@ -136,7 +141,7 @@ export const JSONSchemaFormFields = ({
136141
<FieldComponent
137142
{...field}
138143
component={
139-
components && components[field.inputType as keyof Components]
144+
components && components[fieldType as keyof Components]
140145
}
141146
/>
142147
{field.statement ? (

src/components/form/fields/FieldSetField.tsx

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,10 @@ export function FieldSetField({
226226
return null; // Skip hidden or deprecated fields
227227
}
228228

229+
const fieldType = field.type;
230+
229231
// Handle nested fieldsets
230-
if (field.inputType === 'fieldset') {
232+
if (fieldType === 'fieldset') {
231233
return (
232234
<FieldSetField
233235
key={`${isFlatFieldset ? field.name : `${name}.${field.name}`}`}
@@ -238,7 +240,7 @@ export function FieldSetField({
238240
);
239241
}
240242

241-
if (field.inputType === 'fieldset-flat') {
243+
if (fieldType === 'fieldset-flat') {
242244
return (
243245
<FieldSetField
244246
key={`${isFlatFieldset ? field.name : `${name}.${field.name}`}`}
@@ -249,9 +251,9 @@ export function FieldSetField({
249251
/>
250252
);
251253
}
252-
// We need to do the check after checking field.inputType === 'fieldset' or field.inputType === 'fieldset-flat'
254+
// We need to do the check after checking (field.type || field.inputType) === 'fieldset' or (field.type || field.inputType) === 'fieldset-flat'
253255
// circular dependency most likely
254-
let FieldComponent = baseFields[field.inputType as BaseTypes];
256+
let FieldComponent = baseFields[fieldType as BaseTypes];
255257

256258
if (field.Component) {
257259
const { Component } = field as {
@@ -282,13 +284,11 @@ export function FieldSetField({
282284

283285
if (!FieldComponent) {
284286
return (
285-
<p className='error'>
286-
Field type {field.inputType as string} not supported
287-
</p>
287+
<p className='error'>Field type {fieldType} not supported</p>
288288
);
289289
}
290290

291-
if (field.inputType === 'select' && field.multiple) {
291+
if (fieldType === 'select' && field.multiple) {
292292
FieldComponent = baseFields['multi-select'];
293293
}
294294

@@ -299,9 +299,7 @@ export function FieldSetField({
299299
<FieldComponent
300300
{...field}
301301
name={`${isFlatFieldset ? field.name : `${name}.${field.name}`}`}
302-
component={
303-
components?.[field.inputType as keyof Components]
304-
}
302+
component={components?.[fieldType as keyof Components]}
305303
/>
306304
{field.statement ? (
307305
<Statement

src/components/form/fields/tests/FieldSetField.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ describe('FieldSetField', () => {
291291
label: 'Test',
292292
description: '',
293293
inputType: 'unknown-type' as $TSFixMe,
294+
type: 'unknown-type' as $TSFixMe,
294295
} as $TSFixMe,
295296
],
296297
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { render, screen, waitFor } from '@testing-library/react';
2+
import { FormProvider, useForm } from 'react-hook-form';
3+
import userEvent from '@testing-library/user-event';
4+
import { JSONSchemaFormFields } from '@/src/components/form/JSONSchemaForm';
5+
import { RadioGroupFieldDefault } from '@/src/components/form/fields/default/RadioGroupFieldDefault';
6+
import { NumberFieldDefault } from '@/src/components/form/fields/default/NumberFieldDefault';
7+
import { TextFieldDefault } from '@/src/components/form/fields/default/TextFieldDefault';
8+
import { SelectFieldDefault } from '@/src/components/form/fields/default/SelectFieldDefault';
9+
10+
vi.mock('@/src/context', () => ({
11+
useFormFields: vi.fn(() => ({
12+
components: {
13+
radio: RadioGroupFieldDefault,
14+
number: NumberFieldDefault,
15+
text: TextFieldDefault,
16+
select: SelectFieldDefault,
17+
},
18+
})),
19+
}));
20+
21+
describe('JSONSchemaForm - Conditional inputType Changes', () => {
22+
it('should render NumberField when field.type changes from hidden to number', async () => {
23+
const user = userEvent.setup();
24+
25+
const TestComponent = () => {
26+
const methods = useForm({
27+
defaultValues: { choice: 'hidden', value: '30' },
28+
});
29+
30+
const choice = methods.watch('choice');
31+
32+
// Simulate how json-schema-form updates fields based on conditionals
33+
// Initially: inputType stays 'hidden', but type changes to 'number'
34+
const fields = [
35+
{
36+
name: 'choice',
37+
label: 'Choice',
38+
inputType: 'radio' as const,
39+
type: 'radio',
40+
isVisible: true,
41+
jsonType: 'string',
42+
required: true,
43+
options: [
44+
{ value: 'hidden', label: 'Hidden' },
45+
{ value: 'number', label: 'Number' },
46+
],
47+
},
48+
{
49+
name: 'value',
50+
label: 'Value',
51+
// inputType stays at the initial value (this is the bug behavior)
52+
inputType: 'hidden' as const,
53+
// type changes based on conditional (this is what we should use)
54+
type: choice === 'number' ? 'number' : 'hidden',
55+
isVisible: true,
56+
jsonType: choice === 'number' ? 'number' : 'string',
57+
required: choice === 'number',
58+
},
59+
];
60+
61+
return (
62+
<FormProvider {...methods}>
63+
<JSONSchemaFormFields fields={fields} />
64+
<div data-testid='current-choice'>{choice}</div>
65+
</FormProvider>
66+
);
67+
};
68+
69+
render(<TestComponent />);
70+
71+
// Initially, choice is 'hidden', so value field should be hidden
72+
expect(screen.queryByLabelText('Value')).not.toBeInTheDocument();
73+
expect(screen.getByTestId('current-choice')).toHaveTextContent('hidden');
74+
75+
// Change choice to 'number'
76+
const numberRadio = screen.getByLabelText('Number');
77+
await user.click(numberRadio);
78+
79+
await waitFor(() => {
80+
expect(screen.getByTestId('current-choice')).toHaveTextContent('number');
81+
});
82+
83+
// Now the value field should render as a number input (not hidden)
84+
// The fix makes it use field.type='number' instead of field.inputType='hidden'
85+
await waitFor(() => {
86+
const input = screen.getByLabelText('Value');
87+
expect(input).toBeInTheDocument();
88+
// NumberField renders as TextField with proper input, not hidden
89+
expect(input).not.toHaveAttribute('type', 'hidden');
90+
});
91+
});
92+
});

src/components/form/tests/JSONSchemaFormCustomComponent.test.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ describe('Custom JSF Component with setValue', () => {
106106
label: 'Payment Information',
107107
description: 'Payment details',
108108
inputType: 'fieldset' as const,
109+
type: 'fieldset' as const,
109110
fields: [
110111
{
111112
name: 'payment_type',
@@ -116,6 +117,7 @@ describe('Custom JSF Component with setValue', () => {
116117
{ value: 'weekly', label: 'Weekly' },
117118
],
118119
inputType: 'text' as const,
120+
type: 'text' as const,
119121
},
120122
],
121123
isFlatFieldset: false,
@@ -261,6 +263,7 @@ describe('Custom JSF Component with setValue', () => {
261263
label: 'Payment Information',
262264
description: 'Payment details',
263265
inputType: 'fieldset' as const,
266+
type: 'fieldset' as const,
264267
fields: [
265268
{
266269
name: 'payment_type',
@@ -271,6 +274,7 @@ describe('Custom JSF Component with setValue', () => {
271274
{ value: 'weekly', label: 'Weekly' },
272275
],
273276
inputType: 'text' as const,
277+
type: 'text' as const,
274278
statement: {
275279
title: 'Fieldset Statement Title',
276280
description: 'This statement is inside a fieldset.',
@@ -325,6 +329,7 @@ describe('Custom JSF Component with setValue', () => {
325329
label: 'Payment Information',
326330
description: 'Payment details',
327331
inputType: 'fieldset' as const,
332+
type: 'fieldset' as const,
328333
fields: [
329334
{
330335
name: 'payment_type',
@@ -335,6 +340,7 @@ describe('Custom JSF Component with setValue', () => {
335340
{ value: 'weekly', label: 'Weekly' },
336341
],
337342
inputType: 'text' as const,
343+
type: 'text' as const,
338344
extra: <ExtraContent />,
339345
},
340346
],

src/components/form/utils.ts

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -133,17 +133,11 @@ function prefillReadOnlyFields(values: Record<string, any>, fields: any[]) {
133133

134134
if (
135135
!Object.prototype.hasOwnProperty.call(values, fieldName!) &&
136-
!(
137-
field.inputType === supportedTypes.FIELDSET &&
138-
field.valueGroupingDisabled
139-
)
136+
!(field.type === supportedTypes.FIELDSET && field.valueGroupingDisabled)
140137
)
141138
return;
142139

143-
if (
144-
field.inputType === supportedTypes.FIELDSET &&
145-
field.valueGroupingDisabled
146-
) {
140+
if (field.type === supportedTypes.FIELDSET && field.valueGroupingDisabled) {
147141
Object.assign(newValues, prefillReadOnlyFields(values, field.fields));
148142
return;
149143
}
@@ -172,7 +166,7 @@ function extractFieldsetFieldsValues(
172166
) {
173167
return fields.reduce<Record<string, any>>((nestedAcc, subField) => {
174168
const isFieldsetValueGroupingDisabled =
175-
subField.inputType === supportedTypes.FIELDSET &&
169+
subField.type === supportedTypes.FIELDSET &&
176170
subField.valueGroupingDisabled;
177171

178172
if (isFieldsetValueGroupingDisabled) {
@@ -305,15 +299,14 @@ export async function parseFormValuesToAPI(
305299
const filteredFields = fields.filter(
306300
(field) =>
307301
formValues[field.name!] ||
308-
(field.inputType === supportedTypes.FIELDSET &&
309-
field.valueGroupingDisabled),
302+
(field.type === supportedTypes.FIELDSET && field.valueGroupingDisabled),
310303
);
311304

312305
const parsedFieldsWithValues = await Promise.all(
313306
filteredFields.map(async (field) => {
314307
const acc: Record<string, any> = {};
315308

316-
switch (field.inputType) {
309+
switch (field.type) {
317310
case supportedTypes.FIELDSET: {
318311
const fieldset = field;
319312
if (fieldset.valueGroupingDisabled) {
@@ -387,8 +380,7 @@ export async function parseFormValuesToAPI(
387380
const formValue = formValues[extraField.name];
388381
const fieldTransformValueToAPI =
389382
extraField?.transformValueToAPI ||
390-
fieldTypesTransformations[extraField.inputType]
391-
?.transformValueToAPI;
383+
fieldTypesTransformations[extraField.type]?.transformValueToAPI;
392384

393385
if (fieldTransformValueToAPI) {
394386
const result = fieldTransformValueToAPI(field)(formValue);
@@ -409,7 +401,7 @@ export async function parseFormValuesToAPI(
409401
const formValue = formValues[field.name];
410402
const fieldTransformValueToAPI =
411403
field?.transformValueToAPI ||
412-
fieldTypesTransformations[field.inputType]?.transformValueToAPI;
404+
fieldTypesTransformations[field.type]?.transformValueToAPI;
413405

414406
if (fieldTransformValueToAPI) {
415407
const result = fieldTransformValueToAPI(field)(formValue);
@@ -496,7 +488,7 @@ function excludeValuesInvisible(
496488
return;
497489
}
498490

499-
if (field.inputType === 'fieldset' && field.valueGroupingDisabled) {
491+
if (field.type === 'fieldset' && field.valueGroupingDisabled) {
500492
Object.assign(
501493
valuesAsked,
502494
excludeValuesInvisible(
@@ -614,7 +606,7 @@ function getInitialDefaultValue(
614606
const defaultFieldValue = get(defaultValues, field.name);
615607
const fieldTransformValueFromAPI =
616608
field?.transformValueFromAPI ||
617-
fieldTypesTransformations[field.inputType]?.transformValueFromAPI;
609+
fieldTypesTransformations[field.type]?.transformValueFromAPI;
618610

619611
if (fieldTransformValueFromAPI) {
620612
return fieldTransformValueFromAPI(field)(defaultFieldValue);
@@ -643,7 +635,7 @@ function getInitialDefaultValue(
643635
excludeString(defaultValueDeprecated) ??
644636
excludeString(field.default) ??
645637
initialValueForCheckboxAsBool ??
646-
getDefaultValueForType(field.inputType)
638+
getDefaultValueForType(field.type)
647639
);
648640
}
649641

@@ -681,10 +673,7 @@ function getInitialSubFieldValues(
681673
);
682674
});
683675

684-
if (
685-
field.inputType === supportedTypes.FIELDSET &&
686-
field.valueGroupingDisabled
687-
) {
676+
if (field.type === supportedTypes.FIELDSET && field.valueGroupingDisabled) {
688677
Object.assign(initialValue, subFieldValues);
689678
} else {
690679
initialValue[field.name!] = subFieldValues;
@@ -714,7 +703,7 @@ export function getInitialValues(
714703
fields
715704
.map((field) => applyFieldDynamicProperties(field, defaultFieldValues))
716705
.forEach((field) => {
717-
switch (field.inputType) {
706+
switch (field.type) {
718707
case supportedTypes.FIELDSET: {
719708
if (field.valueGroupingDisabled) {
720709
Object.assign(

0 commit comments

Comments
 (0)