I want to add sub categories with react on a list, but when i click on sub cat, i have two events : First on sub and second on parent category.
How can i have only child category ?
There is my actual code :
getList(myList){
return myList.map((item) => {
let subList = '';
if (item.hasSub){
subList = (<ul>{this.getList(item.sub)}</ul>)
}
return (
<li onClick={() => {this.props.clicHandler(item.id)}}>{item.title}<div>{subList}</div></li>);
})
}
I'm using recursive method to create my list.
My actual array is like this :
this.list.push({id: 1, title:'coucou' , hasSub: false});
this.list.push({id: 2, title:'toto' , hasSub: false});
this.list.push({id: 3, title: 'cat', sub: [{id:4, title:'titi' , hasSub: false}, {id:5, title:'tutu' , hasSub: false}] , hasSub: true});
Thank you for your help !
The problem is that click events "bubble" up to parent elements. The solution is to call Event.prototype.stopPropagation on it.
In the below snippet I've added a clickHandler method to the component, and in getList I pass both the event (evt) and item.id to it. In that method I call evt.stopPropagation() before calling this.props.clickHandler. However, you could also do this in the parent component's clickHandler method, or within getList. Choose whatever best suits your use case.
class App extends React.Component {
constructor() {
super();
this.clickHandler = this.clickHandler.bind(this);
}
render() {
return <ul>{this.getList(this.props.items)}</ul>;
}
getList(list) {
return list.map((item) => {
const subList = item.hasSub && (<ul>{this.getList(item.sub)}</ul>);
return (
<li key={item.id} onClick={evt => {this.clickHandler(evt, item.id)}}>{item.title}<div>{subList}</div></li>
);
});
}
clickHandler(evt, itemId) {
// Stop event propagatation before calling handler passed in props
evt.stopPropagation();
this.props.clickHandler(itemId);
}
}
const data = [
{ id: 'a', title: 'A', hasSub: false },
{ id: 'b', title: 'B', hasSub: true, sub: [
{ id: 'b1', title: 'B1', hasSub: true, sub: [
{ id: 'b1a', title: 'B1a', hasSub: false },
] },
{ id: 'b2', title: 'B2', hasSub: false },
] },
{ id: 'c', title: 'C', hasSub: true, sub: [
{ id: 'c1', title: 'C1', hasSub: false },
{ id: 'c2', title: 'C2', hasSub: false },
] },
];
function clickHandler(itemId) {
console.log('clicked item', itemId);
}
ReactDOM.render(<App items={data} clickHandler={clickHandler}/>, document.querySelector('div'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div></div>
P.S. Your code is excellent, but might I recommend a somewhat more "React way" to render nested items?
What your component's render method does is call a getItems function that in turn calls itself, recursively. However, if you recall that most React components can themselves be thought of as functions, you can refactor your code so that your render method instead renders an <Items> component that recursively renders instances of itself.
This approach tends to yield smaller, functional components that are more reusable and easier to test. Take a look in the snippet below.
const Items = ({items, onClick}) => (
<ul>
{items.map(item => (
<li key={item.id} onClick={evt => onClick(evt, item.id)}>
{item.title}
{item.hasSub && <Items items={item.sub} onClick={onClick}/>}
</li>
))}
</ul>
);
const data = [
{ id: 'a', title: 'A', hasSub: false },
{ id: 'b', title: 'B', hasSub: true, sub: [
{ id: 'b1', title: 'B1', hasSub: true, sub: [
{ id: 'b1a', title: 'B1a', hasSub: false },
] },
] },
];
function clickHandler(evt, itemId) {
evt.stopPropagation();
console.log('clicked item', itemId);
}
ReactDOM.render(<Items items={data} onClick={clickHandler}/>, document.querySelector('div'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.js"></script>
<div></div>
You need to prevent the event propagation :
getList(myList){
return myList.map((item) => {
let subList = '';
if (item.hasSub){
subList = (<ul>{this.getList(item.sub)}</ul>)
}
return (
<li onClick={(event) => {event.preventDefault(); this.props.clicHandler(item.id)}}>{item.title}<div>{subList}</div></li>);
})
}
This way the first item to catch the click will stop the click propagation.
Related
I am new to React and dev in general, but I am struggling to figure out how to achieve what I am trying to do. I feel as though I may have missed something along the way.
My goal is to have a list of items, that which on clicked individually, will toggle the visibility of their information.
The problem is that I am not able to map over the state in the parent element to display each object. But the state is in an array so I don't understand why it wouldn't be iterable. I do not get this problem when its just an object that I pass props to the child without state.
Is this the correct way to go about doing this? Am I supposed to create another array just to map over my object? I've also been a little confused as some sources create a class and use the constructor and render function. Is that deprecated or should I be doing it this way?
Parent
import React from "react";
import { useState } from "react";
//Components
import Card from "./Card";
const CardStack = () => {
const [habits, setHabits] = [
{
id: 1,
merit: "good",
title: "Good Habit",
count: 4,
text: "Words to be hidden",
visible: false,
},
{
id: 2,
merit: "bad",
title: "Bad Habit",
count: 1,
text: "Words to be hidden",
visible: false,
},
{
id: 3,
merit: "good",
title: "Good Habit",
count: 6,
text: "Words to be hidden",
visible: true,
},
];
const toggleCard = () => {
this.setHabits((habit) => {
habit.visible = !visible;
});
};
return (
<div className="card-stack">
{habits.map((habit) => (
<Card habit={habit} key={habit.id} onClick={toggleCard} />
))}
</div>
);
};
export default CardStack;
Child
import React from "react";
//Components
import Button from "./Button";
const Cards = ({ habit, onClick }) => {
return (
<div className="card" key={habit.id} onClick={onClick}>
<h4 className="title" merit={habit.merit}>
{habit.title}
<div className="btn-group">
<Button className="button" />
<span className="count">{habit.count}</span>
<Button className="button" />
</div>
{habit.visible ? (
<div className="content">
<p>visible</p>
</div>
) : null}
</h4>
</div>
);
};
export default Cards;
There are a number of problems with your code.
The first has been pointed out by #talfreds in their answer – you need to call useState() to initialize the state variable and its corresponding setter.
const CardStack = () => {
const [habits, setHabits] = useState([
{
id: 1,
merit: "good",
title: "Good Habit",
count: 4,
text: "Words to be hidden",
visible: false,
},
...]);
Just doing this should allow your component to render.
But once you click the button, your current toggle handler will overwrite the array stored in habits with a boolean.
To fix this you need to understand that the callback you pass to setState is passed the current value of the relevant state variable for you to work with, and the state will be set to the value that you return from the callback. When working with arrays you need to avoid directly mutating this passed value, in this example by using map() which returns a new array, and by cloning the 'habit' object that we are changing use spread syntax.
const toggleCard = (id) => { // pass the id of the 'habit' to toggle
setHabits((habits) => { // the current 'habits' array is passed to the callback
// return a new array and avoid mutating nested objects when updating it
return habits.map((habit) => habit.id === id ? { ...habit, visible: !habit.visible } : habit);
});
};
// usage
{habits.map((habit) => (
...
<button type="button" onClick={() => toggleCard(habit.id)}>Toggle</button>
...
)}
The last glaring problem is your use of this which is necessary when working with a class based component, but isn't necessary in a function component and actually won't work at all in the context of an arrow function.
Here is a shortened example snippet that may help you working through these ideas.
const { useEffect, useState } = React;
const App = () => {
const [ habits, setHabits ] = useState([ // call useState to initialize 'habits' state
{
id: 1,
merit: 'good',
title: 'Good Habit',
count: 4,
text: 'Words to be hidden',
visible: false,
},
{
id: 2,
merit: 'bad',
title: 'Bad Habit',
count: 1,
text: 'Words to be hidden',
visible: false,
},
{
id: 3,
merit: 'good',
title: 'Good Habit',
count: 6,
text: 'Words to be hidden',
visible: true,
},
]);
useEffect(() => {
console.log('This: ', this);
}, []);
const toggleCard = (id) => { // id passed from mapped buttons
setHabits((habits) => { // the current 'habits' array is passed to the callback
// return a new array and avoid mutating nested objects when updating it
return habits.map((habit) => habit.id === id ? { ...habit, visible: !habit.visible } : habit);
});
};
return (
<div className="card-stack">
{habits.map((habit) => (
<div key={habit.id} className="card">
<h3>{habit.title}</h3>
{habit.visible
? (<p>{habit.text}</p>)
: null}
<button type="button" onClick={() => toggleCard(habit.id)}>Toggle</button>
</div>
))}
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
<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="root"></div>
Looks like you forgot to call useState?
{
id: 1,
merit: "good",
title: "Good Habit",
count: 4,
text: "Words to be hidden",
visible: false,
},
{
id: 2,
merit: "bad",
title: "Bad Habit",
count: 1,
text: "Words to be hidden",
visible: false,
},
{
id: 3,
merit: "good",
title: "Good Habit",
count: 6,
text: "Words to be hidden",
visible: true,
},
]);
As you have it:
{
id: 1,
merit: "good",
title: "Good Habit",
count: 4,
text: "Words to be hidden",
visible: false,
},
{
id: 2,
merit: "bad",
title: "Bad Habit",
count: 1,
text: "Words to be hidden",
visible: false,
},
{
id: 3,
merit: "good",
title: "Good Habit",
count: 6,
text: "Words to be hidden",
visible: true,
},
];
habits.map()
-->Uncaught TypeError: habits.map is not a function```
I am dynamically adding and removing Dropdowns based on the index of the array. I am setting the index using. When adding or removing a dropdown, I increment and decrement that index. The goal would be an array that looks something like this:
[
{ index: 0, type: "facebook" },
{ index: 1, type: "instagram" }
]
The problem is when I add a new dropdown, the index is incrementing by 2 or 3 instead of 1, resulting in the following output.
[
{ index: 0, type: "facebook" },
{ index: 2, type: "instagram" }
]
Here is the code for my component:
class SocialProfileComponent extends Component {
constructor(props) {
super(props);
let index = 0;
this.state = {
options: [
{value: 'email', label: 'Email'},
{value: 'instagram', label: 'Instagram'},
{value: 'linkedin', label: 'Linkedin'},
{value: 'pinterest', label: 'Pinterest'},
{value: 'skype', label: 'Skype'},
{value: 'tiktok', label: 'TikTok'},
{value: 'tumblr', label: 'Tumblr'},
{value: 'twitter', label: 'Twitter'},
{value: 'whatsapp', label: 'WhatsApp'},
{value: 'wordpress', label: 'Wordpress'},
{value: 'youtube', label: 'Youtube'},
{value: 'other', label: 'Other'},
],
socialDetails: [
{
index: index,
type: "",
link: "",
}
]
};
}
addNewRow = (e) => {
this.setState(prevState => ({
socialDetails: [
...prevState.socialDetails,
{
index: index++,
type: "",
link: "",
}
]
}));
}
deleteRow = (index) => {
this.setState({
socialDetails: this.state.socialDetails.filter(
(s) => index !== s.index
)
})
}
clickOnDelete(record) {
this.setState({
socialDetails: this.state.socialDetails.filter(r => r!==record)
})
}
componentDidMount() {
}
render = () => {
let {socialDetails, options} = this.state;
const {onInputChange} = this.props;
return (
<>
<SocialProfile
label={`Social profiles`}
addRow={this.addNewRow}
deleteRow={this.deleteRow}
socialDetails={socialDetails}
options={options}
onInputChange={onInputChange}
formKey={'socialprofiles'}
createKeyValue={this.createNewKeyValuePair}
placeholder={'Select'}
/>
</>
)
}
}
Code Sandbox
The behavior you are experiencing where the index increases by 2 or 3 is a result of React strict mode. In addition to other things, strict mode helps detect unexpected side effects to help you prepare your app for the upcoming concurrent mode feature. In concurrent mode, React will break up rendering into smaller chunks pausing and resuming work as necessary. This means that render phase lifecycle methods can be run more than once.
To help you prepare for this upcoming behavior of concurrent mode, strict mode will intentionally invoke render phase lifecycles twice to identify potential side effects. State updater functions are one instance of this, meaning that calling index++ in your state updater function will be run twice in strict mode.
The easiest solution would be to simply assign the new index to a variable before calling this.setState so that your state updater function is idempotent and can be called more than once.
addNewRow = (e) => {
const newIndex = ++this.index
this.setState((prevState) => ({
socialDetails: [
...prevState.socialDetails,
{
index: newIndex,
type: "",
link: ""
}
]
}));
};
i have the data structure like below,
const item = {
id: '1',
orders: [
{
id: '1',
title: 'order-1',
status: 'new',
startDate: '2020-08-13T00:00:00.000Z',
}
],
subItems: [
{
id: '1',
title: 'subitem-one',
status: 'new',
startDate: '2020-08-13T00:00:00.000Z',
orders: [
{
id: '1',
title: 'subitem1-order-one',
status: 'new',
},
{
id: '2',
title: 'subitem1-order-two',
status: 'new',
},
]
},
{
id: '2',
title: 'subitem-two',
status: 'new',
startDate: '2020-08-13T00:00:00.000Z',
orders: [
{
id: '2',
title: 'subitem2-order-one',
status: 'new',
},
},
I have to display each subitem name from above data in each card in a list.
below is how it should look in a list.
below is my code,
function Parent({items}: Props) {
//items is the same data as mentioned above
return (
//how should i map the items data to loop through each subitem and display it as in the picture above.
);
}
I am not sure how to loop through each subitem and display its name in a card using javascript and react.
could someone help me with this. thanks.
Ciao, assuming that you are using Card component from material ui you could do something like:
import { Card, CardHeader} from '#material-ui/core';
function Parent({items}: Props) {
//items is the same data as mentioned above
return (
items.subItems.map(el => {
<Card>
<CardHeader
title={el.title}
/>
</Card>
})
);
}
I'm assuming that Parent is a React functional component, so you can return the data in this way,
function Parent({items} : Props) {
return (
<div>
{
items.subItems.map(function (subItem) {
return <div>{subItem.title}</div>;
});
}
</div>
)
}
The template is shown below:
const Parent = () => {
const data = [...] // the data must be an array
return data.map(child => <Child data={child} />)
}
I have a parent component that contains two arrays
newChoiceArray: [
{ id: 1, text: '1', questionId: 'favourite number?', value: '1' },
{ id: 2, text: '2', questionId: 'favourite number?', value: '2' },
{ id: 3, text: '3', questionId: 'favourite number?', value: '3' },
],
ChoiceArray: [
{ id: 4, text: 'red', questionId: 'favourite colour?', value: '1' },
{ id: 5, text: 'blue', questionId: 'favourite colour?', value: '4' },
],
}
I have a component that renders a button for each object inside of an array
const MultiChoiceQuestions = props => {
const { multiChoiceArray, handleClick } = props
return (
<div>
{multiChoiceArray.map(questionChoice => {
return (
<button type="button" key={questionChoice.id} onClick={handleClick}>
{questionChoice.text}
</button>
)
})}
</div>
)
}
When I render my child component, I can pass an array as a prop
<MultiChoiceQuestions handleClick={this.testClick} multiChoiceArray={newChoiceArray} />
<MultiChoiceQuestions handleClick={this.testClick} multiChoiceArray={ChoiceArray} />
When I click the button, I want to see which object inside of that array is being selected.
testClick = event => {
event.preventDefault()
console.log(event.currentTarget)
}
Instead of outputting this <button type="button">3</button>
It should output this { id: 3, text: '3', questionId: 'favourite number?', value: '3' },
you can pass the object as button value, for example:
{multiChoiceArray.map(questionChoice => {
return (
<button type="button" key={questionChoice.id} value={JSON.stringify(questionChoice)} onClick={handleClick}>
{questionChoice.text}
</button>
)
}
and then you can catch this, using property value from currentTarget:
testClick = event => {
event.preventDefault()
console.log(JSON.parse(event.currentTarget.value))
}
I don't know if is the best approach, but I know that works.
UPDATE
I was wondering about this question, will you use something else from event object ? else you can pass directly the item into the function:
{multiChoiceArray.map(questionChoice => {
return (
<button type="button" key={questionChoice.id} onClick={() => handleClick(questionChoice)}>
{questionChoice.text}
</button>
)
}
// parent file
testClick = obj => {
console.log(obj)
}
I'm using the react-sortablejs library.
When trying to move cards within the list. I get the error:
Cannot read property 'map' of undefined
I have a dense structure and it gets lost here. How to handle onChange so that I can see in the console that the order of the notes within the list has changed.
Demo here
import Sortable from 'react-sortablejs';
// Functional Component
const SortableList = ({ items, onChange }) => {
return (
<div>
<Sortable
tag="ul"
onChange={(order, sortable, evt) => {
console.log(order)
onChange(order);
}}
>
{items.listItems.map(val => {
return <li key={uniqueId()} data-id={val}>List Item: {val.title}</li>})
}
</Sortable>
</div>
);
};
class App extends React.Component {
state = {
item: {
id: "abc123",
name: "AAA",
lists: [
{
id: "def456",
list_id: "654wer",
title: 'List1',
desc: "description",
listItems: [
{
id: "ghj678",
title: "ListItems1",
listItemsId: "88abf1"
},
{
id: "poi098",
title: "ListItems2",
listItemsId: "2a49f25"
},
{
id: "1oiwewedf098",
title: "ListItems3",
listItemsId: "1a49f25dsd8"
}
]
},
{
id: "1ef456",
list_id: "654wer",
title: 'List 2',
desc: "description",
listItems: [
{
id: "1hj678",
title: "ListItems4",
listItemsId: "18abf1"
},
{
id: "1oi098",
title: "ListItems5",
listItemsId: "1a49f25"
},
{
id: "1oiwewe098",
title: "ListItems6",
listItemsId: "1a49f25dsd"
}
]
},
{
id: "2ef456",
title: 'List 3',
list_id: "254wer",
desc: "description",
listItems: [
{
id: "2hj678",
title: "ListItems7",
listItemsId: "28abf1"
},
{
id: "2oi098",
title: "ListItems8",
listItemsId: "234a49f25"
},
{
id: "df098",
title: "ListItems9",
listItemsId: "1asd8"
}
]
}
]
}
};
render() {
const c = this.state.item['lists'].map(item => { return item.listItems});
return (
this.state.item['lists'].map(item => {
return (<div>
{item.title}
<SortableList
key={uniqueId()}
items={item}
onChange={(item) => {
console.log(item)
this.setState({item});
}}
>
</SortableList>
</div>)
})
)
}
};
Thanks in advance.
You have to update few changes in your code.
Update the SortableList function as below.
First pass data-id={val.id} in li and after that in onChange method you will receive the order with id. So based on that we are sorting the records.
const SortableList = ({ items, onChange }) => {
return (
<div>
<Sortable
tag="ul"
onChange={(order, sortable, evt) => {
items.listItems.sort(function(a, b){
return order.indexOf(a.id) - order.indexOf(b.id);
});
onChange(items);
}}
>
{items.listItems.map(val => {
return <li key={uniqueId()} data-id={val.id}>List Item: {val.title}</li>})
}
</Sortable>
</div>
);
};
Update the onChange event of App component.
onChange={(item) => {
let itemObj = {...this.state.item};
itemObj.lists.map(x=>{
if(x.id === item.id) x = item;
});
this.setState({itemObj});
}}
That's it!
Here is the working demo for you
https://stackblitz.com/edit/react-sortablejs-blzxwd
When remove the onChange event in the Sortable list, Its works.
const SortableList = ({ items, onChange }) => {
return (
<div>
<Sortable
tag="ul"
>
{items.listItems.map(val => {
return <li key={uniqueId()} data-id={val}>List Item: {val.title}</li>})
}
</Sortable>
</div>
);
};