diff --git a/packages/main/src/components/Form/Form.cy.tsx b/packages/main/src/components/Form/Form.cy.tsx
index 1e33cd2c845..1b478cb244f 100644
--- a/packages/main/src/components/Form/Form.cy.tsx
+++ b/packages/main/src/components/Form/Form.cy.tsx
@@ -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';
@@ -30,6 +31,47 @@ const component = (
);
+const ConditionRenderingExample = () => {
+ const [show, toggle] = useReducer((prev) => !prev, false);
+ const [show2, toggle2] = useReducer((prev) => !prev, false);
+ const [show3, toggle3] = useReducer((prev) => !prev, false);
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
describe('Form', () => {
it('size S - labels and fields should cover full width', () => {
cy.viewport(393, 852); // iPhone 14 Pro
@@ -99,6 +141,53 @@ describe('Form', () => {
cy.findByTestId('notSupported').should('not.exist');
});
+ it('conditionally render FormItems & FormGroups', () => {
+ cy.mount();
+ 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: (
diff --git a/packages/main/src/components/Form/FormContext.ts b/packages/main/src/components/Form/FormContext.ts
index 76cc5db2883..3c1a5e36bed 100644
--- a/packages/main/src/components/Form/FormContext.ts
+++ b/packages/main/src/components/Form/FormContext.ts
@@ -1,7 +1,7 @@
import { createContext, useContext } from 'react';
import type { FormContextType, GroupContextType } from './types.js';
-export const FormContext = createContext({ labelSpan: null });
+export const FormContext = createContext({ labelSpan: null, recalcTrigger: 0 });
export function useFormContext() {
return useContext(FormContext);
diff --git a/packages/main/src/components/Form/index.tsx b/packages/main/src/components/Form/index.tsx
index fb7634f9709..0ca4a0aa931 100644
--- a/packages/main/src/components/Form/index.tsx
+++ b/packages/main/src/components/Form/index.tsx
@@ -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';
@@ -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 {
@@ -165,8 +169,8 @@ const Form = forwardRef((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) {
@@ -201,7 +205,7 @@ const Form = forwardRef((props, ref) => {
});
}, []);
- const formLayoutContextValue = useMemo((): Omit => {
+ const formLayoutContextValue = useMemo((): Omit => {
const formItems: FormItemLayoutInfo[] = [];
const formGroups: FormGroupLayoutInfo[] = [];
@@ -261,8 +265,35 @@ const Form = forwardRef((props, ref) => {
const formClassNames = clsx(classes.form, classes[backgroundDesign.toLowerCase()]);
const CustomTag = as as ElementType;
+ const prevFormItems = useRef(undefined);
+ const prevFormGroups = useRef(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 (
-
+
void;
labelSpan: null | number;
rowsWithGroup?: Record;
+ recalcTrigger: number;
};
export type GroupContextType = {
diff --git a/packages/main/src/components/FormGroup/index.tsx b/packages/main/src/components/FormGroup/index.tsx
index 0ffaeebd375..cb8e0b207a3 100644
--- a/packages/main/src/components/FormGroup/index.tsx
+++ b/packages/main/src/components/FormGroup/index.tsx
@@ -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;
diff --git a/packages/main/src/components/FormItem/index.tsx b/packages/main/src/components/FormItem/index.tsx
index 6d156c45c02..573e0b61bfc 100644
--- a/packages/main/src/components/FormItem/index.tsx
+++ b/packages/main/src/components/FormItem/index.tsx
@@ -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();
@@ -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]);