How to detect overflow of React component without ReactDOM? - javascript

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?

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;
},

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>
);
}
}

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.

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',
});

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>
)}

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>
);
};

Related

How do I reference an element that is to be returned by a React component?

I want to start out by telling you that I am new to react and I come from a background of working almost entirely with vanilla JS and HTML.
I want to know how to reference/pass an element as an argument in a function similar to how you would go about it in JS:
const myElement = document.getElementById("my-element");
or
const myElement = document.createElement("div");
function testFunc(element) {
return element.getBoundingClientRect();
}
testFunc(myElement);
I have googled a bit but haven't found any good answers, all I could find was about "ref" but I am sure there is a simpler solution.
This is what I have in React now:
import React from "react";
import "./myComp.css";
function myComp(props) {
const sliderContainer = (
<div className="slider-container" style={props.styles}></div>
);
function myFunc(element) {
return element.getBoundingClientRect();
}
const elementDimensions = myFunc(sliderContainer);
return { sliderContainer };
}
export default myComp;
But what i want to do is:
import React from "react";
import "./myComp.css";
function myComp(props) {
const sliderContainer = "slider-container" //SOME SORT OF REFRENCE / SELECTOR, I'VE FIGURED OUT THAT querySelectors IS NOT THE RIGHT APPORACH
function myFunc(element) {
return element.getBoundingClientRect();
}
const elementDimensions = myFunc(sliderContainer);
return (
<div className="slider-container" style={props.styles}>
<div className="myChild"></div>
</div>
);
}
export default myComp;
This should work:
const sliderContainer = useRef(null);
function myFunc(element) {
return element.getBoundingClientRect();
}
useEffect(() => {
const elementDimensions = myFunc(sliderContainer.current);
});
return (
<div ref={sliderContainer} className="slider-container" style={props.styles}>
<div className="myChild"></div>
</div>
);
The ref property will assign the element to sliderContainer.current once the component has been mounted.
Note that the value is initially set to be null: useRef(null).
You must wait until the element is not null before accessing it as the element will not yet be mounted when the component is initially rendered.
in React you can use
document.getElementById('id-object');
or
document.querySelector('object-class');
But you can find more info in this article on Medium.
Hope that this was useful!
You can use document.getElementById('slider-container'); or any other use of document in React.

Using React's useRef hook to handle onKeyDown event

