devextreme-react
Version:
DevExtreme React UI and Visualization Components
107 lines (105 loc) • 4.72 kB
JavaScript
/*!
* devextreme-react
* Version: 25.2.3
* Build date: Fri Dec 12 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 GUARD_NODE_CLASS_NAME = '__dx_react_guard_node__';
const createHiddenNode = (containerNodeName, ref, defaultElement, className = '') => {
const style = { display: 'none' };
switch (containerNodeName) {
case 'TABLE':
return React.createElement("tbody", { style: style, ref: ref, className: className });
case 'TBODY':
return React.createElement("tr", { style: style, ref: ref, className: className });
default:
return React.createElement(defaultElement, { style, ref, className });
}
};
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 guardElement = useRef();
const removalListenerElement = useRef();
const onTemplateRemoved = useCallback((_, args) => {
// eslint-disable-next-line spellcheck/spell-checker
if (args?.isUnmounting || isRemovalLocked.current) {
return;
}
[
...elements.current,
removalListenerElement.current,
].forEach((el) => el && events.off(el, DX_REMOVE_EVENT, onTemplateRemoved));
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,
guardElement.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 guardNode = createHiddenNode(container?.nodeName, (node) => {
guardElement.current = node;
elements.current = [];
let currentNode = node?.previousSibling;
while (currentNode && (typeof currentNode.className !== 'string'
|| !currentNode.className.includes(GUARD_NODE_CLASS_NAME))) {
elements.current.push(currentNode);
currentNode = currentNode?.previousSibling;
}
}, 'div', GUARD_NODE_CLASS_NAME);
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 }),
guardNode,
removalListener)), container);
};
export const TemplateWrapper = memo(TemplateWrapperComponent);