In react-contenteditable, the html attributes only accepts string, how can I manage to add JSX element with eventlistener with in the string.
Sandbox
import ContentEditable from "react-contenteditable";
import "./styles.css";
const text = "I want to order cheese chicken pizza.";
const Elems = {
cheese: (
<span style={{ color: "red" }} onClick={() => alert("clicked cheese span")}>
cheese
</span>
),
chicken: (
<span
style={{ color: "red" }}
onClick={() => alert("clicked chicken span")}
>
chicken
</span>
)
};
export default function App() {
const swapText = () => {
const text_array = text.split(" ");
console.log(text_array);
const a = text_array.map((item) => {
if (item in Elems) item = Elems[item];
else item += " ";
return item;
});
return a;
};
return (
<div className="App">
<h2>React contenteditable</h2>
<ContentEditable html={swapText()} />
</div>
);
}
You can convert react elements to markup using ReactDOMServer.renderToStaticMarkup(element). This would help with the styles, but not with the click handler:
if (item in Elems) item = renderToStaticMarkup(Elems[item]);
For the items to be clickable, you'll need to pass an onClick handler to <ContentEditable> component (or a parent of it):
<ContentEditable onClick={handleClick} html={swapText()} />
You would also need to identify the clickable elements. In this example, I've data-action tags to both of them:
const Elems = {
cheese: (
<span style={{ color: 'red' }} data-action="cheese">
cheese
</span>
),
chicken: (
<span style={{ color: 'red' }} data-action="chicken">
chicken
</span>
)
};
The click handler searches the event target or a parent that has the data-action tag using Element.closest(), if it finds one it acts on the tags value:
const handleClick = (e) => {
const target = e.target.closest('[data-action]');
if (!target) return;
const action = target.dataset.action;
alert(action);
};
Working example - sandbox
Related
**> This is my Gallery Component **
import React, {useState} from 'react';
import useFirestore from '../hooks/useFirestore';
import { motion } from 'framer-motion';
const Gallery = ({ setSelectedImg }) => {
const { docs } = useFirestore('images');
here im setting the state as a Tags array
const [tags, setTags] = useState([""]);
const addTag = (e) => {
if (e.key === "Enter") {
if (e.target.value.length > 0) {
setTags([...tags, e.target.value]);
e.target.value = "";
}
}
};
functions for adding and removing Tags
const removeTag = (removedTag) => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
};
return (
<>
<div className="img-grid">
{docs && docs.map(doc => (
< motion.div className="img-wrap" key={doc.id}
layout
whileHover={{ opacity: 1 }}s
onClick={() => setSelectedImg(doc.url)}
>
here Im adding the Tag input to each Image...the problem is that when adding a Tag is added to all the pictures. I want to add the tags for the image that I´m selecting.
<div className="tag-container">
{tags.map((tag, ) => {
return (
<div key={doc.id} className="tag">
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
<motion.img src={doc.url} alt="uploaded pic"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
>
</motion.img>
</motion.div>
))}
</div>
</>
)
}
export default Gallery;
The tags array that you are using to store values entered by the user are not unique with respect to each image item. Meaning, every image item in your program is using the same instance of the tags array, what you need to do is
Either create an object that stores an array of tags for each image:
const [tagsObj, setTagsObj] = {}, then while adding a new tag for say image_1, you can simply do setTagsObj(prevObj => {...prevObj, image_1: [...prevObj?.image_1, newTagValue]},
Or create an Image Component which would then handle tags for a single image:
Gallery Component:
{
imageList.map(imageEl =>
<ImageItem key={imageEl} image={imageEl} />
)
}
ImageItem Component:
import {useState} from 'react';
export default function ImageItem({image}) {
const [tags, setTags] = useState([]);
const addTag = (e) => {
if (e.key === "Enter") {
const newVal = e.target.value;
if (newVal.length > 0) {
setTags(prevTags => [...prevTags, newVal]);
e.target.value = '';
}
}
};
const removeTag = (removedTag) => {
setTags(prevTags => prevTags.filter((tag) => tag !== removedTag));
}
return (
<div style={{margin: '12px', padding: '12px', width: '100px', height:'100px', display:'flex', flexDirection: 'column', alignItems:'center'}}>
<span>{image}</span>
{tags.map((tag, index) => {
return (
<div key={tag+index}>
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
);
}
Refer this sandbox for ease, if available Gallery unique image tags sandbox
I suggest using the second method, as it is easy to understand and debug later on.
I hope this helps, please accept the answer if it does!
I'm trying develop a little app in which on you can select multiple music album, using Next.js.
I display my albums like the image below, and I would like to add a check mark when clicked and hide it when clicked again.
My code looks like that :
import Image from "next/image";
import {Card,CardActionArea} from "#mui/material";
import { container, card } from "../styles/forms.module.css";
import album from "../public/album.json"
export default function Album() {
const albumList = {} ;
function addAlbum(albumId, image){
if ( !(albumId in albumList) ){
albumList[albumId] = true;
//display check on image
}
else{
delete albumList[albumId]
//hide check on image
}
console.log(albumList)
}
return (
<div className={container}>
{Object.keys(album.albums.items).map((image) => (
<Card className={card}>
<CardActionArea onClick={() => addAlbum(album.albums.items[image].id)}>
<Image alt={album.albums.items[image].artists[0].name} width="100%" height="100%" src={album.albums.items[image].images[1].url} />
</CardActionArea>
</Card>
))}
</div>
);
}
I know I should use useState to do so, but how can I use it for each one of my albums?
Sorry if it's a dumb question, I'm new with Hook stuff.
I think there are a few ways to go about this, but here is a way to explain the useState in a way that fits the question. CodeSandbox
For simplicity I made a Card component that knowns if it has been clicked or not and determines wither or not it should show the checkmark. Then if that component is clicked again a clickhandler from the parent is fired. This clickhandle moves the Card into a different state array to be handled.
The main Component:
export default function App() {
const [unselectedCards, setUnselectedCards] = useState([
"Car",
"Truck",
"Van",
"Scooter"
]);
const [selectedCards, setSelectedCards] = useState([]);
const addCard = (title) => {
const temp = unselectedCards;
const index = temp.indexOf(title);
temp.splice(index, 1);
setUnselectedCards(temp);
setSelectedCards([...selectedCards, title]);
};
const removeCard = (title) => {
console.log("title", title);
const temp = selectedCards;
const index = temp.indexOf(title);
temp.splice(index, 1);
setSelectedCards(temp);
setUnselectedCards([...unselectedCards, title]);
};
return (
<div className="App">
<h1>Current Cards</h1>
<div style={{ display: "flex", columnGap: "12px" }}>
{unselectedCards.map((title) => (
<Card title={title} onClickHandler={addCard} key={title} />
))}
</div>
<h1>Selected Cards</h1>
<div style={{ display: "flex", columnGap: "12px" }}>
{selectedCards.map((title) => (
<Card title={title} onClickHandler={removeCard} key={title} />
))}
</div>
</div>
);
}
The Card Component
export const Card = ({ onClickHandler, title }) => {
const [checked, setChecked] = useState(false);
const handleClickEvent = (onClickHandler, title, checked) => {
if (checked) {
onClickHandler(title);
} else {
setChecked(true);
}
};
return (
<div
style={{
width: "200px",
height: "250px",
background: "blue",
position: "relative"
}}
onClick={() => handleClickEvent(onClickHandler, title, checked)}
>
{checked ? (
<div
id="checkmark"
style={{ position: "absolute", left: "5px", top: "5px" }}
></div>
) : null}
<h3>{title}</h3>
</div>
);
};
I tried to make the useState actions as simple as possible with just a string array to help you see how it is used and then you can apply it to your own system.
You do not need to have a state for each album, you just need to set albumList as a state:
const [albumList, setAlbumList] = setState({});
function addAlbum(albumId, image) {
const newList = {...albumList};
if(!(albumId in albumList)) {
newList[albumId] = true;
} else {
delete albumList[albumId]
}
setAlbumList(newList);
}
And then in your loop you can make a condition to display the check mark or not by checking if the id is in albumList.
I'm trying to display fields based on the value of a props so let's say my props value = 2 then I want to display 2 inputs but I can't manage to get it work.
This is what I tried
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
<div>
<p style={{color:'black'}}>Tier {tier + 1}</p>
<InputNumber />
</div>
})
}
const onPropsValueLoaded = (value) => {
let tmp = value
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(value).keys()
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}
useEffect(() => {
onPropsValueLoaded(props.numberOfTiers);
}, [])
return (
<>
<Button type="primary" onClick={showModal}>
Buy tickets
</Button>
<Modal
title="Buy ticket"
visible={visible}
onOk={handleOk}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<p style={{ color: 'black' }}>{props.numberOfTiers}</p>
{loadFields.length ? (
<div>{addField()}</div>
) : null}
<p style={{ color: 'black' }}>Total price: </p>
</Modal>
</>
);
so here props.NumberOfTiers = 2 so I want 2 input fields to be displayed but right now none are displayed even though loadFields.length is not null
I am displaying this inside a modal (even though I don't think it changes anything).
I am doing this when I load the page that's why I am using the useEffect(), because if I use a field and update this onChange it works nicely.
EDIT:
I changed the onPropsValueLoaded() function
const generateArrays = Array.from({length : tmp}, (v,k) => k)
instead of
const generateArrays = Array.from(value).keys()
There are couple of things you should fix in here,
First, you need to return div in addField function to render the inputs.
Second, you should move your function onPropsValueLoaded inside useEffect or use useCallback to prevent effect change on each render.
Third, your method of creating array using Array.from is not correct syntax which should be Array.from(Array(number).keys()).
So the working code should be , I also made a sample here
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [numberOfFields, setNumberOfFields] = useState(0);
const [loadFields, setloadFields] = useState([]);
const addField = () => {
return loadFields.map((tier) => {
return (
<div key={tier}>
<p style={{ color: "black" }}>Tier {tier + 1}</p>
<input type="text" />
</div>
);
});
};
useEffect(() => {
let tmp = 2; // tier number
setNumberOfFields(tmp);
if (numberOfFields > 0) {
const generateArrays = Array.from(Array(tmp).keys());
setloadFields(generateArrays);
} else {
setloadFields([]);
}
}, [numberOfFields]);
return (
<>
<button type="button">Buy tickets</button>
<p style={{ color: "black" }}>2</p>
{loadFields.length ? <div>{addField()}</div> : null}
<p style={{ color: "black" }}>Total price: </p>
</>
);
}
How can I render only the icon cartIcon dynamically? Because right now, like the code below, when I enter in the component with the mouse, all the icons appears not only the icon of the single product.
I think because of map but how can I render only to it?
interface IItemsProps {
products: ProductsType;
}
const Items: React.FunctionComponent<IItemsProps> = ({ products }) => {
const [state, setState] = React.useState<boolean>(false);
const handleMouseEnter = () => {
setState(true);
};
const handleMouseLeave = () => {
setState(false);
};
const itemUI = products.map((item: SingleProductsType) => {
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state && <IconsCarts />} ** //HERE I NEED TO SHOW THIS COMPONENT ONLY WHEN I
// ENTER WITH THE MOUSE BUT ONLY FOR THE SELECTED
//PRODUCT NOT ALL OF THEM **
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
});
return <WrapperItems>{itemUI}</WrapperItems>;
};
export default Items;
You could store the hovered _id in state, so you know which one it was.
const [state, setState] = React.useState<string | null>(null); // or `number` ?
Then
{state === _id && <IconsCarts />}
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={() => setState(_id)}
onMouseLeave={() => setState(null)}
/>
Or you could move the useState into a component that is called every loop of your map, so that each item has its own private state.
function MyItem({item}: { item: SingleProductsType }) {
const [state, setState] = React.useState<boolean>(false);
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state && <IconsCarts />}
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
}
Now you can do:
{products.map((item: SingleProductsType) => <MyItem item={item} />}
Lastly, if all you want to do is show/hide the cart icon when you enter some element with the mouse, this solution is probably way overkill. You can do this with CSS alone, which is going to be a far cleaner solution since it takes no javascript code whatsoever, and you don't have to track state at all.
.item {
width: 100px;
height: 100px;
background: #aaa;
margin: 10px;
}
.item button {
display: none;
}
.item:hover button {
display: block;
}
<div class="item">
Foo
<button>Add to cart</button>
</div>
<div class="item">
Bar
<button>Add to cart</button>
</div>
<div class="item">
Baz
<button>Add to cart</button>
</div>
With a boolean in state, all you know is whether to show an icon, but what about knowing which list item to show the icon on? Instead of state being a boolean, how about we use the index of the product.
interface IItemsProps {
products: ProductsType;
}
const Items: React.FunctionComponent<IItemsProps> = ({ products }) => {
const [state, setState] = React.useState<number>(-1);
const handleMouseEnter = (index) => {
setState(index);
};
const handleMouseLeave = () => {
setState(-1);
};
const itemUI = products.map((item: SingleProductsType, index: number) => {
const { name, price, _id } = item;
return (
<WrapperSingleItem key={uuidv4()} id={_id}>
{state === index && <IconsCarts />} ** //Check if index matches state before showing icon **
<ImgProduct
src={mouse}
alt={name}
onMouseEnter={() => handleMouseEnter(index)}
onMouseLeave={handleMouseLeave}
/>
<WrapperTextProduct>
<TextName>{name}</TextName>
<div>
<TextActualPrice>$ {price}</TextActualPrice>
<TextPreviousPrice>
$ {Math.trunc((price * 20) / 100 + price)}.00
</TextPreviousPrice>
</div>
</WrapperTextProduct>
</WrapperSingleItem>
);
});
return <WrapperItems>{itemUI}</WrapperItems>;
};
export default Items;
Now the condition to show the icon is if the index of the list item matches the index in state. And we pass in the index to handleMouseEnter to set state to that index, and handleMouseLeave will reset it back to -1.
UPDATED:
function TheCards() {
let cardMasterList = require('./scryfall-oracle-cards.json')
let cardMasterListSorted = sortByKey(cardMasterList, 'name')
let [cardClicked, setCardClicked] = useState(null)
//console.log(cardMasterList[0].name)
//console.log(cardMasterListSorted)
const handleClick = () => {
setCardClicked('red')
console.log(cardClicked)
//Please make this do something with the clicked span tag!
}
return (
<form id="card-master-list">
{cardMasterListSorted
.filter(({legalities}) => legalities.vintage === 'legal')
.filter(({rarity}) => rarity === 'common' || 'uncommon')
.slice(0,10)
.map(
(cardName) => {
return (
<li key={cardName.id}>
<span className="card-name" onClick={ handleClick }>{cardName.name}</span>
</li>
)
}
)
}
</form>
)
}
Update: This is the updated code above. I STILL can't figure out how to reference what I'm clicking on. I want to do more than simply change the color of the text, I'm just using "change it to red" as something simple to do. I am sad to say I'm missing jQuery and its easy "this" reference points, because something like "this.handleClick" doesn't work.
function TheCards() {
let cardMasterList = require('./scryfall-oracle-cards.json')
let cardMasterListSorted = sortByKey(cardMasterList, 'name')
console.log(cardMasterList[0].name);
console.log(cardMasterListSorted)
return (
<form id="card-master-list">
{cardMasterListSorted
.filter(({legalities}) => legalities.vintage === 'legal')
.filter(({rarity}) => rarity === 'common' || 'uncommon')
.map(
(cardName) => {
return (
<li key={cardName.id}>
<span className="card-name" onClick={ function DoTheThing() { document.querySelector('.card-name').style.color = 'red' } }>{cardName.name}</span>
</li>
)
}
)
}
</form>
)
}
All I want to do is so when the span is clicked on, it turns red. This seems impossible as nothing I try works - writing another function to fire in any way I can think of. "this" isn't working as expected. What can I replace the document.querySelector line with to make it work?
The method in your code is not the recommended way of updating the UI when rendering with React. Ideally you would update the style of the span via styling data stored in your components state:
<li key={cardName.id}>
<span className="card-name" onClick={
() => {
/* Set the color state of component to red which triggers a
render() and will update the style prop of the span
accordingly */
this.setState({ color : 'red' });
}
} style={{ color : this.state.color }}>
{cardName.name}</span>
</li>
If you really want to update the styling via the pattern in your original code, then one way to do this would be via a "callback ref" which would give you access to the span's corresponding DOM element:
<li key={cardName.id}>
<span className="card-name" ref={ (spanElement) => {
spanElement.addEventListener('click', () => {
spanElement.style.color = 'red';
});
}}>
{cardName.name}</span>
</li>
Here's an example of how to update styles the React way:
import React, { useState } from "react";
import ReactDOM from "react-dom";
const tiles = [
{
id: 1,
n: "one"
},
{
id: 2,
n: "two"
}
];
const App = () => {
const [clickedId, setClickedId] = useState(null);
const click = id => {
setClickedId(id);
};
return (
<div style={{ display: "flex", flexDirection: "column" }}>
{tiles.map(tile => (
<div
onClick={() => click(tile.id)}
style={{ color: tile.id === clickedId ? "red" : "black" }}
>
{tile.n}
</div>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Live example here.
That should work as you have it. Though as codecubed.io pointed out is not a very react way to do it. instead it is preferred to toggle a class. something like this:
const styles = {
redTextClass: {
color: 'red',
}
}
<span className={stateVariable ? styles.redTextClass : null}></span>