Updating nested objects in nested arrays in React - javascript

Expected effect:
click button -> call function save -> pass object p to function update
update second object{a: 'purple', desc: 'grt', date: '12 -10-2019 '} in colors array, which is in theproducts array
Before update: {a: 'purple', desc: 'grt', date: '12 -10-2019 '}
After update: {a: 'violet', desc: 'gt', date: '12 -12-1980 '}
Error in console.log:
Uncaught TypeError: this.props.product.colors.map is not a function
App
class App extends Component {
constructor (props) {
super(props);
this.state = {
products: [
{
colors: [{a:'orange', desc: 'grtrt', date: '02-12-2019'}, {a:'purple', desc: 'grt', date: '12-10-2019'}]
desc: 'gfgfg',
},
{
colors: [{a:'black', desc: 'g', date: '12-12-2019'}, {a: 'white', {a:'black', desc: 'grtrt', date: '12-12-2119'}, }, {a:'gray', desc:'', date: '01-01-2000'}],
desc: 'gfgfgfg',
}
],
selectProductIndex: 0 //It is first object in products array
index: 1 //It is second object in colors array
}
}
update = (item) => {
const {selectProductIndex} = this.state;
this.setState(prevState => {
return {
products: [
...prevState.products.slice(0, selectProductIndex),
Object.assign({}, prevState.products[selectProductIndex], {colors: item}),
...prevState.products.slice(selectProductIndex + 1)
]
};
});
}
render () {
return (
<div>
<Items
product={this.state.products[this.state.selectProductIndex]}
update = {this.update}
/>
</div>
)
}
Items
class Items extends Component {
render () {
return (
<ul>
{
this.props.product.colors
.map((item, index) =>
<Item
key= {index}
index = {index}
time = {item}
update = {this.props.update}
/>
)
}
</ul>
</div>
);
}
}
Item
class Item extends Component {
save = () => {
const p = {
a:'violet', desc: 'gt', date: '12-12-1980'
}
this.props.update(p)
}
render() {
return (
<div>
<button onClick={this.save}>Save</button>
</div>
)
}
}

You need to pass the index of the colors item and then update it accordingly
class Item extends Component {
save = () => {
const p = {
a:'violet', desc: 'gt', date: '12-12-1980'
}
this.props.update(p, this.props.index)
}
render() {
return (
<div>
<button onClick={this.save}>Save</button>
</div>
)
}
}
and then in the topmost parent
update = (item, colorIndex) => {
const {selectProductIndex} = this.state;
this.setState(prevState => {
return {
products: [
...prevState.products.slice(0, selectProductIndex),
Object.assign({}, prevState.products[selectProductIndex], {colors: prevState.products[selectProductIndex].colors.map((it,idx) => {
if(idx === colorsIndex) { return item}
return it;
})}),
...prevState.products.slice(selectProductIndex + 1)
]
};
});
}
Working demo
const { Component } = React;
class App extends Component {
constructor (props) {
super(props);
this.state = {
products: [
{
colors: [{a:'orange', desc: 'grtrt', date: '02-12-2019'}, {a:'purple', desc: 'grt', date: '12-10-2019'}],
desc: 'gfgfg',
},
{
colors: [{a:'black', desc: 'g', date: '12-12-2019'}, {a:'black', desc: 'grtrt', date: '12-12-2119'}, {a:'gray', desc:'', date: '01-01-2000'}],
desc: 'gfgfgfg',
}
],
selectProductIndex: 0,
index: 1
}
}
update = (item, colorIndex) => {
const {selectProductIndex} = this.state;
this.setState(prevState => {
return {
products: [
...prevState.products.slice(0, selectProductIndex),
Object.assign({}, prevState.products[selectProductIndex], {colors: prevState.products[selectProductIndex].colors.map((it,idx) => {
if(idx === colorIndex) { return item}
return it;
})}),
...prevState.products.slice(selectProductIndex + 1)
]
};
});
}
render () {
return (
<div>
<Items
product={this.state.products[this.state.selectProductIndex]}
update = {this.update}
/>
</div>
)
}
}
class Items extends Component {
render () {
return (
<ul>
{
this.props.product.colors
.map((item, index) =>
<Item
key= {index}
index = {index}
time = {item}
update = {this.props.update}
/>
)
}
</ul>
);
}
}
class Item extends Component {
save = () => {
const p = {
a:'violet', desc: 'gt', date: '12-12-1980'
}
this.props.update(p, this.props.index)
}
render() {
return (
<div>
<pre>{JSON.stringify(this.props.time)}</pre>
<button onClick={this.save}>Save</button>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('app'));
<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="app" />

Related

React doesn't see mapped props

I have this message in browser:
TypeError: data.map is not a function
I am passing an array from another component here as props. What I am doing wrong?
Thank you in advance!
EDITED:
event-data.js
const months = ['January', 'February', 'March'];
const eventType = ['Party', 'Karaoke', 'Concert'];
const monthObject = [
{ id: 'sort-by-month' },
{ name: 'By month' },
{ values: months },
];
const eventObject = [
{ id: 'sort-by-category' },
{ name: 'By category' },
{ values: eventType },
];
const eventData = { monthObject, eventObject };
event-filter-bar.js
import eventData from '../../data/event-data';
class EventFilterBar extends React.Component {
render() {
return (
<FilterToolbar data={eventData} />
);
}
}
filter-toolbar.js
class FilterToolbar extends Component {
render() {
const { data } = this.props;
return (
<ButtonToolbar className="justify-content-center">
<DropdownMaker data={data} />
<DropdownWithDate />
<ResetButton />
</ButtonToolbar>
);
}
}
FilterToolbar.propTypes = {
data: PropTypes.array.isRequired,
};
dropdown-maker.js
class DropdownMaker extends Component {
render() {
const { data } = this.props;
const eventFilters = data.map((e) => (
<DropdownMenu
id={e.id}
name={e.name}
values={e.values}
key={e.id}
/>
));
return (
{ eventFilters }
);
}
}
DropdownMaker.propTypes = {
data: PropTypes.array.isRequired,
};
Check if the data is actually populated or not before map through it.
class DropdownMaker extends Component {
render() {
const { data } = this.props;
const eventFilters = (data && data.length > 0) && data.map((e) => (
<DropdownMenu
id={e.id}
name={e.name}
values={e.values}
key={e.id} //<-- don't forget to add a unique key prop while use loop
/>
));
return (
{ eventFilters }
);
}
}
DropdownMaker.propTypes = {
data: PropTypes.array.isRequired,
};
Feel free to comment if it's not working.

Adding clicked items to new array in React

I am making API calls and rendering different components within an object. One of those is illustrated below:
class Bases extends Component {
constructor() {
super();
this.state = {
'basesObject': {}
}
}
componentDidMount() {
this.getBases();
}
getBases() {
fetch('http://localhost:4000/cupcakes/bases')
.then(results => results.json())
.then(results => this.setState({'basesObject': results}))
}
render() {
let {basesObject} = this.state;
let {bases} = basesObject;
console.log(bases);
//FALSY values: undefined, null, NaN, 0, false, ""
return (
<div>
{bases && bases.map(item =>
<button key={item.key} className="boxes">
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
)}
</div>
)
}
}
The above renders a set of buttons. All my components look basically the same.
I render my components here:
class App extends Component {
state = {
ordersArray: []
}
render() {
return (
<div>
<h1>Bases</h1>
<Bases />
<h1>Frostings</h1>
<Frostings />
<h1>Toppings</h1>
<Toppings />
</div>
);
}
}
I need to figure out the simplest way to, when a button is clicked by the user, add the key of each clicked element to a new array and I am not sure where to start. The user must select one of each, but is allowed to select as many toppings as they want.
Try this
We can use the same component for all categories. All the data is handled by the parent (stateless component).
function Buttons({ list, handleClick }) {
return (
<div>
{list.map(({ key, name, price, isSelected }) => (
<button
className={isSelected ? "active" : ""}
key={key}
onClick={() => handleClick(key)}
>
<span>{name}</span>
<span>${price}</span>
</button>
))}
</div>
);
}
Fetch data in App component, pass the data and handleClick method into Buttons.
class App extends Component {
state = {
basesArray: [],
toppingsArray: []
};
componentDidMount() {
// Get bases and toppings list, and add isSelected attribute with default value false
this.setState({
basesArray: [
{ key: "bases1", name: "bases1", price: 1, isSelected: false },
{ key: "bases2", name: "bases2", price: 2, isSelected: false },
{ key: "bases3", name: "bases3", price: 3, isSelected: false }
],
toppingsArray: [
{ key: "topping1", name: "topping1", price: 1, isSelected: false },
{ key: "topping2", name: "topping2", price: 2, isSelected: false },
{ key: "topping3", name: "topping3", price: 3, isSelected: false }
]
});
}
// for single selected category
handleSingleSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => ({
...item,
isSelected: item.key === key
}))
}));
};
// for multiple selected category
handleMultiSelected = type => key => {
this.setState(state => ({
[type]: state[type].map(item => {
if (item.key === key) {
return {
...item,
isSelected: !item.isSelected
};
}
return item;
})
}));
};
// get final selected item
handleSubmit = () => {
const { basesArray, toppingsArray } = this.state;
const selectedBases = basesArray.filter(({ isSelected }) => isSelected);
const selectedToppings = toppingsArray.filter(({ isSelected }) => isSelected);
// submit the result here
}
render() {
const { basesArray, toppingsArray } = this.state;
return (
<div>
<h1>Bases</h1>
<Buttons
list={basesArray}
handleClick={this.handleSingleSelected("basesArray")}
/>
<h1>Toppings</h1>
<Buttons
list={toppingsArray}
handleClick={this.handleMultiSelected("toppingsArray")}
/>
</div>
);
}
}
export default App;
CSS
button {
margin: 5px;
}
button.active {
background: lightblue;
}
I think the following example would be a good start for your case.
Define a handleClick function where you can set state with setState as the following:
handleClick(item) {
this.setState(prevState => {
return {
...prevState,
clickedItems: [...prevState.clickedItems, item.key]
};
});
}
Create an array called clickedItems in constructor for state and bind handleClick:
constructor() {
super();
this.state = {
basesObject: {},
clickedItems: [],
}
this.handleClick = this.handleClick.bind(this);
}
You need to add a onClick={() => handleClick(item)} handler for onClick:
<button key={item.key} className="boxes" onClick={() => handleClick(item)}>
{/* <p>{item.key}</p> */}
<p>{item.name}</p>
<p>${item.price}.00</p>
{/* <p>{item.ingredients}</p> */}
</button>
I hope that helps!

