Skip to content

Commit

Permalink
add rendering named templates inside leaf nested options
Browse files Browse the repository at this point in the history
  • Loading branch information
VasilyStrelyaev committed Feb 21, 2025
1 parent c52bf31 commit 2facfa6
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 22 deletions.
75 changes: 75 additions & 0 deletions packages/devextreme-react/src/core/__tests__/template.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,28 @@ describe('component/render in nested options', () => {

NestedComponent.componentType = 'option';

const LeafNestedComponent = function NestedComponent(props: any) {
return (
<ConfigurationComponent<{
item?: any;
itemRender?: any;
itemComponent?: any;
} & React.PropsWithChildren>
elementDescriptor={{
OptionName: 'leafOption',
TemplateProps: [{
tmplOption: 'item',
render: 'itemRender',
component: 'itemComponent',
}],
}}
{...props}
/>
);
} as React.ComponentType<any> & NestedComponentMeta;

LeafNestedComponent.componentType = 'option';

const CollectionNestedComponent = function CollectionNestedComponent(props: any) {
return (
<ConfigurationComponent<{
Expand Down Expand Up @@ -723,6 +745,59 @@ describe('component/render in nested options', () => {
expect(typeof integrationOptions.templates['option.item'].render).toBe('function');
});

it('pass integrationOptions from its nested Template component to widget', () => {
const ItemTemplate = () => <div>Template</div>;
render(
<ComponentWithTemplates>
<NestedComponent itemTemplate='myTemplate'>
<Template name='myTemplate' render={ItemTemplate} />
</NestedComponent>
<LeafNestedComponent itemTemplate='leafOptionTemplate'>
<Template name='leafOptionTemplate' render={ItemTemplate} />
</LeafNestedComponent>
</ComponentWithTemplates>,
);

let options = WidgetClass.mock.calls[0][1];
let { integrationOptions } = options;

expect(integrationOptions.templates['option.item']).toBeDefined();
expect(typeof integrationOptions.templates['option.item']).toBe('string');
expect(typeof integrationOptions.templates['option.item'].render).toBe('function');

expect(integrationOptions.templates['leafOption.item']).toBeDefined();
expect(typeof integrationOptions.templates['leafOption.item']).toBe('string');
expect(typeof integrationOptions.templates['leafOption.item'].render).toBe('function');

const MyCustomComponent = () => (
<>
<NestedComponent itemTemplate='myTemplate'>
<Template name='myTemplate' render={ItemTemplate} />
</NestedComponent>
<LeafNestedComponent itemTemplate='leafOptionTemplate'>
<Template name='leafOptionTemplate' render={ItemTemplate} />
</LeafNestedComponent>
</>
);

render(
<ComponentWithTemplates>
<MyCustomComponent />
</ComponentWithTemplates>,
);

options = WidgetClass.mock.calls[1][1];
({ integrationOptions } = options);

expect(integrationOptions.templates['option.item']).toBeDefined();
expect(typeof integrationOptions.templates['option.item']).toBe('string');
expect(typeof integrationOptions.templates['option.item'].render).toBe('function');

expect(integrationOptions.templates['leafOption.item']).toBeDefined();
expect(typeof integrationOptions.templates['leafOption.item']).toBe('string');
expect(typeof integrationOptions.templates['leafOption.item'].render).toBe('function');
});

it('pass integrationOptions options to widget with several templates', () => {
const UserTemplate = () => <div>Template</div>;
render(
Expand Down
6 changes: 3 additions & 3 deletions packages/devextreme-react/src/core/component-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
const updateTemplates = useRef<(callback: () => void) => void>();

const prevPropsRef = useRef<P & ComponentBaseProps>();
const childrenContainer = useRef<HTMLDivElement>(null);
const childrenContainerRef = useRef<HTMLDivElement>(null);

const { parentType } = useContext(NestedOptionContext);

Expand All @@ -111,7 +111,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
},
props,
},
childrenContainer,
{ testContainerRef: childrenContainerRef },
Symbol('initial update token'),
'component',
);
Expand Down Expand Up @@ -435,7 +435,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
return (
<RestoreTreeContext.Provider value={restoreTree}>
<TemplateRenderingContext.Provider value={renderContextValue}>
<div ref={childrenContainer} {...getElementProps()}>
<div ref={childrenContainerRef} {...getElementProps()}>
<NestedOptionContext.Provider value={context}>
{renderContent()}
</NestedOptionContext.Provider>
Expand Down
27 changes: 14 additions & 13 deletions packages/devextreme-react/src/core/nested-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,40 +30,41 @@ const NestedOption = function NestedOption<P>(
): React.ReactElement | null {
const { children } = props;
const { elementDescriptor, ...restProps } = props;
const { isTemplateRendering } = useContext(TemplateRenderingContext);

if (!elementDescriptor || typeof document === 'undefined') {
if (!elementDescriptor || typeof document === 'undefined' || isTemplateRendering) {
return null;
}

const usesNamedTemplate = elementDescriptor.TemplateProps?.some(prop =>
props[prop.tmplOption] && typeof props[prop.tmplOption] === 'string'
);

const renderChildren = hasExpectedChildren(elementDescriptor) || usesNamedTemplate;

const {
parentExpectedChildren,
onChildOptionsReady: triggerParentOptionsReady,
getOptionComponentKey,
treeUpdateToken,
} = useContext(NestedOptionContext);

const { isTemplateRendering } = useContext(TemplateRenderingContext);
const [optionComponentKey] = useState(getOptionComponentKey());
const optionElement = getOptionInfo(elementDescriptor, restProps, parentExpectedChildren);
const mainContainer = useMemo(() => document.createElement('div'), []);

const templateInfo = renderChildren ? { testContainer: mainContainer } : { hasTemplate: !!children };

const [
config,
context,
] = useOptionScanning(optionElement, mainContainer, treeUpdateToken, 'option');
] = useOptionScanning(optionElement, templateInfo, treeUpdateToken, 'option');

useLayoutEffect(() => {
if (!isTemplateRendering) {
triggerParentOptionsReady(config, optionElement.descriptor, treeUpdateToken, optionComponentKey);
}
triggerParentOptionsReady(config, optionElement.descriptor, treeUpdateToken, optionComponentKey);
}, [treeUpdateToken]);

if (children && !isTemplateRendering && !hasExpectedChildren(elementDescriptor)) {
mainContainer.appendChild(document.createElement('div'));
return null;
}

return isTemplateRendering ? null : React.createElement(
return renderChildren ? React.createElement(
React.Fragment,
{},
createPortal(
Expand All @@ -76,7 +77,7 @@ const NestedOption = function NestedOption<P>(
),
mainContainer,
),
);
) : null;
};

export default NestedOption;
Expand Down
14 changes: 8 additions & 6 deletions packages/devextreme-react/src/core/use-option-scanning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ import { mergeNameParts } from './configuration/utils';
import { createConfigBuilder, IConfigNode } from './configuration/config-node';
import { NestedOptionContext, NestedOptionContextContent } from './contexts';

export type TemplateInfo = { testContainer: HTMLDivElement } | { testContainerRef: RefObject<HTMLDivElement> } | { hasTemplate: boolean };

export function useOptionScanning(
optionElement: IOptionElement,
templateContainer: HTMLDivElement | RefObject<HTMLDivElement>,
templateInfo: TemplateInfo,
parentUpdateToken: symbol,
parentType: 'option' | 'component',
): [
Expand Down Expand Up @@ -66,12 +68,12 @@ export function useOptionScanning(
};

useLayoutEffect(() => {
const container = templateContainer && 'current' in templateContainer
? templateContainer.current
: templateContainer;
const hasTemplate =
'testContainer' in templateInfo && !!templateInfo.testContainer.childNodes.length ||
'testContainerRef' in templateInfo && !!templateInfo.testContainerRef.current?.childNodes.length ||
'hasTemplate' in templateInfo && templateInfo.hasTemplate;

const hasTemplateRendered = !!container && container.childNodes.length > 0;
configBuilder.updateAnonymousTemplates(hasTemplateRendered);
configBuilder.updateAnonymousTemplates(hasTemplate);
}, [parentUpdateToken]);

return [configBuilder.node, context];
Expand Down

0 comments on commit 2facfa6

Please sign in to comment.