Reputation: 1645
Basically i want to be able to detect if a react component has children which are overflowing. Just as in this question. I have found that the same thing is possible using ReactDOM, however i cannot/should not use ReactDOM. I don't see anything on the suggested alternative,ref
, that is equivalent.
So what i need to know is if it is possible to detect overflow within a react component under these conditions. And to the same point, is it possible to detect width at all?
Upvotes: 22
Views: 75126
Reputation: 521
The same could be achieved using React hooks:
The first thing you need would be a state which holds boolean values for text open and overflow active:
const [textOpen, setTextOpen] = useState(false);
const [overflowActive, setOverflowActive] = useState(false);
Next, you need a ref on the element you want to check for overflowing:
const textRef = useRef();
<p ref={textRef}>
Some huuuuge text
</p>
The next thing is a function that checks if the element is overflowing:
function isOverflowActive(event) {
return event.offsetHeight < event.scrollHeight || event.offsetWidth < event.scrollWidth;
}
Then you need a useEffect hook that checks if the overflow exists with the above function:
useEffect(() => {
if (isOverflowActive(textRef.current)) {
setOverflowActive(true);
return;
}
setOverflowActive(false);
}, [isOverflowActive]);
And now with those two states and a function that checks the existence of an overflowing element, you can conditionally render some element (eg. Show more button):
{!textOpen && !overflowActive ? null : (
<button>{textOpen ? 'Show less' : 'Show more'}</button>
)}
Upvotes: 9
Reputation: 183
I needed to achieve this in React TypeScript, as such here is the updated solution in TypeScript using React Hooks. This solution will return true if there are at least 4 lines of text.
We declare the necessary state variables:
const [overflowActive, setOverflowActive] = useState<boolean>(false);
const [showMore, setShowMore] = useState<boolean>(false);
We declare the necessary ref using useRef
:
const overflowingText = useRef<HTMLSpanElement | null>(null);
We create a function that checks for overflow:
const checkOverflow = (textContainer: HTMLSpanElement | null): boolean => {
if (textContainer)
return (
textContainer.offsetHeight < textContainer.scrollHeight || textContainer.offsetWidth < textContainer.scrollWidth
);
return false;
};
Lets build a useEffect
that will be called when overflowActive
changes and will check our current ref object to determine whether the object is overflowing:
useEffect(() => {
if (checkOverflow(overflowingText.current)) {
setOverflowActive(true);
return;
}
setOverflowActive(false);
}, [overflowActive]);
In our component's return statement, we need to bind the ref to an appropriate element. I am using Material UI
coupled with styled-components
so the element in this example will be StyledTypography
:
<StyledTypography ref={overflowingText}>{message}</StyledTypography>
Styling the component in styled-components
:
const StyledTypography = styled(Typography)({
display: '-webkit-box',
'-webkit-line-clamp': '4',
'-webkit-box-orient': 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
Upvotes: 14
Reputation: 101
To anyone who wonder how it can be done with hooks and useRef:
// This is custom effect that calls onResize when page load and on window resize
const useResizeEffect = (onResize, deps = []) => {
useEffect(() => {
onResize();
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [...deps, onResize]);
};
const App = () => {
const [isScrollable, setIsScrollable] = useState(false);
const [container, setContainer] = useState(null);
// this has to be done by ref so when window event resize listener will trigger - we will get the current element
const containerRef = useRef(container);
containerRef.current = container;
const setScrollableOnResize = useCallback(() => {
if (!containerRef.current) return;
const { clientWidth, scrollWidth } = containerRef.current;
setIsScrollable(scrollWidth > clientWidth);
}, [containerRef]);
useResizeEffect(setScrollableOnResize, [containerRef]);
return (
<div
className={"container" + (isScrollable ? " scrollable" : "")}
ref={(element) => {
if (!element) return;
setContainer(element);
const { clientWidth, scrollWidth } = element;
setIsScrollable(scrollWidth > clientWidth);
}}
>
<div className="content">
<div>some conetnt</div>
</div>
</div>
);
};
Upvotes: 4
Reputation: 1999
The implementation of the solution proposed by @Jemar Jones:
export default class OverflowText extends Component {
constructor(props) {
super(props);
this.state = {
overflowActive: false
};
}
isEllipsisActive(e) {
return e.offsetHeight < e.scrollHeight || e.offsetWidth < e.scrollWidth;
}
componentDidMount() {
this.setState({ overflowActive: this.isEllipsisActive(this.span) });
}
render() {
return (
<div
style={{
width: "145px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
overflow: "hidden"
}}
ref={ref => (this.span = ref)}
>
<div>{"Triggered: " + this.state.overflowActive}</div>
<span>This is a long text that activates ellipsis</span>
</div>
);
}
}
Upvotes: 15
Reputation: 1645
In addition to @jered's excellent answer, i'd like to mention the qualifier that a ref will only return an element that directly has access to the various properties of regular DOM elements if the ref is placed directly on a DOM element. That is to say, it does not behave in this way with Components.
So if you are like me and have the following:
var MyComponent = React.createClass({
render: function(){
return <SomeComponent id="my-component" ref={(el) => {this.element = el}}/>
}
})
and when you attempt to access DOM properties of this.element
(probably in componentDidMount
or componentDidUpdate
) and you are not seeing said properties, the following may be an alternative that works for you
var MyComponent = React.createClass({
render: function(){
return <div ref={(el) => {this.element = el}}>
<SomeComponent id="my-component"/>
</div>
}
})
Now you can do something like the following:
componentDidUpdate() {
const element = this.element;
// Things involving accessing DOM properties on element
// In the case of what this question actually asks:
const hasOverflowingChildren = element.offsetHeight < element.scrollHeight ||
element.offsetWidth < element.scrollWidth;
},
Upvotes: 27
Reputation: 11581
Yep, you can use ref
.
Read more about how ref
works in the official documentation: https://facebook.github.io/react/docs/refs-and-the-dom.html
Basically, ref
is just a callback that is run when a component renders for the first time, immediately before componentDidMount
is called. The parameter in the callback is the DOM element that is calling the ref
function. So if you have something like this:
var MyComponent = React.createClass({
render: function(){
return <div id="my-component" ref={(el) => {this.domElement = el}}>Hello World</div>
}
})
When MyComponent
mounts it will call the ref
function that sets this.domElement
to the DOM element #my-component
.
With that, it's fairly easy to use something like getBoundingClientRect()
to measure your DOM elements after they render and determine if the children overflow the parent:
https://jsbin.com/lexonoyamu/edit?js,console,output
Keep in mind there is no way to measure the size/overflow of DOM elements before they render because by definition they don't exist yet. You can't measure the width/height of something until you render it to the screen.
Upvotes: 12