Как получить ссылку на дочерний 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 шт):
В общем, не получается совсем без сетапа обойтись. приходится прокидывать ссылку через аргументы декоратора.
код декоратора:
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
Редюсер тоже можно улучшить, лучше чтобы декоратор сам динамически его регистрировал и настраивал на обслуживание произвольного количество попапов.