Skip to content

Commit

Permalink
fix(Form): support conditional rendering of children (#4942)
Browse files Browse the repository at this point in the history
Fixes #4923
  • Loading branch information
Lukas742 authored Aug 4, 2023
1 parent 03835db commit 9d3bcdb
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 12 deletions.
89 changes: 89 additions & 0 deletions packages/main/src/components/Form/Form.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useReducer } from 'react';
import { createPortal } from 'react-dom';
import { InputType } from '../../enums/index.js';
import { Input, Label } from '../../webComponents/index.js';
Expand Down Expand Up @@ -30,6 +31,47 @@ const component = (
</Form>
);

const ConditionRenderingExample = () => {
const [show, toggle] = useReducer((prev) => !prev, false);
const [show2, toggle2] = useReducer((prev) => !prev, false);
const [show3, toggle3] = useReducer((prev) => !prev, false);
return (
<>
<button onClick={toggle}>Toggle Input</button>
<button onClick={toggle2}>Toggle Group</button>
<button onClick={toggle3}>Toggle Group2</button>
<Form>
<FormItem label="Item 1">
<Input data-testid="1" />
</FormItem>
{show3 && <FormGroup titleText="Empty Group" />}
{show && (
<FormItem label="Item 2">
<Input data-testid="2" />
</FormItem>
)}
{show2 && (
<FormGroup titleText="Group 1">
<FormItem label="Item1 Grouped">
<Input data-testid="g1" />
</FormItem>
<FormItem label="Item2 Grouped">
<Input data-testid="g2" />
</FormItem>
</FormGroup>
)}

<FormItem label="Item 3">
<Input data-testid="3" />
</FormItem>
<FormItem label="Item 4">
<Input data-testid="4" />
</FormItem>
</Form>
</>
);
};