Show highest score from list

I have an array in this.state. Array has objects having 2 keys: [text] and [score].
I need to display [text] key only of object with highest score.
e.g.
[{
text: 'foo',
score: 1,
},
{
text: 'bar',
score: 0.1,
}]
Here highest score is 1 with [text] key value as "foo".
So just render "foo".
Code:
class Score extends React.Component {
constructor(props) {
super(props);
this.state =
{
text: [
{
title: 'el',
data: [
{
el: 'hello',
score: 1
},
{
el: 'hallo',
score: 0.10
}
]
},
]
}
}
render() {
return (
<div>
{
this.state.text.map((q, i) => (
<div key={i} className="card">
{
q.data.map((i, j) =>
(
<div key={j}>
<p>{i.el}</p>
<p>{i.score}</p>
</div>
)
)}
</div>
))
}
</div>
);
}
}
const Root = document.getElementById("root");
ReactDOM.render(<Score />, Root);
Array#map will display every element, not just that one with highest score.
Inside map you should drop some logic which will find the object with highest score and display it.
this.state.text.map((q, i) => {
const r = q.data.sort((a, b) => b.score - a.score)[0];
// ^^^^^^^^^^^^^^^^^^^ sort ascending and pick the first element
return (
<div key={i} className="card">
<p>{r.el}</p>
<p>{r.score}</p>
</div>
);
})
const r = [{ text: 'hello', score: 1 }, { text: 'hallo', score: 0.1 }];
const res = r.sort((a, b) => b.score - a.score)[0];
console.log(res);
you can use reduce and find the highest before rendering.
const maxObject = this.state.text.reduce(function(prev, current) {
return (prev.score > current.score) ? prev : current
},this.state.text[0])
and then you can use this maxObject in your jsx.
you need to sort the values, but your structure is such that you need to search inside the first object of your text array...
relevant js:
import React from "react";
class Score extends React.Component {
constructor(props) {
super(props);
const dataForArray = [
{
title: "el",
data: [
{
el: "hello",
score: 1
},
{
el: "hallo",
score: 1.1
},
{
el: "hallo",
score: 0.1
}
]
}
];
let dataToSort = dataForArray[0].data;
if (dataToSort) {
const highestVal = dataToSort.sort((a, b) => b.score - a.score )[0];
console.log("highest:", highestVal);
}
this.state = { text: dataForArray, highest:highestVal };
}
render() {
return (
<div>
{this.state.text.map((q, i) => (
<div key={i} className="card">
{q.data.map((i, j) => (
<div key={j}>
<p>{i.el}</p>
<p>{i.score}</p>
</div>
))}
</div>
))}
<hr/>
Highest value is: <b> {this.state.highest.el} - {this.state.highest.score} </b>
</div>
);
}
}
export default Score;
complete working stackblitz here