my first experience using useRef and forwardRef. I've been given some hints on a potential solution but I'm still a little way off, however the component is small and fairly simple.
What I am trying to do is use useRef to pass the result of a handleOnKeyDown function, which basically is looking for a keyCode 13 ("Enter" on keyboard) and then "click" the findGoodsBtnRef by passing it down into the CcceAttribute component via ref={findGoodsBtn}
I know what I have at current isn't right (because it doesn't work) and my editor is complaining about my use of ForwardRef on the top line, can anyone advise where I'm potentially going wrong on trying to implement this solution? TIA
const CommodityCodeLookupAttribute = forwardRef<Props, typeof CcceAttribute>(
({ attribute: productLookupAttribute, onChange, ...props }: ref) => {
const { object } = useFormObjectContext();
const ccceAttribute = object.attributeCollection.find(isCcceResultAttribute);
if (!ccceAttribute) {
return null;
}
const findsGoodsBtnRef = React.useRef();
const findGoodsBtn = findsGoodsBtnRef.current;
const handleOnKeyDown = (event) => {
if (event.keyCode === "13") {
// event.preventDefault();
console.log("Default prevented");
findsGoodsBtnRef.current.click();
// use the ref that points to the ccce attribute button -> click()
}
};
return (
<>
<StringAttributeDefault
attribute={productLookupAttribute}
onKeyDown={handleOnKeyDown}
{...otherProps}
/>
<CcceAttribute attribute={ccceAttribute} ref={findGoodsBtn} />
</>
);
};

How to implement progress bar in React JS?

I am trying to implement a progress bar in my react js project, but It's not working properly.
Code for child component where I have implemented progress bar -
export const Child = (props: ChildProps): React.ReactElement => {
const { text, showProgress, typeId } = props;
const [progress, setProgress] = React.useState<number>(0);
React.useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
if (oldProgress === 100) {
return 0;
}
const diff = Math.random() * 100;
return Math.min(oldProgress + diff, 100);
});
}, 500);
return () => {
if(progress === 100){
clearInterval(timer);
}
};
}, []);
return (
<Grid container>
<Grid container item direction="column">
<Label>{text}</Label>
<Select
options={myOptions}
value={mySelectedValue}
onChange={(selectedOption: Select<string, string>) =>
onChange('TYPE', selectedOption.value, typeId)
}
/>
</Grid>
{showProgress && (<ProgressBarContainer>
<ProgressBar color={'Blue'} height={'Small'} progress={progress} />
</ProgressBarContainer>)
}
</Grid>
);
}
I have defined a callback for onChange function in parent component. Everytime I call onChange event, it should display the progress.
Code for parent component -
const [showProgress , setShowProgress] = React.useState<boolean>(false);
const [isApiCalled , setIsApiCalled] = React.useState<boolean>(false);
const [myData , setMyData] = React.useState<any>([]);
const onChange = (type: string, value: string, typeid: number): void => {
if (type === 'TYPE') {
setShowProgress(true);
setIsApiCalled(true);
}
setMyData(updateData); // I haven't written the code for it here.
}
const saveData = (isSave: boolean): void => {
callAPI(saveRequestData); // calling API
setShowProgress(false);
}
React.useEffect(()=> {
saveData(true)
setIsApiCalled(false);
}, [isApiCalled])
return (
<Child myData={myData} onChange={onChange} showProgress={showProgress} />;
);
Everytime I change the data in Child, it should call progress as I am setting the progress to be true. It is calling the progress but not in a proper way. Like progress should start from 0 everytime I do call onChange and should increase continue. But first time it starts from 0 but it never continue increasing and afterwards it never starts from 0. I don't know what I am doing wrong. Can anyone help ?
In my app I have included a progress bar too, more specifically a circular progress bar. I have used an already developed package. You can follow the instructions here: https://www.npmjs.com/package/react-circular-progressbar
A simple implementation:
import { CircularProgressbar } from "react-circular-progressbar";
const MyComponent: React.FC = () => {
const points = 30000 //only of testing
const percentage = (points / 10000) * 100; //calculate the percentage
return (
<div>
<CircularProgressbar value={percentage} text={points} />
</div>
);
}
export default MyComponent;
For custom styling and CSS, you can copy and modify the code here: https://github.com/kevinsqi/react-circular-progressbar/blob/HEAD/src/styles.css
It seems because of closure, useEffect() captures the initial value there on mount and never updates it. Since Child is already mounted and useEffect() is not called after mount setInterval always has initial value of progress that is 0 and progress is not updated.
Similar problem is explained by Dan Abramov in his blog:
Excerpt from his blog about this problem
The problem is that useEffect captures the count from the first
render. It is equal to 0. We never re-apply the effect so the closure
in setInterval always references the count from the first render, and
count + 1 is always 1. Oops!
To solve this, you can use useRef hook to capture callback inside interval and on re-render just update it with new reference of the callback. So that setInterval points to updated callback after every interval. Something like:
useEffect(() => {
savedCallback.current = callback;
});
You can refer Dan's blog for clear understanding
https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Using multiple refs on a single React element

