React Component behaving unexpectedly after implementing audio and search functions - javascript

I have trying to figure out why my component is not working as expected.
The below code creates a list of word cards and each card will play audio when clicked.
There is also a search function to filter out the cards.
However, I find that these two functions do not work together. After searching, sometimes, I could not get the audio working. The audio part works fine without any searching.
I am getting the error
Uncaught TypeError: myAudio.current is null
handlePlayAudio WordsList.js:45
So I am guessing when I type in the search, something is causing myAudio.current to become null. The trouble is, sometimes it works but sometimes it doen't!
Does anyone know what is going on and how to fix it?
WordsList.js
import React, { useState, useRef } from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
// import { Link } from "gatsby"
// import slugify from "slugify"
import styled from "styled-components"
const WordsList = ({ words = [] }) => {
const [searchField, setSearchField] = useState("")
const myAudio = useRef("")
const filteredWords = words.filter(word => {
return (
word.english.toLowerCase().includes(searchField) ||
word.japanese.toLowerCase().includes(searchField) ||
word.romaji.toLowerCase().includes(searchField)
)
})
const handleSearchChange = event => {
const searchField = event.target.value.toLowerCase()
setSearchField(searchField)
}
return (
<Wrapper>
<div className="search-container">
<input
className="search-box"
type="search"
placeholder="search english, japanese, or romaji"
onChange={handleSearchChange}
/>
</div>
<div className="wrapper">
{filteredWords.map(word => {
const { id, english, japanese, romaji, image, audio } = word
const pathToImage = getImage(image)
const audioUrl = audio.file.url
const handlePlayAudio = () => {
myAudio.current.src = `http:${audioUrl}`
myAudio.current.play()
}
return (
<div
className="card"
onClick={handlePlayAudio}
onKeyDown={handlePlayAudio}
key={id}
>
<audio ref={myAudio} src={`http:${audio.file.url}`} />
{console.log(audio)}
{/*<Link key={id} to={`/${slug}`}>*/}
<GatsbyImage image={pathToImage} className="img" alt={english} />
<p>
<b>{english}</b> | {japanese} | {romaji}
</p>
</div>
)
})}
</div>
</Wrapper>
)
}
const Wrapper = styled.section`
.wrapper {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
.card {
color: #333;
}
.card:hover {
opacity: 0.9;
cursor: pointer;
}
.img {
width: 100%;
}
p {
padding-top: 0.6rem;
font-size: 0.9rem;
}
.search-container {
margin: 0 0 1.6rem 0;
text-align: center;
}
input {
width: 100%;
padding: 0.3rem 0.5rem;
border: 1px solid #999;
border-radius: 0;
::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: #bbb;
opacity: 1; /* Firefox */
}
:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: #bbb;
}
::-ms-input-placeholder {
/* Microsoft Edge */
color: #bbb;
}
text-transform: none;
}
`
export default WordsList
Thanks,
Andy

