I made 5 blocks and want to make the letters on each block thick when the mouse is hover. I made isHover state and changed the thickness of the writing according to the state, but the problem is that the thickness of all five changes. I think I can solve it by using conditional rendering, but I don't know how to use it. Of course, it can be implemented only with css, but I want to implement it with conditional rendering because I am practicing the code concisely.
import "./styles.css";
import styled from "styled-components";
import { useState } from "react";
export default function App() {
const array = [
{ id: "1", title: "ABC" },
{ id: "2", title: "DEF" },
{ id: "3", title: "GHI" },
{ id: "4", title: "JKL" },
{ id: "5", title: "MNO" }
];
const [isHover, setIsHover] = useState(false);
return (
<Head isHover={isHover}>
<div className="header">
{array.map((content, id) => {
return (
<div
className="header__title"
onMouseEnter={() => {
setIsHover(true);
}}
onMouseLeave={() => {
setIsHover(false);
}}
>
{content.title}
</div>
);
})}
</div>
</Head>
);
}
const Head = styled.div`
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
.header {
display: inline-flex;
border: 1px solid black;
box-sizing: border-box;
}
.header__title {
border: 1px solid red;
padding: 5px 10px;
font-weight: ${(props) => (props.isHover ? "700" : "400")};
}
`;
codesandbox
https://codesandbox.io/s/aged-cherry-53pr2r?file=/src/App.js:0-1170
The problem is that you are using the same state for all the 5 blocks. There are multiple approaches you could take to solve this problem.
1. Multiple states
You could create 5 different isHover<N> states (maybe a single one, but as an array)
2. Component extraction
You could just extract out a component for each entry in array and do state management in that component.
function App() {
const array = [...];
return (
<Head>
<div className="header">
{array.map((content, id) => (
<HeaderTitle key={content.id} content={content} />
)}
</div>
</Head>
);
}
function HeaderTitle({ content }) {
const [isHover, setIsHover] = useState(false);
return (
<StyledHeaderTitle
isHover={isHover}
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
>
{content.title}
</StyledHeaderTitle>
);
}
const StyledHeaderTitle = styled.div`
font-weight: ${(props) => (props.isHover ? "700" : "400")};
`
3. Using style prop
Directly apply the font weight using the style prop (An extension to approach 2)
function HeaderTitle({ content }) {
const [isHover, setIsHover] = useState(false);
return (
<StyledHeaderTitle
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
style={{ fontWeight: isHover ? "700" : "400" }}
>
{content.title}
</StyledHeaderTitle>
);
}
4. CSS
CSS already allows you to track hover states over different elements and you don't need to manually track it in javascript.
.header__title {
border: 1px solid red;
padding: 5px 10px;
font-weight: 400;
&:hover {
font-weight: 700;
}
}
There's no need to use React state and event listeners here, you can do it all in CSS instead:
.header__title {
border: 1px solid red;
padding: 5px 10px;
font-weight: 400;
}
.header__title:hover {
font-weight: 700;
}
Just add this pseudo class and you're good to go
.header__title:hover {
font-weight: 700;
}
Related
I am trying to extent react component in styled-component and trying to add custom style on extended component but unable to see the style changes that I am applying
I have created a button component in /src/newbutton.js with following code
import styled from "styled-components";
const Button = styled.button`
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
export const NewButton = ({ className, children }) => {
return (
<Button primary>Primary</Button>
)
}
And extending and creating another button component with custom style in /src/custom-button.js with following code
import styled from "styled-components";
import { NewButton } from './button'
const ButtonWrapper = styled(NewButton)`
width: 100%;
color: red
`;
const ExtendedButton = ({ className, children }) => {
return (
<ButtonWrapper />
)
}
I have added the custom style like width: 100% & color: red but it is not applying on ExtendedButton. Infect colour and width is same as NewButton
You need to pass a className to your NewButton in order to customize it, using styled-components.
Styled components works by creating a unique className that associated with a component and its CSS.
export const NewButton = ({ className, children }) => {
return (
<Button className={className} primary>Primary</Button>
)
}
I am posting the complete working code for future reference based on #Flat Globe solution. And it is working fine as expected.
I have modified the Button component code just by adding className in /src/newbutton.js with following code
import styled from "styled-components";
const Button = styled.button`
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
export const NewButton = ({ className, children }) => {
return (
<Button primary className={className}>Primary</Button>
)
}
I have also modified the extended-button code by just passing the className in /src/custom-button.js. check the full code below
import styled from "styled-components";
import { NewButton } from './button'
const ButtonWrapper = styled(NewButton)`
width: 100%;
color: red
`;
const ExtendedButton = ({ className, children }) => {
return (
<ButtonWrapper className="extended-button"/>
)
}
I'm very new to React so any advice would be appreciated on how to move an agent thumbnail to the teamComp div when it is clicked.
I'm also lost as to how to tackle filtering the data through a dropdown menu. Like how would I update the page without refreshing so that only the agents with the selected roles appear.
Anything would help, like I said before, I am a complete beginner to React and feel like I am underutilizing a lot of what makes React powerful.
App.js
import { useEffect, useMemo, useState } from "react";
import AgentCard from "./components/agentCard";
import Select from "react-select"
function App() {
const options = useMemo(
() => [
{value: "controller", label: "Controller"},
{value: "duelist", label: "Duelist"},
{value: "initiator", label: "Initiator"},
{value: "sentinel", label: "Sentinel"},
],
[]
);
const [agentDetails, setAgentDetails] = useState([]);
const getAllAgents = async () => {
const res = await fetch("https://valorant-api.com/v1/agents/");
const results = await res.json();
const agentNames = [],
agentImages = [],
agentRoles = [],
agentDetails = [];
for (let i = 0; i < Object.keys(results["data"]).length; i++) {
if (results["data"][i]["developerName"] != "Hunter_NPE") {
agentNames.push(results["data"][i]["displayName"]);
agentImages.push(results["data"][i]["displayIcon"]);
agentRoles.push(results["data"][i]["role"]["displayName"]);
}
else {
continue;
}
}
for (let i = 0; i < agentNames.length; i++) {
agentDetails[i] = [agentNames[i], [agentImages[i], agentRoles[i]]];
}
agentDetails.sort();
setAgentDetails(agentDetails);
};
useEffect(() => {
getAllAgents();
}, []);
return (
<div className="app-container">
<h2>Valorant Team Builder</h2>
<div className="teamComp">
</div>
<Select options={options} defaultValue={options} isMulti/>
<div id="agent_container" className="agent-container">
{agentDetails.map((agentDetails) => (
<AgentCard
img={agentDetails[1][0]}
name={agentDetails[0]}
role={agentDetails[1][1]}
/>
))}
</div>
</div>
);
}
export default App;
agentCard.js
import React from 'react'
const agentCard = ({role, name, img}) => {
return (
<div className="card-container">
<div className="img-container">
<img src={img} alt={name} />
</div>
<div className="info">
<h3 className="name">{name}</h3>
<small className="role"><span>Role: {role}</span></small>
</div>
</div>
)
}
export default agentCard
index.css
#import url('https://fonts.googleapis.com/css?family=Muli&display=swap');
#import url('https://fonts.googleapis.com/css?family=Lato:300,400&display=swap');
* {
box-sizing: border-box;
}
body {
background: #EFEFBB;
background: -webkit-linear-gradient(to right, #D4D3DD, #EFEFBB);
background: linear-gradient(to right, #D4D3DD, #EFEFBB);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-family: 'Lato';
margin: 0;
}
h1 {
letter-spacing: 3px;
}
.agent-container {
display: flex;
flex-wrap: wrap;
align-items: space-between;
justify-content: center;
margin: 0 auto;
max-width: 1200px;
}
.app-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 3rem 0.5rem;
}
.card-container {
background-color: #eee;
border-radius: 20px;
box-shadow: 0 3px 15px rgba(100, 100, 100, 0.5);
margin: 10px;
padding: 20px;
text-align: center;
}
.card-container:hover {
filter: brightness(70%);
transition: all 150ms ease;
}
.img-container img {
margin-top: 1.5rem;
height: 128px;
width: 128px;
}
.name {
margin-bottom: 0.2rem;
}
.teamComp h3 {
float: left;
}
Moving cards
To move a card to a different list you need a new state array that will represent "the members of the team". Something like:
const [team, setTeam] = useState([]);
Render the items in team inside the "teamComp" <div>, the same way you do it in the agent container.
Then add the new function prop to the card and use it in the onClick handler in the card <div>:
<AgentCard
key={agentDetails[0]}
img={agentDetails[1][0]}
name={agentDetails[0]}
role={agentDetails[1][1]}
handleClick={moveToTeam}
/>
...
<div className="card-container" onClick={() => handleClick(name)}>
and in this function, add the agentDetails item to the team state and remove it from the agentDetails state. Make sure that you supply new arrays when setting state:
const moveToTeam = (name) => {
const newTeam = [...team, agentDetails.find((agent) => agent[0] === name)];
const newAgentDetails = agentDetails.filter((agent) => agent[0] !== name);
setTeam(newTeam);
setAgentDetails(newAgentDetails);
};
Filtering
For filtering you need another state that contains all selected options:
const [options, setOptions] = useState(allOptions);
where allOptions is an array of all available options, and it should not change.
Add the onChange handler to the <Select> component:
<Select
options={allOptions}
onChange={(selectedOptions) => setOptions(selectedOptions)}
defaultValue={allOptions}
isMulti
/>
and finally use options to filter cards:
<div id="agent_container" className="agent-container">
{agentDetails
.filter(
(agentDetails) =>
options.filter((option) => option.label === agentDetails[1][1])
.length > 0
)
.map((agentDetails) => (
<AgentCard
key={agentDetails[0]}
img={agentDetails[1][0]}
name={agentDetails[0]}
role={agentDetails[1][1]}
handleClick={moveToTeam}
/>
))}
</div>
You can see the complete example on codesandbox.
I left most of the names in place, although I think using agentDetails for different things is confusing. The data structures can also be improved, but I left them unchanged as well.
In my Class component Field.jsx render(), I'm expanding my <Position> component using <Flipper>, (an abstracted flip animation), like so:
import { Flipper, Flipped } from 'react-flip-toolkit'
import { Position } from "./Position";
import "./css/Position.css";
class Field extends Component {
constructor(props) {
super(props);
this.state = {
fullScreen: false,
};
}
toggleFullScreen() {
this.setState({ fullScreen: !this.state.fullScreen });
}
...
render() {
const { players } = this.props;
const { fullScreen } = this.state;
if(players){
return (
<div className="back">
<div className="field-wrapper" >
<Output output={this.props.strategy} />
<Flipper flipKey={fullScreen}>
<Flipped flipId="player">
<div className="field-row">
{this.getPlayersByPosition(players, 5).map((player,i) => (
<Position
key={i}
className={fullScreen ? "full-screen-player" : "player"}
getPositionData={this.getPositionData}
toggleFullScreen={this.toggleFullScreen.bind(this)}
>{player.name}</Position>
))}
</div>
</Flipped>
</Flipper>
</div>
</div>
);
}else{
return null}
}
When I render it, I get clickable items from the mapped function getPlayersByPosition(), like so:
And if I click on each item, it expands to a div with player name:
Which is passed as props.children at component <div>
Position.jsx
import React from "react";
import "./css/Position.css";
export const Position = props => (
<div
className={props.className}
onClick={() => {
props.getPositionData(props.children);
props.toggleFullScreen();
console.log(props.getPositionData(props.children))
}}
>
{props.children}
</div>
);
getPositionData(), however, returns an object with many items on its turn, as seen by console above:
{matches: 7, mean: 6.15, price: 9.46, value: 0.67, G: 3, …}
QUESTION:
How do I pass and print theses other props keys and values on the expanded purple div as text?, so as to end with:
Patrick de Paula
matches: 7
mean: 6.15
price:9.46
....
NOTE:
Position.css
.position-wrapper {
height: 4em;
display: flex;
justify-content: center;
align-items: center;
font-weight: lighter;
font-size: 1.4em;
color: #888888;
flex: 1;
/*outline: 1px solid #888888;*/
}
.player {
height: 4em;
width: 4em;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
font-weight: lighter;
font-size: 1.4em;
/*background-color: #66CD00;*/
color: #ffffff;
}
.full-screen-player {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
background-image: linear-gradient(
45deg,
rgb(121, 113, 234),
rgb(97, 71, 182)
);
}
Looks like the props are all set & ready to be print as seen on your console. You can access them via props.getPositionData(props.children).property_name_here or destructure them
export const Position = props => {
const { matches, mean, price } = props.getPositionData(props.children);
return (
<div
className={props.className}
onClick={() => {
props.getPositionData(props.children);
props.toggleFullScreen();
console.log(props.getPositionData(props.children))
}}
>
<p>Name: {props.children}</p>
<p>Matches: {matches}</p>
<p>Mean: {mean}</p>
<p>Price: {price}</p>
</div>
)
}
Regarding the issue on the fullScreen prop (see comments section):
Is there a way to print them ONLY after toggleFullScreen()
Since you already have a state on the Field component which holds your fullScreen value, on your Field component, you need to pass the fullScreen prop as well to the Position component. e.g., fullScreen={this.state.fullScreen}. Back on Position component, have some condition statements when you are rendering.
Example:
<>
{props.fullScreen &&
<p>Name: {props.children}</p>
}
</>
I've been trying to add a onClick event to the divs which will resize a div to fullscreen when clicking on that div but when I click on a div, all the div are getting expanded. How do I restrict that onClick event to only a single a div and make that single div resize to full screen? I've also added transition so that when it resize to fullscreen, it looks like a animation but all the divs have been affected by it when clicking on just a single div
import React from "react";
import "./styles.scss";
const colors = [
"palevioletred",
"red",
"green",
"blue",
"yellow",
"orange",
"lightblue"
];
const randomColor = items => {
return items[randomHeight(0, items.length)];
};
const randomHeight = (min, max) => {
return Math.floor(min + Math.random() * (max - min + 1));
};
export default class App extends React.Component {
constructor(props) {
super(props);
this.addActiveClass = this.addActiveClass.bind(this);
this.state = {
active: false
};
}
addActiveClass() {
const currentState = this.state.active;
this.setState({ active: !currentState });
}
render() {
return (
<div class="container">
{Array.from({ length: 30 }).map((item, index) => (
<div
key={index}
style={{
background: randomColor(colors),
height: randomHeight(100, 200)
}}
className={this.state.active ? "full" : null}
onClick={this.addActiveClass}
/>
))}
</div>
);
}
}
* {
box-sizing: border-box;
}
body {
margin: 40px;
background-color: #fff;
color: #444;
font: 2em Sansita, sans-serif;
}
.container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
max-height: 100vh;
}
.container > * {
border: 2px solid orange;
border-radius: 5px;
margin: 10px;
padding: 10px 20px;
background-color: red;
color: #fff;
}
.full{
width: 100%;
height: 100%;
transition: 2s;
}
codesandbox
Currently you're sharing one state with all of the divs. In order to resolve this problem, create activeIndex state, initialize it with -1 maybe, and use it like:
// ...
class App extends React.Component {
constructor(props) {
super(props);
this.addActiveClass = this.addActiveClass.bind(this);
this.state = {
activeIndex: -1
};
}
addActiveClass(activeIndex) {
this.setState(prev => ({
activeIndex: prev.activeIndex === activeIndex ? -1 : activeIndex
}));
}
render() {
return (
<div className="container">
{Array.from({ length: 30 }).map((item, index) => {
return (
<div
key={index}
style={{
background: randomColor(colors),
height: randomHeight(100, 200)
}}
className={this.state.activeIndex === index ? "full" : ""}
onClick={() => this.addActiveClass(index)}
/>
);
})}
</div>
);
}
}
I created a Dropdown that when I click outside of it the dropdown disappears. I used a click event listener to determine if I clicked outside the dropdown.
After a few clicks, the page slows down and crashes. Perhaps the state is being rendered in a loop or too many events are being fired at once?
How do I fix this?
Also, is there a more React way to determine if I clicked outside an element? (Instead of using a document.body event listener)
Here is the codepen:
const items = [
{
value: 'User1'
},
{
value: 'User2'
},
{
value: 'User3'
},
{
value: 'User4'
},
{
value: 'User5'
}
];
class Dropdown extends React.Component {
state = {
isActive: false,
}
render() {
const { isActive } = this.state;
document.addEventListener('click', (evt) => {
if (evt.target.closest('#dropdownContent')) {
//console.warn('clicked inside target do nothing');
return;
}
if (evt.target.closest('#dropdownHeader')) {
//console.warn('clicked the header toggle');
this.setState({isActive: !isActive});
}
//console.warn('clicked outside target');
if (isActive) {
this.setState({isActive: false});
}
});
return (
<div id="container">
<div id="dropdownHeader">select option</div>
{isActive && (
<div id="dropdownContent">
{items.map((item) => (
<div id="item" key={item.value}>
{item.value}
</div>
))}
</div>
)}
</div>
);
};
}
ReactDOM.render(
<Dropdown items={items} />,
document.getElementById('root')
);
#container {
position: relative;
height: 250px;
border: 1px solid black;
}
#dropdownHeader {
width: 100%;
max-width: 12em;
padding: 0.2em 0 0.2em 0.2em;
margin: 1em;
cursor: pointer;
box-shadow: 0 1px 4px 3px rgba(34, 36, 38, 0.15);
}
#dropdownContent {
display: flex;
flex-direction: column;
position: absolute;
top: 3em;
width: 100%;
max-width: 12em;
margin-left: 1em;
box-shadow: 0 1px 4px 0 rgba(34, 36, 38, 0.15);
padding: 0.2em;
}
#item {
font-size: 12px;
font-weight: 500;
padding: 0.75em 1em 0.75em 2em;
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root">
<!-- This element's contents will be replaced with your component. -->
</div>
There's a pretty simple explanation for what you're experiencing. :)
The way I was able to figure it out was the number of warnings that were showing up in the terminal every time I clicked somewhere was getting higher and higher, especially when the state changed.
The answer though is that since you were adding the event listener code in the render function, every time the code re-rendered it would add more and more event listeners slowing down your code.
Basically the solution is that you should move the adding of event listeners to componentDidMount so it's only run once.
Updated working javascript:
const items = [
{
value: 'User1'
},
{
value: 'User2'
},
{
value: 'User3'
},
{
value: 'User4'
},
{
value: 'User5'
}
];
class Dropdown extends React.Component {
state = {
isActive: false,
}
// added component did mount here
componentDidMount(){
const { isActive } = this.state;
document.addEventListener('click', (evt) => {
if (evt.target.closest('#dropdownContent')) {
console.warn('clicked inside target do nothing');
return;
}
if (evt.target.closest('#dropdownHeader')) {
console.warn('clicked the header toggle');
this.setState({isActive: !isActive});
}
console.warn('clicked outside target');
if (isActive) {
this.setState({isActive: false});
}
});
}
render() {
const { isActive } = this.state;
//removed event listener here
return (
<div id="container">
<div id="dropdownHeader">select option</div>
{isActive && (
<div id="dropdownContent">
{items.map((item) => (
<div id="item" key={item.value}>
{item.value}
</div>
))}
</div>
)}
</div>
);
};
}
ReactDOM.render(
<Dropdown items={items} />,
document.getElementById('root')
);