Skip to content

Commit 46d6fad

Browse files
fix: render named templates inside custom configuration components (DevExpress#29067)
1 parent 6d872c3 commit 46d6fad

File tree

6 files changed

+207
-54
lines changed

6 files changed

+207
-54
lines changed

packages/devextreme-react/src/core/__tests__/template.test.tsx

Lines changed: 150 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -453,11 +453,45 @@ describe('nested template', () => {
453453
</ComponentWithTemplates>,
454454
);
455455

456-
const options = WidgetClass.mock.calls[0][1];
456+
let options = WidgetClass.mock.calls[0][1];
457457

458458
expect(options.item).toBeUndefined();
459459

460-
const { integrationOptions } = options;
460+
let { integrationOptions } = options;
461+
462+
expect(integrationOptions).toBeDefined();
463+
expect(integrationOptions.templates).toBeDefined();
464+
465+
expect(integrationOptions.templates.item1).toBeDefined();
466+
expect(typeof integrationOptions.templates.item1.render).toBe('function');
467+
468+
expect(integrationOptions.templates.item2).toBeDefined();
469+
expect(typeof integrationOptions.templates.item2.render).toBe('function');
470+
471+
expect(integrationOptions.templates.item3).toBeDefined();
472+
expect(typeof integrationOptions.templates.item3.render).toBe('function');
473+
474+
const MySetOfTemplates = () => (
475+
<>
476+
<Template name="item1" render={ItemTemplate} />
477+
<Template name="item2" component={ItemTemplate} />
478+
<Template name="item3">
479+
<ItemTemplate />
480+
</Template>
481+
</>
482+
);
483+
484+
render(
485+
<ComponentWithTemplates>
486+
<MySetOfTemplates />
487+
</ComponentWithTemplates>,
488+
);
489+
490+
options = WidgetClass.mock.calls[1][1];
491+
492+
expect(options.item).toBeUndefined();
493+
494+
({ integrationOptions } = options);
461495

462496
expect(integrationOptions).toBeDefined();
463497
expect(integrationOptions.templates).toBeDefined();
@@ -475,7 +509,7 @@ describe('nested template', () => {
475509
it('renders nested templates', () => {
476510
const ref = React.createRef<HTMLDivElement>();
477511
const FirstTemplate = () => <div className="template">Template</div>;
478-
const { container } = render(
512+
let { container } = render(
479513
<ComponentWithTemplates>
480514
<Template name="item1" render={FirstTemplate} />
481515
<div ref={ref} />
@@ -484,11 +518,27 @@ describe('nested template', () => {
484518
act(() => { renderTemplate('item1', undefined, ref.current); });
485519

486520
expect(container.querySelector('.template')?.outerHTML).toBe('<div class="template">Template</div>');
521+
522+
const MyCustomComponent = () => (
523+
<>
524+
<Template name="item1" render={FirstTemplate} />
525+
<div ref={ref} />
526+
</>
527+
);
528+
529+
({ container } = render(
530+
<ComponentWithTemplates>
531+
<MyCustomComponent />
532+
</ComponentWithTemplates>,
533+
));
534+
act(() => { renderTemplate('item1', undefined, ref.current); });
535+
536+
expect(container.querySelector('.template')?.outerHTML).toBe('<div class="template">Template</div>');
487537
});
488538

489539
it('renders children of nested template', () => {
490540
const ref = React.createRef<HTMLDivElement>();
491-
const { container } = render(
541+
let { container } = render(
492542
<ComponentWithTemplates>
493543
<Template name="item1">
494544
<div className="template">Template</div>
@@ -498,6 +548,25 @@ describe('nested template', () => {
498548
);
499549
act(() => { renderTemplate('item1', undefined, ref.current); });
500550

551+
expect(container.querySelector('.template')?.outerHTML)
552+
.toBe('<div class="template">Template</div>');
553+
554+
const MyCustomComponent = () => (
555+
<>
556+
<Template name="item1">
557+
<div className="template">Template</div>
558+
</Template>
559+
<div ref={ref} />
560+
</>
561+
);
562+
563+
({ container } = render(
564+
<ComponentWithTemplates>
565+
<MyCustomComponent />
566+
</ComponentWithTemplates>,
567+
));
568+
act(() => { renderTemplate('item1', undefined, ref.current); });
569+
501570
expect(container.querySelector('.template')?.outerHTML)
502571
.toBe('<div class="template">Template</div>');
503572
});
@@ -587,6 +656,28 @@ describe('component/render in nested options', () => {
587656

588657
NestedComponent.componentType = 'option';
589658

659+
const LeafNestedComponent = function NestedComponent(props: any) {
660+
return (
661+
<ConfigurationComponent<{
662+
item?: any;
663+
itemRender?: any;
664+
itemComponent?: any;
665+
} & React.PropsWithChildren>
666+
elementDescriptor={{
667+
OptionName: 'leafOption',
668+
TemplateProps: [{
669+
tmplOption: 'item',
670+
render: 'itemRender',
671+
component: 'itemComponent',
672+
}],
673+
}}
674+
{...props}
675+
/>
676+
);
677+
} as React.ComponentType<any> & NestedComponentMeta;
678+
679+
LeafNestedComponent.componentType = 'option';
680+
590681
const CollectionNestedComponent = function CollectionNestedComponent(props: any) {
591682
return (
592683
<ConfigurationComponent<{
@@ -654,6 +745,61 @@ describe('component/render in nested options', () => {
654745
expect(typeof integrationOptions.templates['option.item'].render).toBe('function');
655746
});
656747

748+
it('pass integrationOptions from its nested Template component to widget', () => {
749+
const ItemTemplate = () => <div>Template</div>;
750+
render(
751+
<ComponentWithTemplates>
752+
<NestedComponent item='myTemplate'>
753+
<Template name='myTemplate' render={ItemTemplate} />
754+
</NestedComponent>
755+
<LeafNestedComponent item='leafOptionTemplate'>
756+
<Template name='leafOptionTemplate' render={ItemTemplate} />
757+
</LeafNestedComponent>
758+
</ComponentWithTemplates>,
759+
);
760+
761+
let options = WidgetClass.mock.calls[0][1];
762+
let { integrationOptions } = options;
763+
764+
expect(integrationOptions.templates.myTemplate).toBeDefined();
765+
expect(typeof integrationOptions.templates.myTemplate.render).toBe('function');
766+
767+
expect(integrationOptions.templates.leafOptionTemplate).toBeDefined();
768+
expect(typeof integrationOptions.templates.leafOptionTemplate.render).toBe('function');
769+
770+
expect(options.option.item).toBe('myTemplate');
771+
expect(options.leafOption.item).toBe('leafOptionTemplate');
772+
773+
const MyCustomComponent = () => (
774+
<>
775+
<NestedComponent item='myTemplate'>
776+
<Template name='myTemplate' render={ItemTemplate} />
777+
</NestedComponent>
778+
<LeafNestedComponent item='leafOptionTemplate'>
779+
<Template name='leafOptionTemplate' render={ItemTemplate} />
780+
</LeafNestedComponent>
781+
</>
782+
);
783+
784+
render(
785+
<ComponentWithTemplates>
786+
<MyCustomComponent />
787+
</ComponentWithTemplates>,
788+
);
789+
790+
options = WidgetClass.mock.calls[1][1];
791+
({ integrationOptions } = options);
792+
793+
expect(integrationOptions.templates.myTemplate).toBeDefined();
794+
expect(typeof integrationOptions.templates.myTemplate.render).toBe('function');
795+
796+
expect(integrationOptions.templates.leafOptionTemplate).toBeDefined();
797+
expect(typeof integrationOptions.templates.leafOptionTemplate.render).toBe('function');
798+
799+
expect(options.option.item).toBe('myTemplate');
800+
expect(options.leafOption.item).toBe('leafOptionTemplate');
801+
});
802+
657803
it('pass integrationOptions options to widget with several templates', () => {
658804
const UserTemplate = () => <div>Template</div>;
659805
render(

packages/devextreme-react/src/core/component-base.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
9494
const updateTemplates = useRef<(callback: () => void) => void>();
9595

9696
const prevPropsRef = useRef<P & ComponentBaseProps>();
97-
const childrenContainer = useRef<HTMLDivElement>(null);
97+
const childrenContainerRef = useRef<HTMLDivElement>(null);
9898

9999
const { parentType } = useContext(NestedOptionContext);
100100

@@ -111,8 +111,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
111111
},
112112
props,
113113
},
114-
props.children,
115-
childrenContainer,
114+
() => !!childrenContainerRef.current?.childNodes.length,
116115
Symbol('initial update token'),
117116
'component',
118117
);
@@ -436,7 +435,7 @@ const ComponentBase = forwardRef<ComponentBaseRef, any>(
436435
return (
437436
<RestoreTreeContext.Provider value={restoreTree}>
438437
<TemplateRenderingContext.Provider value={renderContextValue}>
439-
<div ref={childrenContainer} {...getElementProps()}>
438+
<div ref={childrenContainerRef} {...getElementProps()}>
440439
<NestedOptionContext.Provider value={context}>
441440
{renderContent()}
442441
</NestedOptionContext.Provider>

packages/devextreme-react/src/core/contexts.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
} from 'react';
55

66
import { IExpectedChild, IOptionDescriptor } from './configuration/react/element';
7-
import { IConfigNode } from './configuration/config-node';
7+
import { IConfigNode, ITemplate } from './configuration/config-node';
88

99
export interface UpdateLocker {
1010
lock: () => void;
@@ -26,6 +26,10 @@ export interface NestedOptionContextContent {
2626
childUpdateToken: symbol,
2727
optionComponentKey: number
2828
) => void;
29+
onNamedTemplateReady: (
30+
template: ITemplate | null,
31+
childUpdateToken: symbol,
32+
) => void;
2933
getOptionComponentKey: () => number;
3034
treeUpdateToken: symbol;
3135
}
@@ -34,6 +38,7 @@ export const NestedOptionContext = createContext<NestedOptionContextContent>({
3438
parentExpectedChildren: {},
3539
parentFullName: '',
3640
onChildOptionsReady: () => undefined,
41+
onNamedTemplateReady: () => undefined,
3742
getOptionComponentKey: () => 0,
3843
treeUpdateToken: Symbol('initial tree update token'),
3944
parentType: 'component',

packages/devextreme-react/src/core/nested-option.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,40 +30,42 @@ const NestedOption = function NestedOption<P>(
3030
): React.ReactElement | null {
3131
const { children } = props;
3232
const { elementDescriptor, ...restProps } = props;
33+
const { isTemplateRendering } = useContext(TemplateRenderingContext);
3334

34-
if (!elementDescriptor || typeof document === 'undefined') {
35+
if (!elementDescriptor || typeof document === 'undefined' || isTemplateRendering) {
3536
return null;
3637
}
3738

39+
const usesNamedTemplate = elementDescriptor.TemplateProps?.some(
40+
(prop) => props[prop.tmplOption] && typeof props[prop.tmplOption] === 'string',
41+
);
42+
3843
const {
3944
parentExpectedChildren,
4045
onChildOptionsReady: triggerParentOptionsReady,
4146
getOptionComponentKey,
4247
treeUpdateToken,
4348
} = useContext(NestedOptionContext);
4449

45-
const { isTemplateRendering } = useContext(TemplateRenderingContext);
4650
const [optionComponentKey] = useState(getOptionComponentKey());
4751
const optionElement = getOptionInfo(elementDescriptor, restProps, parentExpectedChildren);
4852
const mainContainer = useMemo(() => document.createElement('div'), []);
53+
const renderChildren = hasExpectedChildren(elementDescriptor) || usesNamedTemplate;
54+
55+
const getHasTemplate = renderChildren
56+
? () => !!mainContainer.childNodes.length
57+
: () => !!children;
4958

5059
const [
5160
config,
5261
context,
53-
] = useOptionScanning(optionElement, children, mainContainer, treeUpdateToken, 'option');
62+
] = useOptionScanning(optionElement, getHasTemplate, treeUpdateToken, 'option');
5463

5564
useLayoutEffect(() => {
56-
if (!isTemplateRendering) {
57-
triggerParentOptionsReady(config, optionElement.descriptor, treeUpdateToken, optionComponentKey);
58-
}
65+
triggerParentOptionsReady(config, optionElement.descriptor, treeUpdateToken, optionComponentKey);
5966
}, [treeUpdateToken]);
6067

61-
if (children && !isTemplateRendering && !hasExpectedChildren(elementDescriptor)) {
62-
mainContainer.appendChild(document.createElement('div'));
63-
return null;
64-
}
65-
66-
return isTemplateRendering ? null : React.createElement(
68+
return renderChildren ? React.createElement(
6769
React.Fragment,
6870
{},
6971
createPortal(
@@ -76,7 +78,7 @@ const NestedOption = function NestedOption<P>(
7678
),
7779
mainContainer,
7880
),
79-
);
81+
) : null;
8082
};
8183

8284
export default NestedOption;

packages/devextreme-react/src/core/template.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as React from 'react';
22

3-
import { memo } from 'react';
3+
import { memo, useContext, useLayoutEffect } from 'react';
4+
import { NestedOptionContext, TemplateRenderingContext } from './contexts';
5+
import { getNamedTemplate } from './configuration/react/templates';
46

57
interface ITemplateMeta {
68
tmplOption: string;
@@ -20,7 +22,24 @@ interface ITemplateArgs {
2022
index?: number;
2123
}
2224

23-
const Template: React.FC<ITemplateProps> = memo(() => null);
25+
const Template: React.FC<ITemplateProps> = memo((props) => {
26+
const {
27+
onNamedTemplateReady,
28+
treeUpdateToken,
29+
} = useContext(NestedOptionContext);
30+
31+
const { isTemplateRendering } = useContext(TemplateRenderingContext);
32+
33+
const template = getNamedTemplate(props);
34+
35+
useLayoutEffect(() => {
36+
if (!isTemplateRendering) {
37+
onNamedTemplateReady(template, treeUpdateToken);
38+
}
39+
}, [treeUpdateToken]);
40+
41+
return null;
42+
});
2443

2544
function findProps(child: React.ReactElement): ITemplateProps | undefined {
2645
if (child.type !== Template) {

0 commit comments

Comments
 (0)