I seem to have come up with an answer.
I moved the audio tag out from inside the map
onclick get the audio url out
Use that url value to update and play the audio tag
import React, { useState, useRef } from "react"
import { GatsbyImage, getImage } from "gatsby-plugin-image"
// import { Link } from "gatsby"
// import slugify from "slugify"
import styled from "styled-components"
const WordsList = ({ words = [] }) => {
const [searchField, setSearchField] = useState("")
const myAudio = useRef("")
const filteredWords = words.filter(word => {
return (
word.english.toLowerCase().includes(searchField) ||
word.japanese.toLowerCase().includes(searchField) ||
word.romaji.toLowerCase().includes(searchField)
)
})
const handleSearchChange = event => {
const searchField = event.target.value.toLowerCase()
setSearchField(searchField)
}
const handleAudio = url => {
myAudio.current.src = url
myAudio.current.play()
}
// const handleTest = test => console.log(test)
return (
<Wrapper>
<audio ref={myAudio} src={""} />
<div className="search-container">
<input
className="search-box"
type="search"
placeholder="search english, japanese, or romaji"
onChange={handleSearchChange}
/>
</div>
<div className="wrapper">
{filteredWords.map(word => {
const { id, english, japanese, romaji, image, audio } = word
const pathToImage = getImage(image)
const audioUrl = audio.file.url
return (
<div
className="card"
onClick={() => handleAudio(audio.file.url)}
key={id}
>
{/*<Link key={id} to={`/${slug}`}>*/}
<GatsbyImage image={pathToImage} className="img" alt={english} />
<p>
<b>{english}</b> | {japanese} | {romaji}
</p>
</div>
)
})}
</div>
</Wrapper>
)
}
const Wrapper = styled.section`
.wrapper {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
}
.card {
color: #333;
}
.card:hover {
opacity: 0.9;
cursor: pointer;
}
.img {
width: 100%;
}
p {
padding-top: 0.6rem;
font-size: 0.9rem;
}
.search-container {
margin: 0 0 1.6rem 0;
text-align: center;
}
input {
width: 100%;
padding: 0.3rem 0.5rem;
border: 1px solid #999;
border-radius: 0;
::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: #bbb;
opacity: 1; /* Firefox */
}
:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: #bbb;
}
::-ms-input-placeholder {
/* Microsoft Edge */
color: #bbb;
}
text-transform: none;
}
`
export default WordsList

Related

fetching from an api - state react context

I am working on an app that fetches current weather for a specified location.
I want to display current time but i have to use "slice()" method to get rid of the first part of the string that looks like this: "localtime: '2022-05-12 13:39' ".
The problem is with useEffect() and states in the context file.
import axios from "axios";
import { createContext, useEffect, useState } from "react";
const Context = createContext({
weather: {},
place: {},
});
export const ContextProvider = ({ children }) => {
const [weather, setWeather] = useState({});
const [place, setPlace] = useState({});
useEffect(() => {
const getWeatherData = async () => {
try {
const request = await axios.get(
"http://api.weatherstack.com/current"
);
const currentWeather = request.data.current;
const currentPlace = request.data.location;
setWeather(currentWeather);
setPlace(currentPlace);
} catch (err) {
console.log(err);
}
};
getWeatherData();
}, []);
console.log(weather);
console.log(place);
const context = {
weather,
place,
};
return <Context.Provider value={context}>{children}</Context.Provider>;
};
export default Context;
When i console.log my states (lines 33 & 34 in context file) i get this result:
result
So on the first render they're undefined but then useEffect runs and update them.
I assume that this is why my "splice()" method does not work properly.
import styled from "styled-components";
import IconWind from "../icons/icon-wind";
import IconWindDirection from "../icons/icon-wind-direction";
// import IconHumidity from "../icons/icon-humidity";
import { useContext } from "react";
import Context from "../../store/context";
const WeatherDisplay = () => {
const current = useContext(Context);
const currentDay = new Date(current.place.localtime).toLocaleString("en-us", {
weekday: "long",
});
const currentTime = current.place.localtime.slice(10);
return (
<Wrapper>
<div>
<PrimaryInfo>
<h1>{current.place.name},</h1>
<h2>{current.weather.temperature}°</h2>
<p>
{currentDay}, {currentTime}
</p>
</PrimaryInfo>
<AdditionalInfo>
<p>
<IconWind /> Wind Speed: {current.weather.wind_speed}
</p>
<p>
<IconWindDirection /> Wind Direction: {current.weather.wind_dir}
</p>
<p>Atmospheric pressure: {current.weather.pressure}</p>
<p>Humidity: {current.weather.humidity}</p>
</AdditionalInfo>
</div>
</Wrapper>
);
};
const Wrapper = styled.section`
width: 50%;
border-radius: 10px 0 0 10px;
& > div {
height: 100%;
padding-left: 4rem;
display: flex;
justify-content: center;
flex-direction: column;
}
`;
const PrimaryInfo = styled.section`
margin-bottom: 10rem;
h1 {
font-size: 5rem;
}
h2 {
font-size: 4rem;
margin: 1rem 0rem;
}
p {
font-size: 2.4rem;
}
`;
const AdditionalInfo = styled.div`
p {
margin: 1rem 0rem;
font-size: 1.2rem;
display: flex;
align-items: center;
}
`;
export default WeatherDisplay;
splice logs
I tried to check if they already have content but still didnt work:
import styled from "styled-components";
import IconWind from "../icons/icon-wind";
import IconWindDirection from "../icons/icon-wind-direction";
// import IconHumidity from "../icons/icon-humidity";
import { useContext } from "react";
import Context from "../../store/context";
const WeatherDisplay = () => {
const current = useContext(Context);
let currentDay;
let currentTime;
if (current.place.length > 0 && current.weather.length > 0) {
currentDay = new Date(current.place.localtime).toLocaleString("en-us", {
weekday: "long",
});
currentTime = current.place.localtime.slice(10);
}
console.log( currentDay);
console.log( currentTime);
return (
<Wrapper>
<div>
<PrimaryInfo>
<h1>{current.place.name},</h1>
<h2>{current.weather.temperature}°</h2>
<p>
{currentDay}, {currentTime}
</p>
</PrimaryInfo>
<AdditionalInfo>
<p>
<IconWind /> Wind Speed: {current.weather.wind_speed}
</p>
<p>
<IconWindDirection /> Wind Direction: {current.weather.wind_dir}
</p>
<p>Atmospheric pressure: {current.weather.pressure}</p>
<p>Humidity: {current.weather.humidity}</p>
</AdditionalInfo>
</div>
</Wrapper>
);
};
const Wrapper = styled.section`
width: 50%;
border-radius: 10px 0 0 10px;
& > div {
height: 100%;
padding-left: 4rem;
display: flex;
justify-content: center;
flex-direction: column;
}
`;
const PrimaryInfo = styled.section`
margin-bottom: 10rem;
h1 {
font-size: 5rem;
}
h2 {
font-size: 4rem;
margin: 1rem 0rem;
}
p {
font-size: 2.4rem;
}
`;
const AdditionalInfo = styled.div`
p {
margin: 1rem 0rem;
font-size: 1.2rem;
display: flex;
align-items: center;
}
`;
export default WeatherDisplay;
It just displays currentDay and currentTime as undefined.
What am I doing wrong?