I'm using the useHover() react hook defined in this recipe. The hook returns a ref and a boolean indicating whether the user is currently hovering over element identified by this ref. It can be used like this...
function App() {
const [hoverRef, isHovered] = useHover();
return (
<div ref={hoverRef}>
{isHovered ? 'Hovering' : 'Not Hovering'}
</div>
);
}
Now let's say that I want to use another (hypothetical) hook called useDrag which returns a ref and a boolean indicating whether the user is dragging the current element around the page. I want to use this on the same element as before like this...
function App() {
const [hoverRef, isHovered] = useHover();
const [dragRef, isDragging] = useDrag();
return (
<div ref={[hoverRef, dragRef]}>
{isHovered ? 'Hovering' : 'Not Hovering'}
{isDragging ? 'Dragging' : 'Not Dragging'}
</div>
);
}
This won't work because the ref prop can only accept a single reference object, not a list like in the example above.
How can I approach this problem so I can use multiple hooks like this on the same element? I found a package that looks like it might be what I'm looking for, but I'm not sure if I'm missing something.
A simple way to go about this is documented below.
Note: the ref attribute on elements takes a function and this function is later called with the element or node when available.
function App() {
const myRef = useRef(null);
return (
<div ref={myRef}>
</div>
);
}
Hence, myRef above is a function with definition
function(element){
// something done here
}
So a simple solution is like below
function App() {
const myRef = useRef(null);
const anotherRef = useRef(null);
return (
<div ref={(el)=> {myRef(el); anotherRef(el);}}>
</div>
);
}
A React ref is really nothing but a container for some mutable data, stored as the current property. See the React docs for more details.
{
current: ... // my ref content
}
Considering this, you should be able to sort this out by hand:
function App() {
const myRef = useRef(null);
const [hoverRef, isHovered] = useHover();
const [dragRef, isDragging] = useDrag();
useEffect(function() {
hoverRef.current = myRef.current;
dragRef.current = myRef.current;
}, [myRef.current]);
return (
<div ref={myRef}>
{isHovered ? 'Hovering' : 'Not Hovering'}
{isDragging ? 'Dragging' : 'Not Dragging'}
</div>
);
}

React Page Scrolls on top after re render by useEffect

My problem - whenever i use <Tabs> component the onChange method calls the handleTabChange function. The component gets called again and the after repainting the useEffect gets called. This causes my page to scroll on top.
How do i make sure the handleChange and useEffect does not cause the page to scroll on top?
export function VarianceGraph({ variancesGraphData }) {
if (variancesGraphData === undefined) {
return null;
}
const [currentProject, setCurrentProject] = useState('All');
const [currentTab, setCurrentTab] = useState('ABCD');
const projectCodes = prepareProjectCodesForConsumption(variancesGraphData);
let data = {};
function handleProjectChange(event) {
const { value } = event.target;
setCurrentProject(value);
}
function handleTabChange(event, tabValue) {
setCurrentTab(tabValue);
}
data = prepareProjectDataForRechartsConsumption(currentProject, variancesGraphData);
const hideABCD = data.ABCD.length < 1;
const hideBCDF = data['BCDF'].length < 1;
useEffect(() => {
if (hideABCD && hideBCDF) {
setCurrentTab('None');
} else if (hideABCD) {
setCurrentTab('BCDF');
} else if (hideBCDF) {
setCurrentTab('ABCD');
}
});
return (
<Grid container direction="row" justify="flex-start" alignItems="flex-start">
<Grid item xs={12}>
I have tried -
I need to have the setCurrentTab in some function so as to not render infinitely.
I have tried using the useLayoutEffect but it has the same behavior.
I can write the component code further if required. Please help. Thanks in advance.
If I am understanding right, you only want your useEffect code run once after VarianceGraph render.
Try to add a second argument [] to your useEffect, otherwise the code in useEffect will run after any state change.
I went ahead and searched the problem and found the below solutions
Keep scroll-y after rerender
But the problem is the getSnapshotBeforeUpdate is not available in hooks
as can be seen here
getSnapshotBeforeUpdate using react hooks
So, the best option is to convert the component to a class component and follow the example here : https://reactjs.org/docs/react-component.html#getsnapshotbeforeupdate
Sample code, in your case, you might have to bind the document.body.scrollHeight
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// Are we adding new items to the list?
// Capture the scroll position so we can adjust scroll later.
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// If we have a snapshot value, we've just added new items.
// Adjust scroll so these new items don't push the old ones out of view.
// (snapshot here is the value returned from getSnapshotBeforeUpdate)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/* ...contents... */}</div>
);
}
}

Categories