React Map onClick and "this" undefined - reactjs

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>

Related

React gallery App. I want Add tags to an image individually but the tag is being added to all images. How can I solve this?

**> 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!

Cannot display a number of input fields given a value

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>
</>
);
}

TypeError: Cannot read property '0' of undefined when building my react app

while building my react app for deployment, I am getting this error
TypeError: Cannot read property '0' of undefined
when I am rending on port3000 I did not see this error but only get it while building the app.
Can anyone assist to resolve this?
import { useState } from "react";
import styles from "./Tabs.module.css"
const Tabs = ({ children}) => {
const [activeTab, setActiveTab] = useState (children [0].props.label);
const handleClick =( e, newActiveTab ) => {
e.preventDefault();
setActiveTab(newActiveTab);
}
return (
<div>
<ul className= {styles.tabs}>
{children.map ((tab) => {
const label = tab.props.label;
return (
<li
className= {label == activeTab ? styles.current : ""}
key= {label}
>
<a href="#" onClick={(e) => handleClick (e, label)}>{label}
</a>
</li>
)
})}
</ul>
{children.map ((tabcontent1) => {
if (tabcontent1.props.label == activeTab)
return (
<div key= {tabcontent1.props.label} className= {styles.content}>{tabcontent1.props.children}
</div>
);
})}
</div>
);
}
export default Tabs ;
In next js, when you don't put export const getServerSideProps = () => {} in your page then that page is automatically subjected to static side rendering. On development mode, you may see a lightening symbol on bottom-right. Anyway you can read the docs on data-fetching on nextjs. However, your issue on this situation can be easily fixed by setting the children through useEffect.
// handle null on your active tab render function
const [activeTab, setActiveTab] = useState(null);
useEffect(() => {
if(children.length)
children[0].props.label
}, [children])
Another Code Sample:
*A simple change in code structure and the way you are trying to do. It's on react but kind of same in next as well *
import React from "react";
const Tabs = ({ tabsData }) => {
const [activeTabIndex, setActiveTabIndex] = React.useState(0);
const switchTabs = (index) => setActiveTabIndex(index);
return (
<div style={{ display: "flex", gap: 20, cursor: "pointer" }}>
{/* Here active tab is given a green color and non actives grey */}
{tabsData.map((x, i) => (
<div
key={i}
style={{ color: activeTabIndex === i ? "green" : "#bbb" }}
onClick={() => switchTabs(i)}
>
{x.label}
</div>
))}
{/* Show Active Tab Content */}
{tabsData[activeTabIndex].content}
</div>
);
};
export default function App() {
// You can place it inside tabs also in this case
// but lets say you have some states on this component
const tabsData = React.useMemo(() => {
return [
// content can be any component or React Element
{ label: "Profile", content: <p>Verify all Input</p> },
{ label: "Settings", content: <p>Settings Input</p> },
{ label: "Info", content: <p>INput info</p> }
];
}, []);
return (
<div className="App">
<Tabs tabsData={tabsData} />
</div>
);
}
and here is also a example sandbox https://codesandbox.io/s/serverless-night-ufqr5?file=/src/App.js:0-1219

How to edit a todo in a todo list without hooks and redux