Switch active buttons using useState and useEffect Hooks for Buttons in JavaScript/React?

I am trying to write a useState() Hook, and perhaps add useEffect() to solve active state on two buttons. It is Delivery buttons that needs the first button-delivery to be active using CSS change, and if clicked on second button, PickUp, it should change CSS UI to stay active.
And yes if it is anyhow possible i want to use Hooks.
Is there any possible way to have it done on this way?
const Header = props => {
const [isActive, setIsActive] = useState(false);
function changeButtons () {
setIsActive = (!isActive)
};
return (
<Fragment>
<header className={classes.header}>
<div className={classes.logo} onClick={reload}>
<div >
Foodzilla
</div>
</div>
<div className={classes.delivery}>
<div
className={isActive ? classes.deliveryAction : classes.deliveryChoice}
onChange={changeButtons}
>Delivery</div>
<div className={classes.or}>or</div>
<div
className={isActive ? classes.pickUpAction : classes.pickUpChoice}
onChange={changeButtons}
>Pick Up</div>
</div>
Okay, so I did a mockup of what you are trying to do (I think :D). Here is a link to the working solution: https://codesandbox.io/s/quiet-mountain-68e10k?file=/src/styles.css:59-775.
The code is also below. There is definitely some refactoring that can be done, but I wanted to at least get you started on the right path.
Quick Summary:
Blue is Cicked (takes precedence over hover and default when active).
Green is Hovered (goes back to default when outside the div).
Red is Default (if not clicked or hovered, show red).
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const [isDeliveryClicked, setIsDeliveryClicked] = useState(false);
const [isPickupClicked, setIsPickupClicked] = useState(false);
const [isDeliveryHovered, setIsDeliveryHovered] = useState(false);
const [isPickupHovered, setIsPickupHovered] = useState(false);
const handleClick = (e) => {
if (e.target.className.includes("delivery")) {
setIsDeliveryClicked(true);
setIsDeliveryHovered(false);
if (isDeliveryClicked === true) {
setIsDeliveryClicked(false);
setIsDeliveryHovered(true)
}
} else if (e.target.className.includes("pickup")) {
setIsPickupClicked(true);
setIsPickupHovered(false);
if (isPickupClicked === true) {
setIsPickupClicked(false);
setIsPickupHovered(true)
}
}
};
const handleOnMouseOver = (e) => {
if (e.target.className.includes("delivery")) {
setIsDeliveryHovered(true);
} else if (e.target.className.includes("pickup")) {
setIsPickupHovered(true);
}
};
const handleOnMouseLeave = (e) => {
if (e.target.className.includes("delivery")) {
setIsDeliveryHovered(false);
} else if (e.target.className.includes("pickup")) {
setIsPickupHovered(false);
}
};
const handleClassStyler = (buttonType) => {
if (buttonType === 'delivery') {
if (isDeliveryClicked === true) {
return "deliveryClicked";
} else if (isDeliveryHovered === true && isDeliveryClicked === false) {
return "deliveryHovered";
} else if (isDeliveryClicked === false && isDeliveryHovered === false) {
return "delivery";
}
} else if (buttonType === 'pickup'){
if (isPickupClicked === true) {
return "pickupClicked";
} else if (isPickupHovered === true && isPickupClicked === false) {
return "pickupHovered";
} else if (isPickupClicked === false && isPickupHovered === false) {
return "pickup";
}
}
};
return (
<div className="App">
<div
onMouseOver={handleOnMouseOver}
onMouseLeave={handleOnMouseLeave}
onClick={handleClick}
className={handleClassStyler('delivery)}
>
Delivery
</div>
<div
onMouseOver={handleOnMouseOver}
onMouseLeave={handleOnMouseLeave}
onClick={handleClick}
className={handlePickupClassStyler('pickup')}
>
Pick Up
</div>
</div>
);
}
The CSS I used for above is:
.delivery {
width: 100px;
height: 100px;
background-color: red;
border: solid black 5px;
margin: 5px;
}
.deliveryClicked {
width: 100px;
height: 100px;
background-color: blue;
border: solid black 5px;
margin: 5px;
}
.deliveryHovered {
width: 100px;
height: 100px;
background-color: green;
border: solid black 5px;
margin: 5px;
}
.pickup {
width: 100px;
height: 100px;
background-color: red;
border: solid black 5px;
margin: 5px;
}
.pickupClicked {
width: 100px;
height: 100px;
background-color: blue;
border: solid black 5px;
margin: 5px;
}
.pickupHovered {
width: 100px;
height: 100px;
background-color: green;
border: solid black 5px;
margin: 5px;
}
Well,
After reading all the input (which was incredibly helpful to get my logic straight) i have come with an idea and some refactoring:
However, as almost close to a solution i still need to solve my default state. And i am stuck again.
Default state should be Delivery, and the (button) should have active CSS as well.
Also, when i add CSS .deliveryChoice:hover {} it does not respond. My guess is that, as it is a child component the header don't respond as it reads no inside buttons.
Right now, they are both off.
My Header component:
const Header = props => {
return (
<Fragment>
<header className={classes.header}>
<div className={classes.logo} onClick={reload}>
<div >
Foodzilla
</div>
</div>
<div className={classes.delivery}>
<DeliveryButton
className={classes.deliveryChoice}
/>
</div>
<div>
<div className={classes.deliveryAdress}>
Delivery to:
</div>
</div>
<div className={classes.deliveryTime}>
<div >
Choose time: Now
</div>
</div>
<HeaderCartButton onClick={props.onShowCart} />
</header>
<div className={classes['main-image']}>
<img src={mealsImg} />
</div>
</Fragment>
And my DeliveryButton:
const deliveryChoice = [{ name: 'Delivery' }, { name: 'PickUp' }]
const DeliveryButton = () => {
const [active, setActive] = useState(false);
return deliveryChoice.map((data, k) => (
<div
key={k}
className={`deliveryChoice ${active === k ?
classes.deliveryAction : ''}`}
onClick={() => setActive(k)}
>
{data.name}
</div>
));
};
And CSS for the Button:
.deliveryChoice {
}
.deliveryAction {
background-color: #ffffff;
border-color: #ffffff;
display: flex;
font-size: 13px;
justify-content: space-around;
width: 4rem;
height: 1.8rem;
cursor: pointer;
color: rgb(0, 0, 0);
align-items: center;
border-radius: 20px;
/* padding-left: 0.5rem; */
}

React.Children returns (function)string as `type` for Custom Components as Child with typescript

This may sound strange, maybe I completely get it wrong in the first place. But as I read some articles and react docs related to get the children and identify specific child via React.Component.map() however when I try this with my custom components, the child returns a stringify function as type. (I know that react does the stringify thing to prevent script injection). But I basically need to identify specific children that pass into the component and place them in the correct positions in another custom component. (materia_ui style).
<Card>
<CardTitle>
</CardTitle>
<CardContent>
</CardContent>
</Card>
The problem is I can't map passed children since the type has a string.
my environment uses
"react": "^17.0.2",
"#types/react": "^17.0.0",
"react-dom": "^17.0.2",
"#types/react-dom": "^17.0.0",
"typescript": "^4.1.2"
and this is what I have so far
type ExpandableCardProps = {
children: ReactElement<any> | ReactElement<any>[],
}
const ExpandableCard = ({children}: ExpandableCardProps) => {
React.Children.map(children, (child) => {
concole.log(child); // just can't map the child elements as described in some articales
})
// note that I need to identify the correct child to be render in correct position
render (
<div>
<div className="title-container">
// I want to render <ExpandableTitle> here
</div>
<div className="content-container">
// I want to render <ExpandableContent> here
</div>
<div className="content-other">
// may be some other elements here
</div>
</div>
);
}
export default ExpandableCardProps;
type CommonType = {
children: ReactNode;
}
export const ExpandableTitle ({children}:CommonType) => {
<div>
{children}
</div>
}
export const ExpandableContent ({children}:CommonType) => {
<div>
{children}
</div>
}
// usage
<ExpandableCard>
<ExpandableTitle>
/*{ some jsx here }*/
</ExpandableTitle>
<ExpandableContent>
/*{ some jsx here }*/
</ExpandableContent>
</ExpandableCard>
Here's what it looks like in the console
Here's an article I was referring to and which explained most closely what I need, but Can't use the pattern it explained since the type stringify thing, wonder it's with the React version or maybe as I mentioned earlier it's completely misunderstood by myself. I need some insight into this. How can I achieve something like this?
This seems to be working for me:
const ExpandableCard = ({children}) => {
const childArray = React.Children.toArray(children);
const expandableTitleIndex = childArray.findIndex(x => x.props.__TYPE === 'ExpandableTitle');
const expandableContentIndex = childArray.findIndex(x => x.props.__TYPE === 'ExpandableContent');
const additionalChildren = childArray.filter((_, index) => (index !== expandableTitleIndex && index !== expandableContentIndex));
return [childArray[expandableTitleIndex], childArray[expandableContentIndex], ...additionalChildren];
};
const App = () => {
return (
<ExpandableCard>
<div>Child 0 (div)</div>
<ExpandableContent>Child 1 (ExpandableContent)</ExpandableContent>
<ExpandableTitle>Child 2 (ExpandableTitle)</ExpandableTitle>
<div>Child 3 (div)</div>
</ExpandableCard>
);
};
const ExpandableTitle = ({children}) => (
<div>
{children}
</div>
);
ExpandableTitle.defaultProps = {
__TYPE: 'ExpandableTitle',
};
const ExpandableContent = ({children}) => (
<div>
{children}
</div>
);
ExpandableContent.defaultProps = {
__TYPE: 'ExpandableContent',
};
ReactDOM.render(<App />, document.querySelector("#app"));
Live on jsFiddle
After a few workarounds with the Neal Burns answer, I concluded with a typescript compatible solution.
I Will post it here since for someone it may be come in handy someday.
import React, { Children, ReactElement, ReactNode, useEffect, useRef, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import './expandableCard.scss';
import { v4 as uuidv4 } from 'uuid'
const types = {
EXPANDABLE_CARD_HEADER: 'expandableCardTitle',
EXPANDABLE_CARD_CONTENT: 'expandableCardContent',
EXPANDABLE_CARD_FOOTER: 'expandableCardFooter',
EXPANDABLE_ITEMS: 'expandableItems',
}
type ExpandableCardProps = {
id?: string;
select?: boolean;
onSelect?: (id: string) => void;
children: ReactElement<ExpandableCardContentProps> | ReactElement<ExpandableCardContentProps>[];
}
const ExpandableCard = ({ id = uuidv4(), select = false, children, onSelect = () => { } }: ExpandableCardProps) => {
const transitionRef = useRef(null);
const [selected, setSelected] = useState(select);
const [expand, setExpand] = useState(false);
const headerElement = useRef<any>(null);
const contentElement = useRef<any>(null);
const expandableFooter = useRef<any>(null);
const expandableItems = useRef<any>(null);
const handleSelected = () => {
setSelected(!selected);
}
useEffect(() => {
if (selected) {
onSelect(id);
}
}, [id, onSelect, selected])
const handleExpand = () => {
setExpand(!expand);
}
Children.forEach(children, (child) => {
switch (child.props.__TYPE) {
case types.EXPANDABLE_CARD_HEADER:
headerElement.current = child;
break;
case types.EXPANDABLE_CARD_CONTENT:
contentElement.current = child;
break;
case types.EXPANDABLE_ITEMS:
expandableItems.current = child;
break;
case types.EXPANDABLE_CARD_FOOTER:
expandableFooter.current = child;
break;
default:
return <div></div>;
}
});
return (
<div className={`expandable-card ${selected ? 'expandable-card-selected' : ''}`}>
<div className={`expandable-card--content ${expand ? 'expandable-card--content-active' : ''}`}>
<div className="expandable-card--expand-button">
<button type="button" onClick={handleExpand}>expand</button>
</div>
{headerElement.current &&
<div className="expandable-card--header">
{headerElement.current}
</div>
}
{contentElement.current}
<div className="d-flex align-items-center mt-3">
<button
type="button"
className={`btn expandable-card--button ${selected ? 'expandable-card--button-active' : ''}`}
onClick={handleSelected}>
{selected && !}
</button>
{expandableFooter.current}
</div>
</div>
<CSSTransition
nodeRef={transitionRef}
in={expand}
timeout={500}
classNames={`expandable-card--drawer`}
mountOnEnter
unmountOnExit>
<div ref={transitionRef} className="expandable-card--drawer">
{expandableItems.current}
</div>
</CSSTransition>
</div >
);
}
type ExpandableCardContentProps = {
children: ReactNode,
__TYPE: string;
}
export const ExpandableCardHeader = ({ children }: ExpandableCardContentProps) => {
return (
<>
{children}
</>
);
}
ExpandableCardHeader.defaultProps = {
__TYPE: types.EXPANDABLE_CARD_HEADER,
}
export const ExpandableCardContent = ({ children }: ExpandableCardContentProps) => (
<>
{children}
</>
);
ExpandableCardContent.defaultProps = {
__TYPE: types.EXPANDABLE_CARD_CONTENT,
}
export const ExpandableCardFooter = ({ children }: ExpandableCardContentProps) => (
<>
{children}
</>
);
ExpandableCardFooter.defaultProps = {
__TYPE: types.EXPANDABLE_CARD_FOOTER,
}
export const ExpandableItems = ({ children }: ExpandableCardContentProps) => (
<>
{children}
</>
);
ExpandableItems.defaultProps = {
__TYPE: types.EXPANDABLE_ITEMS,
}
export default ExpandableCard;
Please note that this is the complete expandable component with animations in it
I'll put up the SCSS code also with this to be complete
.expandable-card {
display: flex;
flex-direction: column;
box-shadow: 0 0px 25px 0px rgba(0, 0, 0, 0.2);
width: 100%;
background-color: #fff;
border-radius: 14px;
position: relative;
&--expand-button {
position: absolute;
top: 10px;
right: 15px;
}
&-selected {
border-bottom: 15px solid yellow;
border-radius: 14px;
}
&--content {
padding: 18px 15px;
border-radius: 14px 14px 0 0;
transition: all 500ms ease-out;
&-active {
z-index: 1;
box-shadow: 0 7px 7px 0 rgba(0, 0, 0, 0.2);
}
}
&--drawer {
display: flex;
flex-direction: column;
width: 100%;
max-height: 0;
background-color: #fff;
padding: 18px 20px;
border-radius: 0 0 14px 14px;
overflow-x: hidden;
overflow-y: auto;
transition: all 500ms ease-out;
/* .classes for help dropdown animations */
&-enter-active {
max-height: 320px;
padding: 18px 20px;
}
&-enter-done {
max-height: 320px;
padding: 18px 20px;
}
&-exit-active {
max-height: 0;
padding: 0 20px;
}
&-exit-done {
max-height: 0;
padding: 0 20px;
}
}
&--header {
display: flex;
align-items: center;
}
&--button {
min-width: 43px;
height: 43px;
background: transparent;
border: 2px solid aqua;
box-sizing: border-box;
border-radius: 10px;
&:focus {
box-shadow: none;
}
&-active {
background-color: blue;
border: none;
}
}
}

Styled Components - Conditionally render an entire css block based on props

I understand that styles can be conditionally rendered such as:
const HelloWorldLabel= styled("div")<{ centered?: boolean }>`
display: ${({ centered }) => (centered ? "block" : "flex")};;
margin: ${({ centered }) => (centered ? "auto 0" : "unset")};
padding: ${({ centered }) => (centered ? "0 15px" : "unset")};
`;
This does not look DRY - How can I (is it possible) render an entire block of css styles based on props?
Something like:
const HelloWorldLabel= styled("div")<{ centered?: boolean }>`
if (centered) {
display: "block" ;
margin: $"auto 0";
padding: "0 15px" ;
} else {
......
}
`;
With styled-component, or any CSS-in-JS, you can conditionally render a css block:
import styled, { css } from 'styled-components';
const light = css`
background-color: white;
color: black;
`;
const dark = css`
background-color: black;
color: white;
`;
const Box = styled.div`
${({ isDark }) => (isDark ? light : dark)}
`;
Full Example:
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import styled, { css } from 'styled-components';
const light = css`
background-color: white;
border: 2px solid black;
color: black;
`;
const dark = css`
background-color: black;
color: white;
`;
const FlexBox = styled.div`
margin: 20px;
padding: 20px;
${({ isDark }) => (isDark ? light : dark)}
`;
const App = () => {
const [isDark, setIsDark] = useState(false);
const toggle = () => setIsDark(b => !b);
return (
<FlexBox isDark={isDark}>
<div>Some Text</div>
<button onClick={toggle}>Change Block</button>
</FlexBox>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
A less verbose way that worked for me is
const SideMenu = styled.aside`
width: 200px;
${({ isHidden }) => isHidden && `
display: none;
`}
// another random prop you need here
${({ redBg }) => redBg && `
background-color: red;
`}
`;
You can use a function and return the css based on prop:
const HelloWorldLabel= styled("div")`
${({centered}) => {
if (centered) {
return `
display: "block" ;
margin: "auto 0";
padding: "0 15px";
`
} else {
return `// Other styles here`
}
}}
`;
The alternative is
let customCss = setCustomCss(position) => {
let positionCss = {
center: [ 'css: value;', 'css:value;'],
left: .....
right: ....
}
return return positionCss[position];
}
let HelloWorldLabel= styled('div')(customCss, {
/* css common to all */
})

Create React List with Remove Button on Hover

I'm creating a DropDown List box and each item in the list has a remove (X) button to remove the item from the list. How is it possible to show the remove button "only" when the item is hovered over?
The current code shows the clear button each each item but I only want it to show when the item is hovered over
Sorry, here is the code
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
const ListWrapper = styled.div`
position: absolute;
width: 16rem;
z-index: 1;
background: white;
&:hover {
cursor: pointer;
}
`;
const ListMenu = styled.div`
position: absolute;
width: 100%;
z-index: 1;
background: white;
overflow-x: hidden;
`;
const ListMenuHeader = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
`;
const DropdownText = Text.Link.extend`
padding-top: 3rem;
`;
const DropdownButton = styled.div`
padding: 1 rem 0.75rem;
`;
const ListMenuItem = styled.div`
display: flex;
background-color: grey)};
color: grey};
>[name~=icon] {
right: 0rem;
border-radius: 0;
background: none;
align-items: right;
justify-content: right;
&:hover {
background-color: grey)};
}
&:focus {
outline: none;
}
`;
class ListListMenu extends React.Component {
static propTypes = {
id: PropTypes.string.isRequired,
text: PropTypes.node.isRequired,
items: PropTypes.arrayOf(PropTypes.any).isRequired,
component: PropTypes.func.isRequired,
selectedItem: PropTypes.any,
getItemProps: PropTypes.func.isRequired,
highlightedIndex: PropTypes.number,
closeListMenu: PropTypes.func.isRequired,
};
static defaultProps = {
selectedItem: null,
highlightedIndex: null,
}
onClearClick = (items,item1) => (item) => {
const index = items.indexOf(item1);
if (index > -1) {
items.splice(index, 1);
}
}
render() {
const {
id, text, items, component, selectedItem, getItemProps,
highlightedIndex, closeListMenu,
} = this.props;
return (
<ListWrapper id={id} >
<ListMenuHeader onClick={closeListMenu}>
<DropdownText>{text}</DropdownText>
<DropdownButton
id={`${id}-button`}
>
<Icon type="caret-up" appearance="neutral" />
</DropdownButton>
</ListMenuHeader>
<ListMenu>
{selectedItems.map((item, index) => (
<ListMenuItem
{...getItemProps({
item,
isActive: highlightedIndex === index,
isSelected: _.isEqual(selectedItem, item),
})}
key={index}
>
{React.createElement(component, { item })}
<Button // CLEAR BUTTON
name={item}
id={item}
icon="remove"
onClick={this.onClearClick(items, item)}
circle
display="flat"
appearance="disabled"
id="clear-search-button"
/>
</ListMenuItem>
))}
</ListMenu>
</ListWrapper>
);
}
}
export default ListListMenu;
Here is one way you could probably just have that "x" appear on hover.
Instead of looking for a "hover" event, what about looking for an "onmouseenter" event combined with "onmouseleave"?
Like so...
class Example extends React.Component {
onHover() {
this.refs.deleteX.style.display = "block";
}
onExit() {
this.refs.deleteX.style.display = "none";
}
render() {
return (
<div>
<input onmouseenter={ this.onHover } onmouseleave={ this.onExit } />
<p ref="deleteX">x</p>
</div>
)
}
}
Kind of like this post

Categories