UNPKG

devextreme-react

Version:

DevExtreme React UI and Visualization Components

323 lines (321 loc) • 13.1 kB
/*! * 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, };