describe('Form', () => {
it('size S - labels and fields should cover full width', () => {
cy.viewport(393, 852); // iPhone 14 Pro
Expand Down Expand Up @@ -99,6 +141,53 @@ describe('Form', () => {
cy.findByTestId('notSupported').should('not.exist');
});

it('conditionally render FormItems & FormGroups', () => {
cy.mount(<ConditionRenderingExample />);
cy.findByText('Item 2').should('not.exist');

cy.findByText('Toggle Input').click();
cy.findByText('Item 2').should('exist');
cy.findByTestId('2').should('be.visible').as('item2');
cy.get('@item2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '1');

cy.findByText('Toggle Group').click();
cy.findByText('Group 1')
.should('be.visible')
.and('have.css', 'grid-column-start', '1')
.and('have.css', 'grid-row-start', '2');
cy.findByTestId('g2').should('be.visible').as('g2');
cy.get('@g2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '4');
cy.findByTestId('2').should('be.visible').as('item2');
cy.get('@item2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '1');

cy.findByText('Toggle Group2').click();
cy.findByText('Empty Group')
.should('be.visible')
.and('have.css', 'grid-column-start', '13')
.and('have.css', 'grid-row-start', '1');
cy.findByText('Group 1')
.should('be.visible')
.and('have.css', 'grid-column-start', '13')
.and('have.css', 'grid-row-start', '3');
cy.findByTestId('g2').should('be.visible').as('g2');
cy.get('@g2').parent().should('have.css', 'grid-column-start', '17').and('have.css', 'grid-row-start', '5');
cy.findByTestId('2').should('be.visible').as('item2');
cy.get('@item2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '4');

cy.findByText('Toggle Input').click();
cy.findByText('Empty Group')
.should('be.visible')
.and('have.css', 'grid-column-start', '13')
.and('have.css', 'grid-row-start', '1');
cy.findByText('Group 1')
.should('be.visible')
.and('have.css', 'grid-column-start', '1')
.and('have.css', 'grid-row-start', '3');
cy.findByTestId('g2').should('be.visible').as('g2');
cy.get('@g2').parent().should('have.css', 'grid-column-start', '5').and('have.css', 'grid-row-start', '5');
cy.findByTestId('2').should('not.exist');
});

cypressPassThroughTestsFactory(Form, {
children: (
<FormItem label="Item">
Expand Down
2 changes: 1 addition & 1 deletion packages/main/src/components/Form/FormContext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createContext, useContext } from 'react';
import type { FormContextType, GroupContextType } from './types.js';

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

export function useFormContext() {
return useContext(FormContext);
Expand Down
41 changes: 36 additions & 5 deletions packages/main/src/components/Form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { Device, useSyncRef } from '@ui5/webcomponents-react-base';
import { clsx } from 'clsx';
import type { ElementType, ReactNode } from 'react';
import React, { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { forwardRef, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
import { createUseStyles } from 'react-jss';
import { FormBackgroundDesign, TitleLevel } from '../../enums/index.js';
import type { CommonProps } from '../../interfaces/index.js';
Expand All @@ -12,6 +12,10 @@ import { styles } from './Form.jss.js';
import { FormContext } from './FormContext.js';
import type { FormContextType, FormElementTypes, FormGroupLayoutInfo, FormItemLayoutInfo, ItemInfo } from './types.js';

const recalcReducerFn = (prev: number) => {
return prev + 1;
};

const useStyles = createUseStyles(styles, { name: 'Form' });

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

const registerItem = useCallback((id: string, type: FormElementTypes, groupId?: string) => {
setItems((state) => {
const clonedMap = new Map(state);
setItems((prev) => {
const clonedMap = new Map(prev);
if (groupId) {
const groupItem = clonedMap.get(groupId);
if (groupItem) {
Expand Down Expand Up @@ -201,7 +205,7 @@ const Form = forwardRef<HTMLFormElement, FormPropTypes>((props, ref) => {
});
}, []);

const formLayoutContextValue = useMemo((): Omit<FormContextType, 'labelSpan'> => {
const formLayoutContextValue = useMemo((): Omit<FormContextType, 'labelSpan' | 'recalcTrigger'> => {
const formItems: FormItemLayoutInfo[] = [];
const formGroups: FormGroupLayoutInfo[] = [];

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

const prevFormItems = useRef<undefined | FormItemLayoutInfo[]>(undefined);
const prevFormGroups = useRef<undefined | FormGroupLayoutInfo[]>(undefined);

const [recalcTrigger, fireRecalc] = useReducer(recalcReducerFn, 0, undefined);
useEffect(() => {
if (prevFormItems.current || prevFormGroups.current) {
let hasChanged =
formLayoutContextValue.formItems.length !== prevFormItems.current.length ||
formLayoutContextValue.formGroups.length !== prevFormGroups.current.length;
if (!hasChanged) {
hasChanged = !formLayoutContextValue.formGroups.every(
(item, index) => prevFormGroups.current.findIndex((element) => element.id === item.id) === index
);
}
if (!hasChanged) {
hasChanged = !formLayoutContextValue.formItems.every(
(item, index) => prevFormItems.current.findIndex((element) => element.id === item.id) === index
);
}
if (hasChanged) {
fireRecalc();
}
}
prevFormItems.current = formLayoutContextValue.formItems;
prevFormGroups.current = formLayoutContextValue.formGroups;
}, [formLayoutContextValue.formItems, formLayoutContextValue.formGroups]);

return (
<FormContext.Provider value={{ ...formLayoutContextValue, labelSpan: currentLabelSpan }}>
<FormContext.Provider value={{ ...formLayoutContextValue, labelSpan: currentLabelSpan, recalcTrigger }}>
<CustomTag
className={clsx(classes.formContainer, className)}
suppressHydrationWarning={true}
Expand Down
1 change: 1 addition & 0 deletions packages/main/src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type FormContextType = {
unregisterItem?: (id: string, groupId?: string) => void;
labelSpan: null | number;
rowsWithGroup?: Record<number, boolean>;
recalcTrigger: number;
};

export type GroupContextType = {
Expand Down
9 changes: 6 additions & 3 deletions packages/main/src/components/FormGroup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,18 @@ export interface FormGroupPropTypes {
*/
const FormGroup = (props: FormGroupPropTypes) => {
const { titleText, children } = props;
const { formGroups: layoutInfos, registerItem, unregisterItem, labelSpan } = useFormContext();
const { formGroups: layoutInfos, registerItem, unregisterItem, labelSpan, recalcTrigger } = useFormContext();
const uniqueId = useIsomorphicId();

useEffect(() => {
registerItem?.(uniqueId, 'formGroup');
return () => unregisterItem?.(uniqueId);
}, [uniqueId, registerItem, unregisterItem]);
}, [uniqueId, registerItem, unregisterItem, recalcTrigger]);

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

if (!layoutInfo) return null;
const { columnIndex, rowIndex } = layoutInfo;
Expand Down
13 changes: 10 additions & 3 deletions packages/main/src/components/FormItem/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,16 @@ const getContentForHtmlLabel = (label: ReactNode) => {
* __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`.
*/
const FormItem = (props: FormItemPropTypes) => {
const { label, children } = props as InternalProps;
const uniqueId = useIsomorphicId();
const { formItems: layoutInfos, registerItem, unregisterItem, labelSpan, rowsWithGroup } = useFormContext();
const { label, children } = props as InternalProps;
const {
formItems: layoutInfos,
registerItem,
unregisterItem,
labelSpan,
rowsWithGroup,
recalcTrigger
} = useFormContext();
const groupContext = useFormGroupContext();
const classes = useStyles();

Expand All @@ -135,7 +142,7 @@ const FormItem = (props: FormItemPropTypes) => {
return () => {
unregisterItem?.(uniqueId, groupContext.id);
};
}, [uniqueId, registerItem, unregisterItem, groupContext.id]);
}, [uniqueId, registerItem, unregisterItem, groupContext.id, recalcTrigger]);

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

Expand Down

0 comments on commit 9d3bcdb

Please sign in to comment.