Reputation: 2406
I am trying to build a generic HOC for a closing element on click of outside its space(generic close on outside solution).
As I see it ,this could be achieved with forwardRef and HOC implementation and although there is an example in official docs I cannot seem to get it right.
So I want my HOC to create a reference to the container of component. It is wrapping because it has handlers to track clicks and act upon them. For instance, lets say we have a generic Dropdown component, one would expect that I can close it on any click outside the area of this component.
The code I currently have:
import React from 'react';
function withClose(Component) {
class ClickContainer extends React.Component {
constructor() {
super();
this.handleClose = this.handleClose.bind(this);
}
componentDidMount() {
document.addEventListener('click', this.handleClose);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClose);
}
handleClose(e) {
// I expect having here context of container of wrapped component to do something like
const { forwardedRef } = this.props; // <- I expect having context in forwardedRef variable
}
render() {
const { forwardedRef, ...rest } = this.props;
return <Component ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
return <ClickContainer {...props} forwardedRef={ref} />;
});
}
export default withClose;
What am I missing here? I cannot make it work, I only get context of wrapped component not the element itself.
Thanks a bunch!
Upvotes: 5
Views: 7628
Reputation: 958
Ref should be passed down to the element
Checkout https://codesandbox.io/s/7yzoqm747x
Assuming
export const Popup = (props,) => {
const { name, forwardRef } = props;
return (
<div ref={forwardRef}> // You need to pass it down from props
{name}
</div>
)
}
And the HOC
export function withClose(Component) {
class ClickContainer extends React.Component {
constructor() {
super();
this.handleClose = this.handleClose.bind(this);
}
componentDidMount() {
document.addEventListener('click', this.handleClose);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClose);
}
handleClose(e) {
const { forwardRef } = this.props;
console.log(forwardRef);
}
render() {
const { forwardRef, ...rest } = this.props;
return <Component forwardRef={forwardRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
return <ClickContainer {...props} forwardRef={ref} />;
});
}
And expect
const CloseablePopup = withClose(Popup);
class App extends Component {
popupRef = React.createRef();
render() {
return (<CloseablePopup ref={popupRef} name="Closable Popup" />);
}
}
Upvotes: 3
Reputation: 934
I Implemented the same thing few days ago.
I wanted a HOC that handle onClickoutside
I wanted that component to check on each click if the child was click or not and if it did to invoke a function on the child.
this was my solution:
import React from 'react';
export default function withClickOutside(WrappedComponent) {
class WithClickOutside extends WrappedComponent {
constructor(props) {
super(props);
this.handleClickOutside = this.handleClickOutside.bind(this);
this.wrappedElement = React.createRef();
}
componentDidMount() {
document.addEventListener('click', this.handleClickOutside, true);
}
componentWillUnmount() {
document.removeEventListener('click', this.handleClickOutside, true);
}
handleClickOutside(event) {
if (event.type !== 'click') return;
if (!this.wrappedElement.current) {
throw new Error(`No ref for element ${WrappedComponent.name}. Please create ref when using withClickOutside`);
}
if (!this.wrappedElement.current.contains(event.target)) {
if (!this.onClickOutside) {
throw new Error(`${WrappedComponent.name} does not implement onClickOutside function. Please create onClickOutside function when using withClickOutside`);
}
this.onClickOutside(event);
}
}
render() {
const wrapped = super.render();
const element = React.cloneElement(
wrapped,
{ ref: this.wrappedElement },
);
return element;
}
}
return WithClickOutside;
}
Then the component you wrap must implement a function called onClickOutside
.
Upvotes: 1