devextreme-react
Version:
DevExtreme React UI and Visualization Components
323 lines (321 loc) • 13.1 kB
JavaScript
/*!
* devextreme-react
* Version: 24.2.6
* Build date: Mon Mar 17 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 { useContext, useMemo, useImperativeHandle, forwardRef, useRef, useLayoutEffect, useCallback, useState, } from 'react';
import { requestAnimationFrame } from 'devextreme/animation/frame';
import { deferUpdate } from 'devextreme/core/utils/common';
import config from 'devextreme/core/config';
import { createPortal } from 'react-dom';
import { useOptionScanning } from './use-option-scanning';
import { OptionsManager, scheduleGuards, unscheduleGuards } from './options-manager';
import { elementPropNames, getClassName } from './widget-config';
import { TemplateManager } from './template-manager';
import { ElementType } from './configuration/react/element';
import { NestedOptionContext, RemovalLockerContext, RestoreTreeContext, TemplateRenderingContext, } from './contexts';
const DX_REMOVE_EVENT = 'dxremove';
config({
buyNowLink: 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeReact.aspx',
licensingDocLink: 'https://go.devexpress.com/Licensing_Documentation_DevExtremeReact.aspx',
});
const ComponentBase = forwardRef((props, ref) => {
const { templateProps = [], defaults = {}, expectedChildren = {}, isPortalComponent = false, useRequestAnimationFrameFlag = false, subscribableOptions = [], WidgetClass, independentEvents = [], renderChildren, beforeCreateWidget = () => undefined, afterCreateWidget = () => undefined, } = props;
const [, setForceUpdateToken] = useState(Symbol('initial force update token'));
const removalLocker = useContext(RemovalLockerContext);
const restoreParentLink = useContext(RestoreTreeContext);
const instance = useRef();
const element = useRef();
const portalContainer = useRef();
const useDeferUpdateForTemplates = useRef(false);
const guardsUpdateScheduled = useRef(false);
const childElementsDetached = useRef(false);
const shouldRestoreFocus = useRef(false);
const optionsManager = useRef(new OptionsManager());
const childNodes = useRef();
const createDXTemplates = useRef();
const clearInstantiationModels = useRef();
const updateTemplates = useRef();
const prevPropsRef = useRef();
const childrenContainerRef = useRef(null);
const { parentType } = useContext(NestedOptionContext);
const [widgetConfig, context] = useOptionScanning({
type: ElementType.Option,
descriptor: {
name: '',
isCollection: false,
templates: templateProps,
initialValuesProps: defaults,
predefinedValuesProps: {},
expectedChildren,
},
props,
}, () => !!childrenContainerRef.current?.childNodes.length, Symbol('initial update token'), 'component');
const restoreTree = useCallback(() => {
if (childElementsDetached.current && childNodes.current?.length && element.current) {
element.current.append(...childNodes.current);
childElementsDetached.current = false;
}
if (restoreParentLink && element.current && !element.current.isConnected) {
restoreParentLink();
}
}, [
childNodes.current,
element.current,
childElementsDetached.current,
restoreParentLink,
]);
const updateCssClasses = useCallback((prevProps, newProps) => {
const prevClassName = prevProps ? getClassName(prevProps) : undefined;
const newClassName = getClassName(newProps);
if (prevClassName === newClassName) {
return;
}
if (prevClassName) {
const classNames = prevClassName.split(' ').filter((c) => c);
if (classNames.length) {
element.current?.classList.remove(...classNames);
}
}
if (newClassName) {
const classNames = newClassName.split(' ').filter((c) => c);
if (classNames.length) {
element.current?.classList.add(...classNames);
}
}
}, [element.current]);
const setInlineStyles = useCallback((styles) => {
if (element.current) {
const el = element.current;
Object.entries(styles).forEach(([name, value]) => {
el.style[name] = value;
});
}
}, [element.current]);
const setTemplateManagerHooks = useCallback(({ createDXTemplates: createDXTemplatesFn, clearInstantiationModels: clearInstantiationModelsFn, updateTemplates: updateTemplatesFn, }) => {
createDXTemplates.current = createDXTemplatesFn;
clearInstantiationModels.current = clearInstantiationModelsFn;
updateTemplates.current = updateTemplatesFn;
}, [
createDXTemplates.current,
clearInstantiationModels.current,
updateTemplates.current,
]);
const getElementProps = useCallback(() => {
const elementProps = {
ref: (el) => {
if (el) {
element.current = el;
}
},
};
elementPropNames.forEach((name) => {
if (name in props) {
elementProps[name] = props[name];
}
});
return elementProps;
}, [element.current]);
const scheduleTemplatesUpdate = useCallback(() => {
if (guardsUpdateScheduled.current) {
return;
}
guardsUpdateScheduled.current = true;
const updateFunc = useDeferUpdateForTemplates.current ? deferUpdate : requestAnimationFrame;
updateFunc(() => {
guardsUpdateScheduled.current = false;
updateTemplates.current?.(() => scheduleGuards());
});
unscheduleGuards();
}, [
guardsUpdateScheduled.current,
useDeferUpdateForTemplates.current,
updateTemplates.current,
]);
const createWidget = useCallback((el) => {
beforeCreateWidget();
el = el || element.current;
let options = {
templatesRenderAsynchronously: true,
...optionsManager.current.getInitialOptions(widgetConfig),
};
const templateOptions = optionsManager.current.getTemplateOptions(widgetConfig);
const dxTemplates = createDXTemplates.current?.(templateOptions);
if (dxTemplates && Object.keys(dxTemplates).length) {
options = {
...options,
integrationOptions: {
templates: dxTemplates,
},
};
}
clearInstantiationModels.current?.();
instance.current = new WidgetClass(el, options);
if (!useRequestAnimationFrameFlag) {
useDeferUpdateForTemplates.current = instance.current.option('integrationOptions.useDeferUpdateForTemplates');
}
optionsManager.current.setInstance(instance.current, widgetConfig, subscribableOptions, independentEvents);
instance.current.on('optionChanged', optionsManager.current.onOptionChanged);
afterCreateWidget();
}, [
beforeCreateWidget,
afterCreateWidget,
element.current,
optionsManager.current,
createDXTemplates.current,
clearInstantiationModels.current,
WidgetClass,
useRequestAnimationFrameFlag,
useDeferUpdateForTemplates.current,
instance.current,
subscribableOptions,
independentEvents,
widgetConfig,
]);
const onTemplatesRendered = useCallback(() => {
if (shouldRestoreFocus.current && instance.current?.focus) {
instance.current.focus();
shouldRestoreFocus.current = false;
}
}, [shouldRestoreFocus.current, instance.current]);
const onComponentUpdated = useCallback(() => {
if (parentType === 'option') {
return;
}
if (!optionsManager.current?.isInstanceSet) {
return;
}
updateCssClasses(prevPropsRef.current, props);
const templateOptions = optionsManager.current.getTemplateOptions(widgetConfig);
const dxTemplates = createDXTemplates.current?.(templateOptions) || {};
optionsManager.current.update(widgetConfig, dxTemplates);
scheduleTemplatesUpdate();
prevPropsRef.current = props;
}, [
optionsManager.current,
prevPropsRef.current,
createDXTemplates.current,
scheduleTemplatesUpdate,
updateCssClasses,
props,
widgetConfig,
]);
const onComponentMounted = useCallback(() => {
if (parentType === 'option') {
return;
}
const { style } = props;
if (childElementsDetached.current) {
restoreTree();
}
else if (element.current?.childNodes.length) {
childNodes.current = Array.from(element.current?.childNodes);
}
updateCssClasses(undefined, props);
if (style) {
setInlineStyles(style);
}
prevPropsRef.current = props;
}, [
childNodes.current,
element.current,
childElementsDetached.current,
updateCssClasses,
setInlineStyles,
props,
]);
const onComponentUnmounted = useCallback(() => {
removalLocker?.lock();
if (instance.current) {
const dxRemoveArgs = { isUnmounting: true };
shouldRestoreFocus.current = !!element.current?.contains(document.activeElement);
childNodes.current?.forEach((child) => child.parentNode?.removeChild(child));
childElementsDetached.current = true;
if (element.current) {
const preventFocusOut = (e) => e.stopPropagation();
events.on(element.current, 'focusout', preventFocusOut);
events.triggerHandler(element.current, DX_REMOVE_EVENT, dxRemoveArgs);
events.off(element.current, 'focusout', preventFocusOut);
}
instance.current.dispose();
instance.current = null;
}
optionsManager.current.dispose();
removalLocker?.unlock();
}, [
removalLocker,
instance.current,
childNodes.current,
element.current,
optionsManager.current,
childElementsDetached.current,
shouldRestoreFocus.current,
]);
useLayoutEffect(() => {
onComponentMounted();
return () => {
onComponentUnmounted();
};
}, []);
useLayoutEffect(() => {
onComponentUpdated();
});
useImperativeHandle(ref, () => ({
getInstance() {
return instance.current;
},
getElement() {
return element.current;
},
createWidget(el) {
createWidget(el);
},
}), [instance.current, element.current, createWidget]);
const _renderChildren = useCallback(() => {
if (renderChildren) {
return renderChildren();
}
const { children } = props;
return children;
}, [props, renderChildren]);
const renderPortal = useCallback(() => portalContainer.current && createPortal(_renderChildren(), portalContainer.current), [portalContainer.current, _renderChildren]);
const renderContent = useCallback(() => {
const { children } = props;
return isPortalComponent && children
? React.createElement('div', {
ref: (node) => {
if (node && portalContainer.current !== node) {
portalContainer.current = node;
setForceUpdateToken(Symbol('force update token'));
}
},
style: { display: 'contents' },
})
: _renderChildren();
}, [
props,
isPortalComponent,
portalContainer.current,
_renderChildren,
]);
const renderContextValue = useMemo(() => ({
isTemplateRendering: false,
}), []);
return (React.createElement(RestoreTreeContext.Provider, { value: restoreTree },
React.createElement(TemplateRenderingContext.Provider, { value: renderContextValue },
React.createElement("div", { ref: childrenContainerRef, ...getElementProps() },
React.createElement(NestedOptionContext.Provider, { value: context }, renderContent()),
React.createElement(TemplateManager, { init: setTemplateManagerHooks, onTemplatesRendered: onTemplatesRendered }),
isPortalComponent
&& React.createElement(NestedOptionContext.Provider, { value: context }, renderPortal())))));
});
export { ComponentBase, DX_REMOVE_EVENT, };