Как получить ссылку на дочерний DOM элемент jsx объекта?

Я оборачиваю компонент в декоратор и мне важно, чтобы элемент внутри себя не имел никакого дополнительного кода, связанного с декоратором. В самом декораторе я пытаюсь понять есть ли компонент в иерархии DOM элементов при клике и самого компонента там нет, так как это jsx объект. Но есть все дочерние элементы в том числе и тот div, с которого начинается рендеринг jsx объекта. Именно его я и хочу засечь в методе handleClickOutside. Мне нужно каким то образом получить ссылку именно на div, на тот div c которого начинается рендеринг внутри оборачиваемого jsx элемента. Тогда проверка внутри этого метода будет работать корректно.

/** Decorator function for wrapped dropdown menu component  */
export const dropdownComponent = label => (WrappedComponent) => {

    class Decorator extends React.Component {

        constructor(props) {
            super(props);
            this.label = label

            this.wrappedComponentRef = React.createRef();
            this.handleClickOutside = this.handleClickOutside.bind(this);
        }

        handleClickOutside = event => {
            const path = event.path || (event.composedPath && event.composedPath());
            if (!path.includes(this.wrappedComponentRef)) {

                console.log(path)
                console.log(this.wrappedComponentRef)
            }
        }



        render()
        {
            /** this if calls bad state warning */
            const currentDropdown = this.props.dropdowns.find(dropdown => dropdown.label === this.label)

            if (currentDropdown && currentDropdown.isOpen) {
                document.addEventListener("click", this.handleClickOutside);

                return <WrappedComponent
                    {...this.props}
                    ref ={this.wrappedComponentRef}
                />

            } else {
                document.removeEventListener("click", this.handleClickOutside);
                return <div className='absolute'/>;
            }
        }
    }
    return connect(mapStateToProps, mapDispatchToProps)(Decorator)
}

В логах показывается иерархия DOM c нужным мне div-ом и инстанс оборачиваемого jsx объекта, которого нет в DOM поскольку jsx.


Ответы (1 шт):

Автор решения: Sergey Inozemcev

В общем, не получается совсем без сетапа обойтись. приходится прокидывать ссылку через аргументы декоратора.

код декоратора:

import React from "react";
import {connect} from "react-redux";
import {ToggleDropdownAC} from "../redux/ux/dropdownReducer";

let mapStateToProps = (state) =>
{
    return {
        /** Preloading status */
        dropdowns: state.ux2.dropdowns,
    }
}

let mapDispatchToProps = (dispatch) =>
{
    return {
        /** Toggle preloader status */
        toggleDropdown: (isOpen, label) => { dispatch( ToggleDropdownAC(isOpen, label) ) }
    }
}

/** Decorator function for wrapped components which needs preloading  */
export const dropdownComponent = (label, ref) => (WrappedComponent) => {

    class Decorator extends React.Component {

        constructor(props) {
            super(props);
            this.label = label
            this.absoluteChildRef = ref
            this.handleClickOutside = this.handleClickOutside.bind(this);
        }

        handleClickOutside = event => {
            /** Get event path Dom hierarchy */
            const path = event.path || (event.composedPath && event.composedPath());

            /** dropdown DOM elements */
            const absoluteChild = this.absoluteChildRef.current
            const relativeParent = this.absoluteChildRef.current.parentElement

            /** if path not includes dropdown or dropdown parent */
            if (!path.includes(absoluteChild)
            &&  !path.includes(relativeParent))
            {
                this.props.toggleDropdown(false, this.label)
            }
        }

        render()
        {
            /** current dropdown status */
            const currentDropdown = this.props.dropdowns.find(dropdown => dropdown.label === this.label)

            /** Render dropdown if open */
            if (currentDropdown && currentDropdown.isOpen)
            {
                document.addEventListener("click", this.handleClickOutside);
                return  <WrappedComponent {...this.props}/>
            }
            /** Render empty div if not */
            else {
                document.removeEventListener("click", this.handleClickOutside);
                return <></>;
            }
        }
    }
    return connect(mapStateToProps, mapDispatchToProps)(Decorator)
}

export const withDropdown = label => (WrappedComponent) => {

    class Decorator extends React.Component {

        /** Decorator for Relative parent */
        constructor(props) {
            super(props);
            this.label = label
            this.handleDropdownOn = this.handleDropdownOn.bind(this);
        }

        handleDropdownOn = event => {
           this.props.toggleDropdown(true, this.label)
        }

        render() {
            return <WrappedComponent {...this.props} dropdownOn={this.handleDropdownOn}/>
        }
    }

    return connect(mapStateToProps, mapDispatchToProps)(Decorator)
} 

код редюсера:

/** Reducer which service dropdowns  */

const TOGGLE_DROPDOWN = 'TOGGLE_DROPDOWN'

let initialState =
    {
        dropdowns:[
            { isOpen: false, label: "headerDropdown", children: null }
        ]
    }

const dropdownReducer = (state = initialState, action) => {

    switch (action.type) {
        /** Handle ToggleDropdown */
        case TOGGLE_DROPDOWN: {

            let currentDropdown = state.dropdowns.find(dropdown => dropdown.label === action.label)
            currentDropdown.isOpen = action.isOpen

            return {
                ...state,
                /** Copy dropdowns */
                dropdowns:[...state.dropdowns]
            }

        }

        default: return state
    }
}
export default dropdownReducer

export const ToggleDropdownAC = (isOpen, label) => { return { type:TOGGLE_DROPDOWN, isOpen, label } }

Чтобы использовать нужно минимально поднастроить компоненты. Поймать ссылку в родителе:

@withDropdown('headerDropdown')
class UserItem extends React.Component {

    constructor(props) {
        super(props);
    }

    render () {
        return <StyledRightSideItems onClick={this.props.dropdownOn}>
            <HeaderUserName />
            <HeaderUserIcon />
            <HeaderUserDropdownControl />
        </StyledRightSideItems>
    }

}
export default UserItem

И отдать ссылку в сплывающем меню:

/** Reference to dropdown absolute container*/
const dropdownRootRef = React.createRef()

@dropdownComponent('headerDropdown', dropdownRootRef)
class UserDropdown extends React.Component {

    render () {
        return  <StyledUserDropdown ref={dropdownRootRef}>
                        <UserDropdownLiner>
                            <ConnectItem/>
                            <UserDropdownSpacer/>
                            <SettingsItem/>
                            <HelpItem/>
                            <UserDropdownSpacer/>
                            <LogoutItem/>
                        </UserDropdownLiner>
                    </StyledUserDropdown>
    }
}
export default UserDropdown

Редюсер тоже можно улучшить, лучше чтобы декоратор сам динамически его регистрировал и настраивал на обслуживание произвольного количество попапов.

→ Ссылка