I have been stuck on this for days reading up on tutorials and articles but can not figure this out. Whenever I click on the pencil icon, I want it to edit the current do to. I have 4 components, the form (searchbar where i add todo), the app.js, the todoList, and a todo.js component. I am keeping all the state in the app and state in the form to keep track of the terms I am entering.
I am thinking I would need to create an editTodo method in the app and pass it down as a prop to the list and then the todoItem. Most tutorials or help online uses hooks or redux but I am learning vanilla React first. I am not asking for the answer directly but rather the steps or thought process to implement editing a todo item in the todolist. I am not sure even if my todo app is correct in the places where I am keeping state. I may get slack for asking.. but I do not know what else to do. Here is my code..
class App extends React.Component {
state = {
todos: []
}
addTodo = (todo) => {
const newToDos = [...this.state.todos, todo];
this.setState({
todos: newToDos
});
};
deleteTodo = (id) => {
const updatedTodos = this.state.todos.filter((todo) => {
return todo.id !== id;
});
this.setState({
todos: updatedTodos
});
}
editTodo = (id, newValue) => {
}
render() {
return (
<div className="container">
<div className="row">
<div className="col">
<Form addTodo={this.addTodo} />
</div>
</div>
<div className="row">
<div className="col">
<ToDoList
todos={this.state.todos}
deleteTodo={this.deleteTodo}
editingTodo={this.state.editingTodo}/>
</div>
</div>
</div>
)
}
}
export default App;
const ToDoList = ({todos, deleteTodo, editingTodo}) => {
const renderedList = todos.map((todo, index) => {
return (
<ul className="list-group" key={todo.id}>
<ToDoItem todo={todo} deleteTodo={deleteTodo} editingTodo={editingTodo}/>
</ul>
)
});
return (
<div>
{renderedList}
</div>
)
}
export default ToDoList;
const ToDoItem = ({todo, deleteTodo}) => {
return (
<div>
<li style={{display: 'flex', justifyContent: 'space-between' }} className="list-group-item m-3">
{todo.text}
<span>
<FontAwesomeIcon
icon={faPencilAlt}
style={{ cursor: 'pointer'}}
/>
<FontAwesomeIcon
icon={faTrash}
style={{ marginLeft: '10px', cursor: 'pointer'}}
onClick={ () => deleteTodo(todo.id)}
/>
</span>
</li>
</div>
);
}
export default ToDoItem;
I don't think the form component is relevant here as I am trying to edit a todo item so will not include it here. If I do need to include it, let me know. It may not look like I have tried to implement this functionality, but either I could not find what I was looking for, understand the code, or just do not know how to implement it.
Update:
I added an isEditing field in the form component to my todo items so that maybe it can help me know if an item is being editing or not. I also redid the editTodo method.
class Form extends React.Component {
state = { term: ''};
handleSubmit = (e) => {
e.preventDefault();
this.props.addTodo({
id: shortid.generate(),
text: this.state.term,
isEditing: false
});
this.setState({
term: ''
});
}
editTodo = (id, newValue) => {
const editedTodos = [...this.state.todos].map((todo) => {
if(todo.id === id) {
todo.isEditing = true;
todo.text = newValue;
}
return todo.text;
});
this.setState({
todos: [...this.state.todos, editedTodos]
});
}
I also passed that method down to the todoList and then to the todoItem like so
const ToDoItem = ({todo, deleteTodo, editTodo}) => {
const renderContent = () => {
if(todo.isEditing) {
return <input type='text' />
} else {
return <span>
<FontAwesomeIcon
icon={faPencilAlt}
style={{ cursor: 'pointer'}}
onClick={ () => editTodo(todo.id, 'new value')}
/>
<FontAwesomeIcon
icon={faTrash}
style={{ marginLeft: '10px', cursor: 'pointer'}}
onClick={ () => deleteTodo(todo.id)}
/>
</span>
}
}
return (
<div>
<li style={{display: 'flex', justifyContent: 'space between'}} className="list-group-item m-3">
{{!todo.isEditing ? todo.text : ''}}
{renderContent()}
</li>
</div>
);
}
So whenever I click on the the edit icon, it successfully shows 'new value' but now also adds an extra todo item which is blank. I figured out how to add the input field so that it shows also. I am accepting the answer Brian provided since it was the most helpful in a lot of ways but have not completed the functionality for editing a todo.
am thinking I would need to create an editTodo method in the app and pass it down as a prop to the list and then the todoItem.
This is exactly what you need to do. And yet:
editTodo method has no logic in it.
ToDoList component receives editingTodo method as a prop instead of defined editTodo.
You are indeed passing the editingTodo futher down to ToDoItem but you are not utilising it there const ToDoItem = ({todo, deleteTodo}) => ...
You don't have an onClick listener on the pencil icon, so nothing can happen.
I don't know how you are planning on doing the editing (modal window with a form, or replacing the text with an input field), either way the bottom line is that you need to trigger your pencil onClick listener with () => editTodo(id, newText).
My recommendation would be - address all 5 points above and for now just hardcode the new value, just to test it out: () => editTodo(id, 'updated value!') and check that everything works. You can worry about getting the real value in there as your next step.

Render JSX element in react-contenteditable

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

Resources