devextreme-react
Version:
DevExtreme React UI and Visualization Components
110 lines (108 loc) • 4.59 kB
JavaScript
/*!
* devextreme-react
* Version: 25.1.5
* Build date: Wed Sep 03 2025
*
* Copyright (c) 2012 - 2025 Developer Express Inc. ALL RIGHTS RESERVED
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file in the root of the project for details.
*
* https://github.com/DevExpress/devextreme-react
*/
import * as React from 'react';
import * as events from 'devextreme/events';
import { useCallback, useLayoutEffect, useEffect, useState, useRef, useMemo, memo, } from 'react';
import { createPortal } from 'react-dom';
import { DX_REMOVE_EVENT } from './component-base';
import { RemovalLockerContext } from './contexts';
const createHiddenNode = (containerNodeName, ref, defaultElement) => {
const style = { display: 'none' };
switch (containerNodeName) {
case 'TABLE':
return React.createElement("tbody", { style: style, ref: ref });
case 'TBODY':
return React.createElement("tr", { style: style, ref: ref });
default:
return React.createElement(defaultElement, { style, ref });
}
};
const TemplateWrapperComponent = ({ templateFactory, data, index, container, onRemoved, onRendered, componentKey, }) => {
const [removalListenerRequired, setRemovalListenerRequired] = useState(false);
const isRemovalLocked = useRef(false);
const removalLocker = useMemo(() => ({
lock() { isRemovalLocked.current = true; },
unlock() { isRemovalLocked.current = false; },
}), []);
const elements = useRef([]);
const hiddenNodeElement = useRef();
const removalListenerElement = useRef();
const onTemplateRemoved = useCallback((_, args) => {
if (args?.isUnmounting || isRemovalLocked.current) {
return;
}
[
...elements.current,
removalListenerElement.current,
].forEach((el) => el && events.off(el, DX_REMOVE_EVENT, onTemplateRemoved));
// In case of multiple root elements, letting the widget remove them all sync
Promise.resolve().then(() => {
onRemoved(componentKey);
});
}, [onRemoved]);
useLayoutEffect(() => {
const elementNodes = elements.current.filter((el) => el.nodeType === Node.ELEMENT_NODE);
if (elementNodes.length) {
elementNodes.forEach((el) => {
events.off(el, DX_REMOVE_EVENT, onTemplateRemoved);
events.on(el, DX_REMOVE_EVENT, onTemplateRemoved);
});
}
else if (!removalListenerRequired) {
setRemovalListenerRequired(true);
}
else if (removalListenerElement.current) {
events.off(removalListenerElement.current, DX_REMOVE_EVENT, onTemplateRemoved);
events.on(removalListenerElement.current, DX_REMOVE_EVENT, onTemplateRemoved);
}
return () => {
const safeAppend = (child) => {
if (child && container && !container.contains(child)) {
container.appendChild(child);
}
};
[
...elements.current,
hiddenNodeElement.current,
removalListenerElement.current,
].forEach((el) => safeAppend(el));
if (elementNodes.length) {
elementNodes.forEach((el) => events.off(el, DX_REMOVE_EVENT, onTemplateRemoved));
}
};
}, [onTemplateRemoved, removalListenerRequired, container]);
useEffect(() => {
onRendered();
}, [onRendered]);
const containerContent = Array.from(container.childNodes);
const hiddenNode = createHiddenNode(container?.nodeName, (node) => {
hiddenNodeElement.current = node;
elements.current = [];
let currentNode = node?.previousSibling;
while (currentNode) {
if (!containerContent.includes(currentNode)) {
elements.current.push(currentNode);
}
currentNode = currentNode?.previousSibling;
}
}, 'div');
const removalListener = removalListenerRequired
? createHiddenNode(container?.nodeName, (node) => { removalListenerElement.current = node; }, 'span')
: undefined;
return createPortal(React.createElement(React.Fragment, null,
React.createElement(RemovalLockerContext.Provider, { value: removalLocker },
templateFactory({ data, index, onRendered }),
hiddenNode,
removalListener)), container);
};
export const TemplateWrapper = memo(TemplateWrapperComponent);