I have a card on my page, which has a title that is the name of a user. The names are sometimes longer than the card, the overflow previously was ellipsis, and a tooltip appeared on hover to show the full name. Now it should be changed in a way, that if it would overflow, then the texts letter-spacing attribute would decrease all the way down to -2, and after that will it start to truly overflow with an ellipsis. How to calculate the values from the available width of the enclosing div? Or how else should it be impemented.
Current state:
For names that can fit with letter-spacing : -2
For long names :
For short names:
Note, container size is the same across all cards!
Let's assume your card component is like this
function Card({ name }) {
return (
<div className="card">
<div className="card__pic"></div>
<h3 className="card__name" ref={nameRef}>
{name}
</h3>
</div>
);
}
All we need is the element containing the name i.e "card__name".
Access the 'name' element.
To access this name element, we have to use useRef hook. Here's how you would use it.
function Card({ name }) {
const nameRef = useRef(null);
useEffect(() => {
// nameRef.current is accessible here
}, []);
return (
<div className="card">
<div className="card__pic"></div>
<h3 className="card__name" ref={nameRef}>
{name}
</h3>
</div>
);
}
We have to pass ref={refVariable} to the element we'd like to access.
The refVariable will be initialized with the element node after mount i.e we can use it inside the useEffect hook.
Determine if the name is overflowing.
Now since we have the element, we can use it to check if the text inside is overflowing. I've created a function isTextOverflowingCard to which we pass the reference to the element we get after mount. It will return true if it text is overflowing else false.
function isTextOverflowingCard(textElement) {
if (textElement.scrollWidth > textElement.clientWidth) {
return true;
}
return false;
}
Now inside Card we can use this function and based on its return value, add a class conditionally which will have the tight letter-spacing value.
function Card({ name }) {
const nameRef = useRef(null);
const [textOverflow, setTextOverflow] = useState(false);
useEffect(() => {
// nameRef.current is accessible here
setTextOverflow(isTextOverflowingCard(nameRef.current));
}, []);
return (
<div className="card">
<div className="card__pic"></div>
<h3 className={`card__name ${textOverflow ? 'card__name--tight-kerning' : ''}`} ref={nameRef}>
{name}
</h3>
</div>
);
}
In CSS,
.card__name--tight-kerning {
letter-spacing: -2px;
}
You can change useEffect to useLayoutEffect, which is better suited for such dom related calculations and applying to ui.
References:
Detect if text has overflown
useEffect vs useLayoutEffect
https://reactjs.org/docs/hooks-reference.html#useref
Related
I'm trying to make an accordion, that opens up upon clicking the "+" icon. But when I click on a single element, all the other elements expand. Is there a way to just open one of the accordions? I've added the screenshot of the accordion and also added the code.
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import data from "../data";
export default function Container() {
const [display, setDisplay] = useState(false);
const clickHandle = () => {
setDisplay(!display);
};
return (
<>
<div className="fContainer container">
<div className="headingContainer">
<h2 className="mainHeading">Questions And Answers About Login</h2>
</div>
<div className="fQuesContainer container">
{data.map((question) => {
const { id, title, info } = question;
return (
<div className="qCard" key={id}>
<div className="qCardTitle">
<h4 className="qTitle">{title}</h4>
<button className="btnContainer" onClick={clickHandle}>
{display === false ? (
<AiFillPlusCircle size="2.4em" />
) : (
<AiFillMinusCircle size="2.4em" />
)}
</button>
</div>
{/* This thing would return the info, or the element */}
{display === false ? (
<></>
) : (
<>
<hr className="fHr" />
<p className="fInfo">{info}</p>
</>
)}
</div>
);
})}
</div>
</div>
</>
);
}
Explanation
Basically you can't use a single variable to toggle all your elements. All the elements will act as a single element, so either all will open or all will close.
You need something that can be checked against each element. Now id is a potential candidate but it has it's drawbacks, since it's a list the best option is using the index of the element itself.
So you first change the display type from boolean (false) to integer type and default it to -1 (anything less than zero)
Then change your .map function from .map((question) =>... to .map((question, questionIndex) =>..., this will get you a variable questionIndex which holds the current question's index
You can use that (questionIndex) and the display variable to check against each other and display the appropriate states.
Benefits when compared to other answers
Since you are dealing with a list of items, it is always best to use the index of an element to toggle the element's display, This ensures you have decoupled your View from your Data. (As much as possible)
If for some reason your id is null or duplicate, it will create issues in your display.
It is easier to just call toggleElement(2) to automatically open an element for a given position via code (on first load). This is useful if you want to maintain the open states between url changes / reloads, you just add the index to the query parameter of the url.
Solution
import { AiFillPlusCircle, AiFillMinusCircle } from "react-icons/ai";
import data from "../data";
export default function Container() {
// Update the display to be of type integer and init it with -1
const [display, setDisplay] = useState(-1);
// Add a parameter to the click function to take the clicked element's index
const toggleElement = (currentIndex) => {
// Check if the element that is clicked is already open
if(currentIndex===display) {
setDisplay(-1); // If it is already open, close it.
}
else {
setDisplay(currentIndex); // else open the clicked element
}
};
return (
<>
<div className="fContainer container">
<div className="headingContainer">
<h2 className="mainHeading">Questions And Answers About Login</h2>
</div>
<div className="fQuesContainer container">
{/* Add a variable questionIndex to the map method to get the index of the current question */}
{data.map((question, questionIndex) => {
const { id, title, info } = question;
return (
<div className="qCard" key={id}>
<div className="qCardTitle">
<h4 className="qTitle">{title}</h4>
{/* Update the button onClick event to pass the current element's index via the questionIndex variable */}
<button className="btnContainer" onClick={()=> toggleElement(questionIndex)}>
{/* Update the UI state based on the comparison of the display and questionIndex variable (Note, if they match, you need to open the element, else close) */}
{display == questionIndex ? (
<AiFillMinusCircle size="2.4em" />
) : (
<AiFillPlusCircle size="2.4em" />
)}
</button>
</div>
{/* This thing would return the info, or the element */}
{/* Update the UI state based on the comparison of the display and questionIndex variable (Note, if they match, you need to open the element, else close) */}
{display == questionIndex ? (
<>
<hr className="fHr" />
<p className="fInfo">{info}</p>
</>
) : (
<></>
)}
</div>
);
})}
</div>
</div>
</>
);
}
Instead of a boolean, use an id in your state
const [display, setDisplay] = useState("");
When you map an item, add this function
const { id, title, info } = question;
const handleSetDisplay = () => {
if(id === display) {
//Close panel
setDisplay("")
} else {
//open specific panel
setDisplay(id)
}
}
Adjust the button's onClick
<button className="btnContainer" onClick={handleSetDisplay}>
Then to compare if your panel should expand, use
{display === id ? ///...: ...}
In short, you need to compare the saved ID with the mapped item's id.
If your id is a number, just change the initial state to 0
I'm new with React and Next.js, I'm creating a timeline, the timeline has colored bars which if pressed, the page shows a div below with info about each bar.
I managed to make the content below appear and disappear with useState hook so the content of each bar doesn't stack, I'm using an animated tag "Section" and only the first time I press any bar, the content is animated, the rest appears statically, I'm wondering if I can use something like the useEffect hook to maybe re-render each content div so the animation appears every time you click each bar, also to erase the last loaded div so doesn't stack on browser memory, I hope I explained myself I started with React 2 days ago, and thank you for your time.
Example reduced code:
//useState hook
const [content, setContent] = useState(null);
//Timeline section
<div>
<Bar1 onClick={() => setContent(BarContent_1)}/>
<Bar2 onClick={() => setContent(BarContent_2)}/>
</div>
//Content display
<div>
{content}
</div>
//Content of each bar, (<Section> div has the animation)
const BarContent_1 = () => {
return (
<Section>
Content of bar 1
</Section>
)
}
const BarContent_2 = () => {
return (
<Section>
Content of bar 2
</Section>
)
}
you can use useState for that to toggle classList on element here is example below what I do, in your project I don't know what you do but I will come up with card example first as your default value set state to false in card component and when you will click the button toggle that boolean false to true
like that () => setState(!state) it will always set the value of opposite state
when you will change state component always re-renders and checks the state, and you will make like that on more info div, if that state is true show more-info active that means your div will be displayed
and if it is false it will be dissapeared
const Card1 = () => {
const [showMore, setShowMore] = useState(false)
return (
<div className="card">
<div>Daniel</div>
<button onClick={() => setShowMore(!showMore)}>Show More</button>
<div class={showMore ? "more-info active": "more-info"}>This is more info Daniel</div>
</div>
)
}
also here is my css what I do
.more-info{
opacity: 0;
transition: 0.5s ease;
}
.active{
opacity: 1
}
in start thats good to make stuff like that but I would recommend you to use array objects for that to .map() components and make toggling animations on that, good luck
also quickly I made working example on sandbox
#callmenikk gave me an idea to use a conditional to render my styled div every time the condition met.
Solution:
//useState hook
const [content, setContent] = useState(null);
//Conditional function 'Refresh'
const Refresh = ({children}) => {
const active = content != setContent
if (active) {
return (
<Section>
{children}
</Section>
)
}
}
//Timeline section
<div>
<Bar1 onClick={() => setContent(BarContent_1)}/>
<Bar2 onClick={() => setContent(BarContent_2)}/>
</div>
//Content display and wrapping the content in my Refresh function
<Refresh>
{content}
</Refresh>
//Content of each bar, no need to wrap content in my styled (Section) tag since my Refresh button will
const BarContent_1 = () => {
return (
<div>
Content of bar 1
</div>
)
}
const BarContent_2 = () => {
return (
<div>
Content of bar 2
</div>
)
}
In normal JavaScript you can grab an element by its id and add a style to it.
For example:
var element = document.getElementById("myElement");
element.style.backgroundColor = "#f5f5f5";
My question is how you can do this in react. Is it even possible to add this style?
In react im using onChange in a function outside the render(). I looked at the React DOM for styling and tried but since styling is in different function it will tell me how the variable is undefined.
this is my code:
ChangeImage() {
var imgStyles = {
backgroundColor: '#000',
padding: 5,
}
}
render() {
return (
<div className="class">
<div className="img-surround">
<img
src={this.state.file}
id="img"
style={imgStyles}/>
</div>
Everything is working except styles and I even tried putting in different functions
If you want to render the element with the style you can return the element like this in a react functional component:
return <div style={{backgroundColor: "#f5f5f5"}}></div>
If you want the element to only have that style in a certain condition you can use the useState hook in a react functional component:
const [myState, setMyState] = useState(false);
return <div style={myState && {backgroundColor: "f5f5f5"}}></div>
And you should change myState's value using setMyState however you like. For example:
const [myState, setMyState] = useState(false);
return <div onClick={() => myState ? setMyState(true) : setMyState(false)} style={myState && {backgroundColor: "f5f5f5"}}></div>
In this example whenever you click on the div the style is added or removed by case
I'm trying to change the text of a <p> to David and Larry accordingly when each button (that has an image inside) has hovered. I have experimented with numerous things and found a way to change the CSS of the button with a function. But I was unable to find anything to change text since <p> is in a different class. Any suggestions to address this problem?
For your information, I have added a CSS color changing function I used earlier to the below code sample.
here's my code.
import React from 'react';
import "./Tri.css";
function Tri() {
function davidon(e) {
e.target.style.background = 'red';
}
function davidoff(e) {
e.target.style.background = 'green';
}
function larryon(e) {
e.target.style.background = 'red';
}
function larryoff(e) {
e.target.style.background = 'green';
}
return (
<div>
<div>
<div>
<button onMouseOver={davidon} onMouseLeave={davidoff}>
<img src={require(`./images/david.png`)} className="david"/>
</button>
<button onMouseOver={larryon} onMouseLeave={larryoff}>
<img src={require(`./images/larry.png`)} className="larry"/>
</button>
</div>
<div className="plex">
<p>Larry Or David?</p>
</div>
</div>
</div>
);
}
export default Tri;
Thanks in advance for you replies.
You need to think more in "React", and use component state and props. The offical documentation is a good place to start.
Here I've got two components.
1) Tri: which has it's own state, and builds the HTML using Button components
2) Button: since you need each button to change color depending on the mouse action it's best to separate that functionality out into a new component so that each instance can have its own state.
(I've intentionally left out the images in this example, but you could pass in a src prop to the button and have that handle the images too if you wanted.)
const { useState } = React;
// `Button` accepts a props object
// Here I've destructured out the button name,
// and the handleHover function
function Button({ name, handleHover }) {
// We initialise the state with "green"
const [ color, setColor ] = useState('green');
function handleColor() {
// We set the new color based on the current color
setColor(color => color === 'red' ? 'green' : 'red');
// And then call the `handleHover` function, passing in `name`
handleHover(name);
}
return (
<button
className={color}
onMouseOver={handleColor}
onMouseLeave={handleColor}
>
{name}
</button>
);
}
function Tri() {
// In `Tri` we set its own state for the name
// initialised to an empty string
const [ name, setName ] = useState('');
// A handler that changes the name
// This is the function we pass to each button
function handleHover(name) {
setName(name);
}
// Set up two buttons using our Button component
// assigning a name to each, and passing in our handler
// Whenever the name (state) is changed the name in the
// paragraph also changes
return (
<div>
<div>
<Button name="Larry" handleHover={handleHover} />
<Button name="David" handleHover={handleHover} />
</div>
<p>{name}</p>
</div>
);
}
ReactDOM.render(
<Tri />,
document.getElementById('react')
);
.red { background-color: red; }
.green { background-color: green; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Try using states. And don't change DOM-nodes dynamically in event handlers. Always use React functionality:
React uses a declarative form of programming (The Virtual DOM specifically). You define variables and set them and React updates the DOM if those change.
useState gives you the opportunity to declare an editable (through a setter function) variable. See Docs on State and Props.
import React from 'react';
import "./Tri.css";
function Tri(props) {
// props as immutable arguments (if needed)
// useState keeps an internal state in the component
let initialTxt = 'Larry Or David?';
const [text, setText] = React.useState(initialTxt);
return (
<div>
<div>
<div>
<button
className="david-btn"
onMouseOver={() => setText('David')}
onMouseLeave={() => setText(initialTxt)}>
<img src={require(`./images/david.png`)} className="david"/>
</button>
<button
className="larry-btn"
onMouseOver={() => setText('Larry')}
onMouseLeave={() => setText(initialTxt)}>>
<img src={require(`./images/larry.png`)} className="larry"/>
</button>
</div>
<div className="plex">
<p>{text}</p>
</div>
</div>
</div>
);
}
Also, extend ./Tri.css with the following code. You could use a style-variable but that would make your code more bloated and unreadable if you have access to CSS.
.david-btn,
.larry-btn {
background-color: green;
}
.david-btn:hover,
.larry-btn:hover {
background-color: red;
}
You are looking for Refs. You can read more about them in documentation.
I've created a simple example (based on your code).
Step by step what I did:
import useRef hook which is used to create reference.
import React, { useRef } from "react";
created reference:
const pTagRef = useRef();
passed reference to your p tag
<div ref={pTagRef} className="plex">
<p>Larry Or David?</p>
</div>
created function which can change the content of this reference where pTagRef.current is DOM element.
function setName(name) {
if (pTagRef.current) {
pTagRef.current.innerText = name;
}
}
called the function whenever name changed
setName("larry");
You should definitely use state for this but I hope this one helps you to get started.
I'm trying to get the height of the parent container element, to use as a height of an SVG background element.
Is there a way to get and pass this property?
I keep getting "TypeError: Cannot read property 'refs' of undefined"
const Services = ({ classes }) => {
return (
<div className={classes.Container} ref="container">//Element I want the height of
<div className={classes.BGContainer}>
<BGSVG width={'210'} height={this.refs.container.height} color={'blue'} />//Where I want to pass height to
</div>
<div className={classes.Services}>
{services.map((e, i) => (
<ServiceItem
title={e.title}
icon={e.icon}
info={e.info}
inverted={i % 2 === 1 ? true : false}
/>
))}
</div>
</div>
);
In a functional component, you need to use the React Hook called useRef like this:
const TextInputWithFocusButton = (props) => {
const inputEl = useRef(null);
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
you will see if you log out inputEl that a reference to the input element is logged. DOM element references allow you to access most properties of the particular DOM element referenced.
You haven't bound "this". Try height={() => this.refs.container.height}
If your Services component is inside lets say ServicesWrapper Component, what you should do is put on it
`servicesWrapperContainerRef = React.createRef()`;
Then, on JSX of wrapper component place
`ref={this.servicesWrapperContainerRef}`
and then, pass that as a prop to the Services component like this
const Services = ({ classes, servicesWrapperContainerRef }) and then you can use it to find out the parents height
`height={this.props.servicesWrapperContainerRef.current.clientHeight}`