Skip to content

Commit 9d3bcdb

Browse files
authored
fix(Form): support conditional rendering of children (#4942)
Fixes #4923
1 parent 03835db commit 9d3bcdb

File tree

6 files changed

+143
-12
lines changed

6 files changed

+143
-12
lines changed

packages/main/src/components/Form/Form.cy.tsx

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useReducer } from 'react';
12
import { createPortal } from 'react-dom';
23
import { InputType } from '../../enums/index.js';
34
import { Input, Label } from '../../webComponents/index.js';
@@ -30,6 +31,47 @@ const component = (
3031
</Form>
3132
);
3233

34+
const ConditionRenderingExample = () => {
35+
const [show, toggle] = useReducer((prev) => !prev, false);
36+
const [show2, toggle2] = useReducer((prev) => !prev, false);
37+
const [show3, toggle3] = useReducer((prev) => !prev, false);
38+
return (
39+
<>
40+
<button onClick={toggle}>Toggle Input</button>
41+
<button onClick={toggle2}>Toggle Group</button>
42+
<button onClick={toggle3}>Toggle Group2</button>
43+
<Form>
44+
<FormItem label="Item 1">
45+
<Input data-testid="1" />
46+
</FormItem>
47+
{show3 && <FormGroup titleText="Empty Group" />}
48+
{show && (
49+
<FormItem label="Item 2">
50+
<Input data-testid="2" />
51+
</FormItem>
52+
)}
53+
{show2 && (
54+
<FormGroup titleText="Group 1">
55+
<FormItem label="Item1 Grouped">
56+
<Input data-testid="g1" />
57+
</FormItem>
58+
<FormItem label="Item2 Grouped">
59+
<Input data-testid="g2" />
60+
</FormItem>
61+
</FormGroup>
62+
)}
63+
64+
<FormItem label="Item 3">
65+
<Input data-testid="3" />
66+
</FormItem>
67+
<FormItem label="Item 4">
68+
<Input data-testid="4" />
69+
</FormItem>
70+
</Form>
71+
</>
72+
);
73+
};
74+
3375
describe('Form', () => {
3476
it('size S - labels and fields should cover full width', () => {
3577
cy.viewport(393, 852); // iPhone 14 Pro
@@ -99,6 +141,53 @@ describe('Form', () => {
99141
cy.findByTestId('notSupported').should('not.exist');
100142
});
101143

144+
it('conditionally render FormItems & FormGroups', () => {
145+
cy.mount(<ConditionRenderingExample />);
146+
cy.findByText('Item 2').should('not.exist');
147+
148+
cy.findByText('Toggle Input').click();
149+
cy.findByText('Item 2').should('exist');
150+
cy.findByTestId('2').should('be.visible').as('item2');
151+
cy.get('@item2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '1');
152+
153+
cy.findByText('Toggle Group').click();
154+
cy.findByText('Group 1')
155+
.should('be.visible')
156+
.and('have.css', 'grid-column-start', '1')
157+
.and('have.css', 'grid-row-start', '2');
158+
cy.findByTestId('g2').should('be.visible').as('g2');
159+
cy.get('@g2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '4');
160+
cy.findByTestId('2').should('be.visible').as('item2');
161+
cy.get('@item2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '1');
162+
163+
cy.findByText('Toggle Group2').click();
164+
cy.findByText('Empty Group')
165+
.should('be.visible')
166+
.and('have.css', 'grid-column-start', '13')
167+
.and('have.css', 'grid-row-start', '1');
168+
cy.findByText('Group 1')
169+
.should('be.visible')
170+
.and('have.css', 'grid-column-start', '13')
171+
.and('have.css', 'grid-row-start', '3');
172+
cy.findByTestId('g2').should('be.visible').as('g2');
173+
cy.get('@g2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '5');
174+
cy.findByTestId('2').should('be.visible').as('item2');
175+
cy.get('@item2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '4');
176+
177+
cy.findByText('Toggle Input').click();
178+
cy.findByText('Empty Group')
179+
.should('be.visible')
180+
.and('have.css', 'grid-column-start', '13')
181+
.and('have.css', 'grid-row-start', '1');
182+
cy.findByText('Group 1')
183+
.should('be.visible')
184+
.and('have.css', 'grid-column-start', '1')
185+
.and('have.css', 'grid-row-start', '3');
186+
cy.findByTestId('g2').should('be.visible').as('g2');
187+
cy.get('@g2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '5');
188+
cy.findByTestId('2').should('not.exist');
189+
});
190+
102191
cypressPassThroughTestsFactory(Form, {
103192
children: (
104193
<FormItem label="Item">

packages/main/src/components/Form/FormContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createContext, useContext } from 'react';
22
import type { FormContextType, GroupContextType } from './types.js';
33

4-
export const FormContext = createContext<FormContextType>({ labelSpan: null });
4+
export const FormContext = createContext<FormContextType>({ labelSpan: null, recalcTrigger: 0 });
55

66
export function useFormContext() {
77
return useContext(FormContext);

packages/main/src/components/Form/index.tsx

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Device, useSyncRef } from '@ui5/webcomponents-react-base';
44
import { clsx } from 'clsx';
55
import type { ElementType, ReactNode } from 'react';
6-
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6+
import React, { forwardRef, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
77
import { createUseStyles } from 'react-jss';
88
import { FormBackgroundDesign, TitleLevel } from '../../enums/index.js';
99
import type { CommonProps } from '../../interfaces/index.js';
@@ -12,6 +12,10 @@ import { styles } from './Form.jss.js';
1212
import { FormContext } from './FormContext.js';
1313
import type { FormContextType, FormElementTypes, FormGroupLayoutInfo, FormItemLayoutInfo, ItemInfo } from './types.js';
1414

15+
const recalcReducerFn = (prev: number) => {
16+
return prev + 1;
17+
};
18+
1519
const useStyles = createUseStyles(styles, { name: 'Form' });
1620

1721
export interface FormPropTypes extends CommonProps {
@@ -165,8 +169,8 @@ const Form = forwardRef<HTMLFormElement, FormPropTypes>((props, ref) => {
165169
const currentNumberOfColumns = columnsMap.get(currentRange);
166170

167171
const registerItem = useCallback((id: string, type: FormElementTypes, groupId?: string) => {
168-
setItems((state) => {
169-
const clonedMap = new Map(state);
172+
setItems((prev) => {
173+
const clonedMap = new Map(prev);
170174
if (groupId) {
171175
const groupItem = clonedMap.get(groupId);
172176
if (groupItem) {
@@ -201,7 +205,7 @@ const Form = forwardRef<HTMLFormElement, FormPropTypes>((props, ref) => {
201205
});
202206
}, []);
203207

204-
const formLayoutContextValue = useMemo((): Omit<FormContextType, 'labelSpan'> => {
208+
const formLayoutContextValue = useMemo((): Omit<FormContextType, 'labelSpan' | 'recalcTrigger'> => {
205209
const formItems: FormItemLayoutInfo[] = [];
206210
const formGroups: FormGroupLayoutInfo[] = [];
207211

@@ -261,8 +265,35 @@ const Form = forwardRef<HTMLFormElement, FormPropTypes>((props, ref) => {
261265
const formClassNames = clsx(classes.form, classes[backgroundDesign.toLowerCase()]);
262266
const CustomTag = as as ElementType;
263267

268+
const prevFormItems = useRef<undefined | FormItemLayoutInfo[]>(undefined);
269+
const prevFormGroups = useRef<undefined | FormGroupLayoutInfo[]>(undefined);
270+
271+
const [recalcTrigger, fireRecalc] = useReducer(recalcReducerFn, 0, undefined);
272+
useEffect(() => {
273+
if (prevFormItems.current || prevFormGroups.current) {
274+
let hasChanged =
275+
formLayoutContextValue.formItems.length !== prevFormItems.current.length ||
276+
formLayoutContextValue.formGroups.length !== prevFormGroups.current.length;
277+
if (!hasChanged) {
278+
hasChanged = !formLayoutContextValue.formGroups.every(
279+
(item, index) => prevFormGroups.current.findIndex((element) => element.id === item.id) === index
280+
);
281+
}
282+
if (!hasChanged) {
283+
hasChanged = !formLayoutContextValue.formItems.every(
284+
(item, index) => prevFormItems.current.findIndex((element) => element.id === item.id) === index
285+
);
286+
}
287+
if (hasChanged) {
288+
fireRecalc();
289+
}
290+
}
291+
prevFormItems.current = formLayoutContextValue.formItems;
292+
prevFormGroups.current = formLayoutContextValue.formGroups;
293+
}, [formLayoutContextValue.formItems, formLayoutContextValue.formGroups]);
294+
264295
return (
265-
<FormContext.Provider value={{ ...formLayoutContextValue, labelSpan: currentLabelSpan }}>
296+
<FormContext.Provider value={{ ...formLayoutContextValue, labelSpan: currentLabelSpan, recalcTrigger }}>
266297
<CustomTag
267298
className={clsx(classes.formContainer, className)}
268299
suppressHydrationWarning={true}

packages/main/src/components/Form/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type FormContextType = {
1616
unregisterItem?: (id: string, groupId?: string) => void;
1717
labelSpan: null | number;
1818
rowsWithGroup?: Record<number, boolean>;
19+
recalcTrigger: number;
1920
};
2021

2122
export type GroupContextType = {

packages/main/src/components/FormGroup/index.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@ export interface FormGroupPropTypes {
2626
*/
2727
const FormGroup = (props: FormGroupPropTypes) => {
2828
const { titleText, children } = props;
29-
const { formGroups: layoutInfos, registerItem, unregisterItem, labelSpan } = useFormContext();
29+
const { formGroups: layoutInfos, registerItem, unregisterItem, labelSpan, recalcTrigger } = useFormContext();
3030
const uniqueId = useIsomorphicId();
3131

3232
useEffect(() => {
3333
registerItem?.(uniqueId, 'formGroup');
3434
return () => unregisterItem?.(uniqueId);
35-
}, [uniqueId, registerItem, unregisterItem]);
35+
}, [uniqueId, registerItem, unregisterItem, recalcTrigger]);
3636

37-
const layoutInfo = useMemo(() => layoutInfos?.find(({ id: groupId }) => uniqueId === groupId), [layoutInfos]);
37+
const layoutInfo = useMemo(
38+
() => layoutInfos?.find(({ id: groupId }) => uniqueId === groupId),
39+
[layoutInfos, uniqueId]
40+
);
3841

3942
if (!layoutInfo) return null;
4043
const { columnIndex, rowIndex } = layoutInfo;

packages/main/src/components/FormItem/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,9 +124,16 @@ const getContentForHtmlLabel = (label: ReactNode) => {
124124
* __Note__: The `FormItem` is only used for calculating the final layout of the `Form`, thus it doesn't accept any other props than `label` and `children`, especially no `className`, `style` or `ref`.
125125
*/
126126
const FormItem = (props: FormItemPropTypes) => {
127-
const { label, children } = props as InternalProps;
128127
const uniqueId = useIsomorphicId();
129-
const { formItems: layoutInfos, registerItem, unregisterItem, labelSpan, rowsWithGroup } = useFormContext();
128+
const { label, children } = props as InternalProps;
129+
const {
130+
formItems: layoutInfos,
131+
registerItem,
132+
unregisterItem,
133+
labelSpan,
134+
rowsWithGroup,
135+
recalcTrigger
136+
} = useFormContext();
130137
const groupContext = useFormGroupContext();
131138
const classes = useStyles();
132139

@@ -135,7 +142,7 @@ const FormItem = (props: FormItemPropTypes) => {
135142
return () => {
136143
unregisterItem?.(uniqueId, groupContext.id);
137144
};
138-
}, [uniqueId, registerItem, unregisterItem, groupContext.id]);
145+
}, [uniqueId, registerItem, unregisterItem, groupContext.id, recalcTrigger]);
139146

140147
const layoutInfo = useMemo(() => layoutInfos?.find(({ id: itemId }) => uniqueId === itemId), [layoutInfos, uniqueId]);
141148

0 commit comments

Comments
 (0)