Add items to page with localstorage

the array is stored in local storage by clicking on the “More” button 2 news is added to DOM
How should I implement this?
const ARR= [
{
id: 1,
Name: 'Name1',
text:'lorem ipsum'
},
{
id: 2,
Name: 'Name2',
text:'lorem ipsum'
},
{
id: 3,
Name: 'Name3',
text:'lorem ipsum'
},
{
id: 4,
Name: 'Name4',
text:'lorem ipsum'
},
];
10 obj
here we save the array in localStorage
and where to execute its JSON.parse (localStorage.getItem ('news')) and I don’t understand how to implement work with local storage by click
```
localStorage.setItem('news', JSON.stringify(ARR));
class NewsOne extends PureComponent {
constructor() {
super();
this.state = {
listItem: ARR,
formAdd: false,
};
this.createItem = this.createItem.bind(this);
this.updateItem = this.updateItem.bind(this);
this.removeItem = this.removeItem.bind(this);
this.addForm = this.addForm.bind(this);
}
updateItem(item) {
const { listItem } = this.state;
this.setState({
listItem: listItem.map(elem => (
elem.id === item.id ? item : elem
))
});
}
removeItem(itemId) {
const { listItem } = this.state;
this.setState({
listItem: listItem.filter(item => item.id !== itemId)
});
}
createItem(item) {
const { listItem } = this.state;
this.setState({
listItem: [item, ...listItem],
});
}
addForm() {
const { formAdd } = this.state;
this.setState({
formAdd: !formAdd,
})
}
render() {
const { listItem, formAdd } = this.state;
return(
<>
<div className="box">
<Title />
<List
data={listItem}
removeFromProps={this.removeItem}
updateFromProps={this.updateItem}
/>
</div>
<button className ="addnews" onClick = {this.addForm}>
Add
</button>
</>
);
}
}
A class that defaultly displays 2 elements on page.
I tried here to interact with the array from localStorage, but it fails
class List extends PureComponent {
constructor() {
super();
this.state = {
count: 2,
}
this.addObj = this.addObj.bind(this);
}
addObj() {
const { count } = this.state;
this.setState({
count: count + 2,
});
}
render() {
const { count } = this.state;
const { data } = this.props;
return(
<>
<ul>
{
data.slice(0, count).map(item => (
<>
<Item
key={item.id}
item={item}
/>
</>
))
}
</ul>
<button className ="addnews" onClick = {this.addObj}>
More
</button>
</>
);
}
}

How to handle recursive array mapping with React components?

Here is a sample function called index() that I use to return an index of headers from a Markdown file. As you can see, with only 3 levels of headers, its quickly getting out of hand.
I tried a variety recursion ideas, but ultimately failed at all attempts to compile in create-react-app.
Could anyone offer any guidance as to how to clean this up and maybe even allow for an infinite/greater levels of nesting?
An array is stored in this.state.index which resembles the array below.
[
{
label: 'Header title',
children: [
{
label: 'Sub-header title'
children: [...]
}
]
}
]
Then that data is used in the function below to generate a ul index.
index() {
if ( this.state.index === undefined || !this.state.index.length )
return;
return (
<ul className="docs--index">
{
this.state.index.map((elm,i=0) => {
let key = i++;
let anchor = '#'+elm.label.split(' ').join('_') + '--' + key;
return (
<li key={key}>
<label><AnchorScroll href={anchor}>{elm.label}</AnchorScroll></label>
<ul>
{
elm.children.map((child,k=0) => {
let key = i+'-'+k++;
let anchor = '#'+child.label.split(' ').join('_') + '--' + key;
return (
<li key={key}>
<label><AnchorScroll href={anchor}>{child.label}</AnchorScroll></label>
<ul>
{
child.children.map((grandchild,j=0) => {
let key = i+'-'+k+'-'+j++;
let anchor = '#'+grandchild.label.split(' ').join('_') + '--' + key;
return (
<li key={key}>
<label><AnchorScroll href={anchor}>{grandchild.label}</AnchorScroll></label>
</li>
);
})
}
</ul>
</li>
);
})
}
</ul>
</li>
);
})
}
</ul>
);
}
Like I said, this is a mess! I'm new to React and coding in general so sorry if this is a silly question.
Seems like you want a recursive call of a component.
For example, component Item will render it self based on a condition (existence of the children array).
Running example:
const data = [
{
label: 'Header title',
children: [
{
label: 'Sub-header title',
children: [
{ label: '3rd level #1' },
{
label: '3rd level #2',
children: [
{ label: 'Level 4' }
]
}
]
}
]
}
]
class Item extends React.Component {
render() {
const { label, children } = this.props;
return (
<div>
<div>{label}</div>
<div style={{margin: '5px 25px'}}>
{children && children.map((item, index) => <Item key={index} {...item} />)}
</div>
</div>
)
}
}
const App = () => (
<div>
{data.map((item, index) => <Item key={index} {...item} />)}
</div>
);
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Edit
Just for completeness, i added another example but this time we handle the isOpen property outside the Item's local component state, in a higher level.
This can be easily moved to a redux reducer or any other state management library, or just let a higher level component like the App in this case manage the changes.
So, to handle changes of a recursive component you would probably write a recursive handler:
const data = [
{
label: 'Header title',
id: 1,
children: [
{
label: 'Sub-header title',
id: 2,
children: [
{ label: '3rd level #1', id: 3 },
{
label: '3rd level #2',
id: 4,
children: [
{ label: 'Level 4', id: 5 }
]
}
]
},
]
},
{
label: 'Header #2',
id: 6,
children: [
{ label: '2nd level #1', id: 7, },
{
label: '2nd level #2',
id: 8,
children: [
{ label: 'Level 3', id: 9 }
]
}
]
}
]
class Item extends React.Component {
toggleOpen = e => {
e.preventDefault();
e.stopPropagation();
const {onClick, id} = this.props;
onClick(id);
};
render() {
const { label, children, isOpen, onClick } = this.props;
return (
<div className="item">
<div
className={`${children && "clickable"}`}
onClick={children && this.toggleOpen}
>
<div
className={`
title-icon
${isOpen && " open"}
${children && "has-children"}
`}
/>
<div className="title">{label}</div>
</div>
<div className="children">
{children &&
isOpen &&
children.map((item, index) => <Item key={index} {...item} onClick={onClick} />)}
</div>
</div>
);
}
}
class App extends React.Component {
state = {
items: data
};
toggleItem = (items, id) => {
const nextState = items.map(item => {
if (item.id !== id) {
if (item.children) {
return {
...item,
children: this.toggleItem(item.children, id)
};
}
return item;
}
return {
...item,
isOpen: !item.isOpen
};
});
return nextState;
};
onItemClick = id => {
this.setState(prev => {
const nextState = this.toggleItem(prev.items, id);
return {
items: nextState
};
});
};
render() {
const { items } = this.state;
return (
<div>
{items.map((item, index) => (
<Item key={index} {...item} onClick={this.onItemClick} />
))}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.item {
padding: 0 5px;
}
.title-icon {
display: inline-block;
margin: 0 10px;
}
.title-icon::before {
margin: 12px 0;
content: "\2219";
}
.title-icon.has-children::before {
content: "\25B6";
}
.title-icon.open::before {
content: "\25E2";
}
.title-icon:not(.has-children)::before {
content: "\2219";
}
.title {
display: inline-block;
margin: 5px 0;
}
.clickable {
cursor: pointer;
user-select: none;
}
.open {
color: green;
}
.children {
margin: 0 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I don't have good test data, but try this out:
index() {
if ( this.state.index === undefined || !this.state.index.length ) {
return null;
}
const sections = this.state.index.map(this.handleMap);
return (
<ul>
{sections}
</ul>
)
}
handleMap(elm, i) {
const anchor = '#'+elm.label.split(' ').join('_') + '--' + i;
return (
<li key={i}>
<label><AnchorScroll href={anchor}>{elm.label}</AnchorScroll></label>
<ul>
{
elm.children.map(this.handleMapChildren)
}
</ul>
</li>
);
}
handleMapChildren(child, i) {
let anchor = '#'+child.label.split(' ').join('_') + '--' + i;
return (
<li key={i}>
<label><AnchorScroll href={anchor}>{child.label}</AnchorScroll></label>
<ul>
{
child.children.map(this.handleMapChildren)
}
</ul>
</li>
);
}

Categories

Resources