I am trying to find out if a div has overflown text and show show more link if it does. I found this stackoverflow answer to check if a div is overflowing. According to this answer, I need to implement a function which can access styles of the element in question and do some checks to see if it is overflowing. How can I access the styles of an element. I tried 2 ways
1. Using ref
import React from "react";
import "./styles.css";
export default function App(props) {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if ( !curOverflow || curOverflow === "visible" )
el.style.overflow = "hidden";
const isOverflowing = el.clientWidth < el.scrollWidth
|| el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
const ref = React.createRef();
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{(checkOverflow()) && <span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>}
</>
)
}
2. Using forward ref
Child component
export const App = React.forwardRef((props, ref) => {
const [showMore, setShowMore] = React.useState(false);
const onClick = () => {
setShowMore(!showMore);
};
const checkOverflow = () => {
const el = ref.current;
const curOverflow = el.style.overflow;
if (!curOverflow || curOverflow === "visible") el.style.overflow = "hidden";
const isOverflowing =
el.clientWidth < el.scrollWidth || el.clientHeight < el.scrollHeight;
el.style.overflow = curOverflow;
return isOverflowing;
};
return (
<>
<div ref={ref} className={showMore ? "container-nowrap" : "container"}>
{props.text}
</div>
{checkOverflow() && (
<span className="link" onClick={onClick}>
{showMore ? "show less" : "show more"}
</span>
)}
</>
);
});
Parent component
import React from "react";
import ReactDOM from "react-dom";
import { App } from "./App";
const rootElement = document.getElementById("root");
const ref = React.createRef();
ReactDOM.render(
<React.StrictMode>
<App
ref={ref}
text="Start editing to see some magic happen! Click show more to expand and show less to collapse the text"
/>
</React.StrictMode>,
rootElement
);
But I got the following error in both approaches - Cannot read property 'style' of null.
What am I doing wrong? How can I achieve what I want?
As Jamie Dixon suggested in the comment, I used useLayoutEffect hook to set showLink true. Here is the code
Component
import React from "react";
import "./styles.css";
export default function App(props) {
const ref = React.createRef();
const [showMore, setShowMore] = React.useState(false);
const [showLink, setShowLink] = React.useState(false);
React.useLayoutEffect(() => {
if (ref.current.clientWidth < ref.current.scrollWidth) {
setShowLink(true);
}
}, [ref]);
const onClickMore = () => {
setShowMore(!showMore);
};
return (
<div>
<div ref={ref} className={showMore ? "" : "container"}>
{props.text}
</div>
{showLink && (
<span className="link more" onClick={onClickMore}>
{showMore ? "show less" : "show more"}
</span>
)}
</div>
);
}
CSS
.container {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 200px;
}
.link {
text-decoration: underline;
cursor: pointer;
color: #0d6aa8;
}
We could create a custom hooks to know if we have overflow.
import * as React from 'react';
const useIsOverflow = (ref, isVerticalOverflow, callback) => {
const [isOverflow, setIsOverflow] = React.useState(undefined);
React.useLayoutEffect(() => {
const { current } = ref;
const { clientWidth, scrollWidth, clientHeight, scrollHeight } = current;
const trigger = () => {
const hasOverflow = isVerticalOverflow ? scrollHeight > clientHeight : scrollWidth > clientWidth;
setIsOverflow(hasOverflow);
if (callback) callback(hasOverflow);
};
if (current) {
trigger();
}
}, [callback, ref, isVerticalOverflow]);
return isOverflow;
};
export default useIsOverflow;
and just check in your component
import * as React from 'react';
import { useIsOverflow } from './useIsOverflow';
const App = () => {
const ref = React.useRef();
const isOverflow = useIsOverflow(ref);
console.log(isOverflow);
// true
return (
<div style={{ overflow: 'auto', height: '100px' }} ref={ref}>
<div style={{ height: '200px' }}>Hello React</div>
</div>
);
};
Thanks to Robin Wieruch for his awesome articles
https://www.robinwieruch.de/react-custom-hook-check-if-overflow/
Solution using TS and Hooks
Create your custom hook:
import React from 'react'
interface OverflowY {
ref: React.RefObject<HTMLDivElement>
isOverflowY: boolean
}
export const useOverflowY = (
callback?: (hasOverflow: boolean) => void
): OverflowY => {
const [isOverflowY, setIsOverflowY] = React.useState(false)
const ref = React.useRef<HTMLDivElement>(null)
React.useLayoutEffect(() => {
const { current } = ref
if (current) {
const hasOverflowY = current.scrollHeight > window.innerHeight
// RHS of assignment could be current.scrollHeight > current.clientWidth
setIsOverflowY(hasOverflowY)
callback?.(hasOverflowY)
}
}, [callback, ref])
return { ref, isOverflowY }
}
use your hook:
const { ref, isOverflowY } = useOverflowY()
//...
<Box ref={ref}>
...code
Import your files as need be and update code to your needs.
Related
I created a WindowPortal to open many new external windows/tabs.
https://codesandbox.io/s/modest-goldwasser-q6lig?file=/src/App.js
How can I pass props to a props.children in createPortal function?
I want pass newWindow as a prop for handling resize of new window.
import React, { useState, useRef, useEffect, useCallback, Children, cloneElement } from "react";
import { createPortal } from "react-dom";
import { create } from "jss";
import { jssPreset, StylesProvider, CssBaseline } from "#material-ui/core";
type WindowPortalProps = {
width: number;
height: number;
close: () => void;
id: number;
title: string;
};
const WindowPortal: React.FC<WindowPortalProps> = (props) => {
const [container, setContainer] = useState<HTMLElement | null>(null);
const newWindow = useRef<Window | null>(null);
const { title } = props;
const [jss, setJss] = useState<any>(null);
useEffect(() => {
// Create container element on client-side
setContainer(document.createElement("div"));
}, []);
const close = useCallback(() => {
props.close();
}, [props]);
useEffect(() => {
// When container is ready
if (container) {
setJss(create({ ...jssPreset(), insertionPoint: container }));
// Create window
newWindow.current = window.open(
"",
"",
`width=${props.width},height=${props.height},left=200,top=200,scrollbars,resizable,menubar,toolbar,location`,
) as Window;
// Append container
newWindow.current.document.body.appendChild(container);
newWindow.current.document.title = title;
const stylesheets = Array.from(document.styleSheets);
stylesheets.forEach((stylesheet) => {
const css = stylesheet as CSSStyleSheet;
const owner = stylesheet.ownerNode as HTMLElement;
if (owner.dataset.jss !== undefined) {
// Ignore JSS stylesheets
return;
}
if (stylesheet.href) {
const newStyleElement = document.createElement("link");
newStyleElement.rel = "stylesheet";
newStyleElement.href = stylesheet.href;
newWindow.current?.document.head.appendChild(newStyleElement);
} else if (css && css.cssRules && css.cssRules.length > 0) {
const newStyleElement = document.createElement("style");
Array.from(css.cssRules).forEach((rule) => {
newStyleElement.appendChild(document.createTextNode(rule.cssText));
});
newWindow.current?.document.head.appendChild(newStyleElement);
}
});
// Save reference to window for cleanup
const curWindow = newWindow.current;
curWindow.addEventListener("beforeunload", close);
// Return cleanup function
return () => {
curWindow.close();
curWindow.removeEventListener("beforeunload", close);
};
}
}, [container]);
return (
container &&
newWindow.current &&
jss &&
createPortal(
<StylesProvider jss={jss} sheetsManager={new Map()}>
<CssBaseline />
{props.children} -> how pass newWindow.current as externalWindow props?
</StylesProvider>,
container,
)
);
};
const ExpandedComponentForWindowPortal = ({ externalWindow, ...rest }) => {
const [countOfResize, setCountOfResize] = useState(0);
const doSomething = () => setCountOfResize(countOfResize + 1);
useEffect(() => {
externalWindow.addEventListener("resize", doSomething);
return () => {
externalWindow.removeEventListener("resize", doSomething);
setCountOfResize(0)
}
}, []);
return <></>;
}
const ComponentInExternalWindow = () => {
const closeWindow = () => {}
return (
<WindowPortal
title={"title"}
width={window.innerWidth}
heigth={window.innerHeigth}
key={2}
id={1}
close={closeWindow}
>
<ExpandedComponentForWindowPortal />
</WindowPortal>
)
}
I would suggest using a context for this use case.
In WindowPortalHooks:
const WindowPortalContext = createContext(null);
export const useWindowPortalContext = () => {
return useContext(WindowPortalContext);
};
and in the end of the function wrap the children with the context provider
return (
container &&
createPortal(
<WindowPortalContext.Provider
value={{
close
}}
>
{props.children}
</WindowPortalContext.Provider>,
container
)
);
Then in any child rendered by MyWindowPortal you could use useWindowPortalContext:
function Expanded(props) {
const { close } = useWindowPortalContext();
return (
<div>
<Typography align="center">{props.title}</Typography>
<button type="button" onClick={close}>
close me
</button>
</div>
);
}
like this i hava a array of components need ref to trigger the comment component collapse, so i need to create some refs to reference each commentListItem, but it doesn't work, how do i do this work?
import React, { useRef, createRef } from "react";
import PropTypes from "prop-types";
import { map, isArray } from "lodash/fp";
import Divider from "#material-ui/core/Divider";
import CommentListItem from "./CommentListItem";
import CommentCollapse from "./CommentCollapse";
function CommentList({ list = [], ...props }) {
const { count = 0 } = props;
const refList = map((o) => {
/* o.ref = createRef(null); */
return o;
})(list);
const onShow = () => {
console.log(refList);
};
return (
<div className="ke-comment-list">
{map.convert({ cap: false })((o, i) => (
<div key={i} className="ke-comment-list-item">
<CommentListItem listItem={o} onShow={onShow} />
{isArray(o.child) && o.child.length ? (
<CommentCollapse {...o}>
<CommentList list={o.child} count={count + 1} />
</CommentCollapse>
) : null}
{count > 0 && list.length - 1 === i ? null : <Divider />}
</div>
))(refList)}
</div>
);
}
CommentList.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
};
export default CommentList;
there is CommentCollapse component for show or hide subcomment.
import React, { useState, forwardRef, useImperativeHandle } from "react";
import ButtonBase from "#material-ui/core/ButtonBase";
import Collapse from "#material-ui/core/Collapse";
const CommentCollapse = ({ children }, ref) => {
const [show, setShow] = useState(false);
const showMore = () => {
setShow((prev) => !prev);
};
const collapseText = () => (show ? "收起" : "展开");
useImperativeHandle(ref, () => ({
showMore: showMore()
}));
return (
<div className="ke-comment-list-children">
<Collapse in={show}>{children}</Collapse>
<ButtonBase size="small" onClick={showMore}>
{collapseText()}
</ButtonBase>
</div>
);
};
export default forwardRef(CommentCollapse);
catch errors
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
have any idear for this situation?
is fixed, just not trigger showMore function in ref.
import React, { useState, forwardRef, useImperativeHandle } from "react";
import ButtonBase from "#material-ui/core/ButtonBase";
import Collapse from "#material-ui/core/Collapse";
const CommentCollapse = ({ children }, ref) => {
const [show, setShow] = useState(false);
const showMore = () => {
setShow((prev) => !prev);
};
const collapseText = () => (show ? "收起" : "展开");
useImperativeHandle(ref, () => ({
showMore
}));
return (
<div className="ke-comment-list-children">
<Collapse in={show}>{children}</Collapse>
<ButtonBase size="small" onClick={showMore}>
{collapseText()}
</ButtonBase>
</div>
);
};
export default forwardRef(CommentCollapse);
I am trying to update the individual style of each button when it is clicked, using the useRef() hook from React.
Right now, when I click any button the style change is always applied to the last button rendered.
I believe this is the bit needing attention but I'm stumped.
const handleClick = () => {
status.current.style.background = 'green';
}
Here's the full bit:
import React, { useRef } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
let background = 'blue';
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({active: false});
}
return controls;
};
const ControlPanel = () => {
const status = useRef('blue');
makeControls();
const handleClick = () => {
status.current.style.background = 'green';
}
return (
<>
{controls.map((control, i) => (
<div
ref={status}
style={{background: background}}
className={'box'}
key={i}
onClick={() => handleClick()}></div>
))}
</>
);
};
ReactDOM.render(<ControlPanel />, document.getElementById('root'));
Currently, your ref targets only the last item, you should target all your control items by making an array of refs.
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({ active: false });
}
return controls;
};
makeControls();
const ControlPanel = () => {
const status = useRef([]);
const handleClick = index => {
status.current[index].style.background = 'green';
};
return (
<>
{controls.map((control, i) => (
<div
ref={ref => (status.current[i] = ref)}
style={{ background: `blue`, width: 100, height: 100 }}
key={i}
onClick={() => handleClick(i)}
/>
))}
</>
);
};
When rendering the list of <div>s your status ref is getting reassigned each time, finally stopping on the last element.
which is why the last element gets updated.
Instead why not store the background color info on the control object itself
for (let i = 1; i <= 9; i++) {
controls.push({active: false,background: 'blue'});
}
{controls.map((control, i) => (
<div
style={{background: control.background}}
className={'box'}
key={i}
onClick={() => handleClick(control)}></div>
))}
const handleClick = (control) => {
control.background = 'green';
}
you can use state to do that
like this
import React, { useRef,useState } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
let controls = [];
const makeControls = () => {
for (let i = 1; i <= 9; i++) {
controls.push({active: false});
}
return controls;
};
const ControlPanel = () => {
const [controlState,setControlState]=useState({background:"blue"})
const status = useRef('blue');
makeControls();
const handleClick = () => {
setControlState({background:"green"});
}
return (
<>
{controls.map((control, i) => (
<div
ref={status}
style={{background: controlState.background}}
className={'box'}
key={i}
onClick={() => handleClick()}></div>
))}
</>
);
};
ReactDOM.render(<ControlPanel />, document.getElementById('root'));
As shown below, this TextInput component does a simple job: when input's value is empty, hides the title because placeholder shows the same words.
But the code doesn't work as expected. InputEvent does run, but reassign activeStyle has no effect.
import React, {useState} from 'react';
import './TextInput.css';
import * as CSS from 'csstype';
type TextInputProps = {
title: string
}
const TextInput: React.FC<TextInputProps> = ({title, children}) => {
const hiddenStyle: CSS.Properties = {
opacity: 0
};
const visibleStyle: CSS.Properties = {
opacity: 1
};
let activeStyle = hiddenStyle
const [rawTextInput, setRawTextInput] = useState("")
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
setRawTextInput(inputValue)
if(inputValue == ""){
activeStyle = hiddenStyle
} else {
activeStyle = visibleStyle
}
}
return (
<div className="TextInput">
<p
className="TextInputTitle"
style={activeStyle}
>
{title}
</p>
<input
className="TextInputField"
type="text"
placeholder={title}
value={rawTextInput}
onChange={InputEvent}
/>
{/*<p className="TextInputHint"></p>*/}
</div>
);
}
export default TextInput
import React, {useState} from 'react';
import './TextInput.css';
import * as CSS from 'csstype';
type TextInputProps = {
title: string
}
const TextInput: React.FC<TextInputProps> = ({title, children}) => {
const hiddenStyle: CSS.Properties = {
opacity: 0
};
const visibleStyle: CSS.Properties = {
opacity: 1
};
let activeStyle = hiddenStyle
const [rawTextInput, setRawTextInput] = useState("")
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
setRawTextInput(inputValue)
if(inputValue == ""){
activeStyle = hiddenStyle
} else {
activeStyle = visibleStyle
}
}
return (
<div className="TextInput">
<p
className="TextInputTitle"
style={activeStyle}
>
{title}
</p>
<input
className="TextInputField"
type="text"
placeholder={title}
value={rawTextInput}
onChange={InputEvent}
/>
{/*<p className="TextInputHint"></p>*/}
</div>
);
}
export default TextInput
Local variable doesn't affect re-render.
let activeStyle = hiddenStyle //local variable
You need to keep this in state and change using setter method.
const [activeStyle, setActiveStyle] = useState(hiddenStyle)
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
setRawTextInput(inputValue)
if(inputValue == ""){
setActiveStyle(hiddenStyle)
} else {
setActiveStyle(visibleStyle)
}
}
I've tried a simpler approach
import React, { useState } from "react";
type TextInputProps = {
title: string;
};
const TextInput: React.FC<TextInputProps> = ({ title, children }) => {
const [rawTextInput, setRawTextInput] = useState("");
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
setRawTextInput(e.target.value)
};
return (
<div className="TextInput">
<p className="TextInputTitle" style={{opacity : rawTextInput == "" ? 0 : 1}}>
{title}
</p>
<input
className="TextInputField"
type="text"
placeholder={title}
value={rawTextInput}
onChange={InputEvent}
/>
{/*<p className="TextInputHint"></p>*/}
</div>
);
};
export default TextInput;
I hope it'll be useful
Changing the value of a local variable does not cause a re-render. And even if something else causes a render, that new render won't have access to local variables from the previous render. You need to use useState for the active style, not a local variable.
just use useState for activeStyle instead of declaring it on the scope of the component,this could cause unsync effect which could be a problem and indeterministic
import React, {useState} from 'react';
import './TextInput.css';
import * as CSS from 'csstype';
type TextInputProps = {
title: string
}
const TextInput: React.FC<TextInputProps> = ({title, children}) => {
const hiddenStyle: CSS.Properties = {
opacity: 0
};
const visibleStyle: CSS.Properties = {
opacity: 1
};
const [rawTextInput, setRawTextInput] = useState("")
//------Put something you want to change dynamically inside useState()
const [titleStyle, setTitleStyle] = useState(hiddenStyle)
//------
const InputEvent = (e: React.FormEvent<HTMLInputElement>) => {
const inputValue = e.currentTarget.value;
setRawTextInput(inputValue)
if(inputValue == ""){
setTitleStyle(hiddenStyle)
} else {
setTitleStyle(visibleStyle)
}
}
return (
<div className="TextInput">
<p
className="TextInputTitle"
style={titleStyle}
>
{title}
</p>
<input
className="TextInputField"
type="text"
placeholder={title}
value={rawTextInput}
onChange={InputEvent}
/>
{/*<p className="TextInputHint"></p>*/}
</div>
);
}
export default TextInput
I am trying to use react hooks to determine if a user has clicked outside an element. I am using useRef to get a reference to the element.
Can anyone see how to fix this. I am getting the following errors and following answers from here.
Property 'contains' does not exist on type 'RefObject'
This error above seems to be a typescript issue.
There is a code sandbox here with a different error.
In both cases it isn't working.
import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
const Menu = () => {
const wrapperRef = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(true);
// below is the same as componentDidMount and componentDidUnmount
useEffect(() => {
document.addEventListener('click', handleClickOutside, true);
return () => {
document.removeEventListener('click', handleClickOutside, true);
};
}, []);
const handleClickOutside = event => {
const domNode = ReactDOM.findDOMNode(wrapperRef);
// error is coming from below
if (!domNode || !domNode.contains(event.target)) {
setIsVisible(false);
}
}
return(
<div ref={wrapperRef}>
<p>Menu</p>
</div>
)
}
the useRef API should be used like this:
import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
function App() {
const wrapperRef = useRef(null);
const [isVisible, setIsVisible] = useState(true);
// below is the same as componentDidMount and componentDidUnmount
useEffect(() => {
document.addEventListener("click", handleClickOutside, false);
return () => {
document.removeEventListener("click", handleClickOutside, false);
};
}, []);
const handleClickOutside = event => {
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
setIsVisible(false);
}
};
return (
isVisible && (
<div className="menu" ref={wrapperRef}>
<p>Menu</p>
</div>
)
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I have created this common hook, which can be used for all divs which want this functionality.
import { useEffect } from 'react';
/**
*
* #param {*} ref - Ref of your parent div
* #param {*} callback - Callback which can be used to change your maintained state in your component
* #author Pranav Shinde 30-Nov-2021
*/
const useOutsideClick = (ref, callback) => {
useEffect(() => {
const handleClickOutside = (evt) => {
if (ref.current && !ref.current.contains(evt.target)) {
callback(); //Do what you want to handle in the callback
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
});
};
export default useOutsideClick;
Usage -
Import the hook in your component
Add a ref to your wrapper div and pass it to the hook
add a callback function to change your state(Hide the dropdown/modal)
import React, { useRef } from 'react';
import useOutsideClick from '../../../../hooks/useOutsideClick';
const ImpactDropDown = ({ setimpactDropDown }) => {
const impactRef = useRef();
useOutsideClick(impactRef, () => setimpactDropDown(false)); //Change my dropdown state to close when clicked outside
return (
<div ref={impactRef} className="wrapper">
{/* Your Dropdown or Modal */}
</div>
);
};
export default ImpactDropDown;
Check out this library from Andarist called use-onclickoutside.
import * as React from 'react'
import useOnClickOutside from 'use-onclickoutside'
export default function Modal({ close }) {
const ref = React.useRef(null)
useOnClickOutside(ref, close)
return <div ref={ref}>{'Modal content'}</div>
}
An alternative solution is to use a full-screen invisible box.
import React, { useState } from 'react';
const Menu = () => {
const [active, setActive] = useState(false);
return(
<div>
{/* The menu has z-index = 1, so it's always on top */}
<div className = 'Menu' onClick = {() => setActive(true)}
{active
? <p> Menu active </p>
: <p> Menu inactive </p>
}
</div>
{/* This is a full-screen box with z-index = 0 */}
{active
? <div className = 'Invisible' onClick = {() => setActive(false)}></div>
: null
}
</div>
);
}
And the CSS:
.Menu{
z-index: 1;
}
.Invisible{
height: 100vh;
left: 0;
position: fixed;
top: 0;
width: 100vw;
z-index: 0;
}