I am trying to recursively render JSON data to nested list using React. Right now I am using simple data object like this:
[{"id": "1",
"name": "Luke"
},
{"id": "2",
"name": "Jim",
"childNodes":[{
"id": "3",
"name": "Lola"
}]
}]
using this class:
export default class NestedList extends Component {
constructor(props) {
super(props);
this.state = {
visible: true
};
}
toggle = () => {
this.setState({ visible: !this.state.visible });
};
renderChild = (child) => {
if (child.childNodes) {
return (
<ul>
{child.myData.map(item => {
return this.renderChild(item);
})}
</ul>
);
}
else if (child.name) {
return <input type="checkbox"><Child name={child.name}/></input>;
}
return null;
}
render() {
return (
<aside>
<div>
<h4>Data Sets</h4>
<ul>
{this.renderChild(this.props.myData)}
</ul>
</div>
</aside>
);
}
}
which calls a Child class that creates list element:
export default class Child extends Component {
render() {
let {name}=this.props;
return (
<li>{name}</li>
);
}
}
but it doesn't print anything. I have tried removing attribute childNodes altogether and tried to print the list but it doesn't work still. I don't understand where I am doing wrong. I would appreciate some help regarding how to fix this.
You need to map through myData first so the rendering process begins:
<ul>
{this.props.myData.map(data => this.renderChild(data))}
</ul>
Also, on childNodes you need to loop through child.childNodes:
if (child.childNodes) {
return (
<ul>
{child.childNodes.map(node => this.renderChild(node))}
</ul>
);
}
there were couple of issues here:
You passed myData to renderChild which doesn't hold childNodes
property nor name property. Hence none of the conditions were met
(null was returned).
So maybe you should loop through myData and
pass each member of the array to renderChild.
Even if we will pass a valid "child" to the renderChild method,
inside this condition:
if (child.childNodes) {
Again you are using a wrong property:
<ul>
{child.myData.map(item => {
return this.renderChild(item);
})}
</ul>
this should be:
{child.childNodes.map(item => {...
Last thing, You can't nest child elements inside an input element.
so change the layout, maybe like this? :
<input type="checkbox"/>
<Child name={child.name} />
Here is a running example with your code:
const data = [
{
id: "1",
name: "Luke"
},
{
id: "2",
name: "Jim",
childNodes: [
{
id: "3",
name: "Lola"
}
]
}
];
class NestedList extends React.Component {
constructor(props) {
super(props);
this.state = {
visible: true
};
}
toggle = () => {
this.setState({ visible: !this.state.visible });
};
renderChild = child => {
if (child.childNodes) {
return (
<ul>
{child.childNodes.map(item => {
return this.renderChild(item);
})}
</ul>
);
} else if (child.name) {
return (
<div>
<input type="checkbox"/>
<Child name={child.name} />
</div>
);
}
return null;
};
render() {
return (
<aside>
<div>
<h4>Data Sets</h4>
<ul>{this.props.myData.map(item => this.renderChild(item))}</ul>
</div>
</aside>
);
}
}
class Child extends React.Component {
render() {
let { name } = this.props;
return <li>{name}</li>;
}
}
ReactDOM.render(<NestedList myData={data} />, 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>
Related
I have two classes. One holds the array, the other holds the array props. These are my classes:
//PARENT CLASS:
constructor() {
super()
this.state = {
items: []
}
this.addItem = this.addItem.bind(this)
}
componentDidMount(){
this.setState({
items: [{
name: 'Sebastian',
num: '001'
},{
name: 'Josh',
num: '002'
}]
})
}
addItem() {
??????
}
render() {
return(
<div>
<MethodA items={this.state.items} addItem={this.addItem}/>
</div>
)
}
//CHILD CLASS:
function MethodA(props) {
return(
<div>
{props.items.map((item, i) =>{
return(<div key={i}>
<span>{item.name}</span>
<span>{item.num}</span>
</div>)
})}
<button onClick={() => { props.addItem() }}>ADD ITEM</button>
</div>
)
}
Current result is like this:
<div>
<span>Sebastian</span>
<span>001</span>
</div>
<div>
<span>Sebastian</span>
<span>002</span>
</div>
Then after the "ADD ITEM" button was hit, this will be the new result:
<div>
<span>Sebastian</span>
<span>001</span>
</div>
<div>
<span>Sebastian</span>
<span>002</span>
</div>
<div>
<span>New Name</span>
<span>New Num</span>
</div>
I'm not sure whether what and how to use between push() or concat() or both. Any ideas?
Firstly, there's no need to set the initial state in componentDidMount, you can do it directly in constructor.
constructor(props) {
super(props);
this.state = {
items: [
{
name: "Sebastian",
num: "001"
},
{
name: "Josh",
num: "002"
}
]
};
this.addItem = this.addItem.bind(this);
}
To add an item you can use functional form of setState and you'll need to pass that item into callback from the child component.
addItem(item) {
this.setState(state => ({
items: [...state.items, item]
}));
}
// Child class
function MethodA(props) {
return(
<div>
{props.items.map((item, i) =>{
return(<div key={i}>
<span>{item.name}</span>
<span>{item.num}</span>
</div>)
})}
<button onClick={() => props.addItem(item)}>ADD ITEM</button> // Pass item to the parent's method
</div>
)
}
Here's the deal. The difference between push() and concat() is in immutability.
If you use push on an array, it will mutate the original array and add a new value to that array (wrong).
If you use concat, it will create a new array for you, leaving the old array untouched (correct).
So you might want to do something along these lines:
addItem(item)
this.setState(state => {
const items = state.items.concat(item);
return {
items,
};
});
}
class Services extends Component {
constructor(props) {
super(props);
this.state = {showoffer: false};
}
showOffers=( )=>{
this.setState({showoffer: !this.state.showoffer});
}
render() {
return (
<div className="OSServicesContainer">
<img className="OSlogomark" src={logomark} alt="logo mark" />
<article className="OssHeadingText">OOM INTERIORS OFFERS</article>
{offersdata.map((offers,index)=>{
return ( <div key={index} className="OssoffersContainermain">
<div className="OssoffersContainer">
<div className="OssofferHeadingmain">
<article className="OssofferHeading">{offers.heading}</article>
</div>
<article className="OssofferText">{offers.subheading}</article>
<div className="OssofferViewbtnmain">
<article key={index} className="OssofferViewbtn" onClick={this.showOffers}>{this.state.showoffer?"View Less":"View More"}</article>
</div>
</div>
{!this.state.showoffer?
null:
<div className="OssOfferSubCompmain">
{offers.offersub.map((offer,key) =>{
return <OssOfferSubComp ofrtext={offer.text} ofrsubtext={offer.subtext} />
})}
</div>}
</div>
)
})}
</div>);
}
}
export default Services;
Above is my code
i want to call showoffer function and update only that element clicked
please what shall i do it is triggering all elements
how to trigger single element??
You can try something like this:
`class Services extends Component {
constructor(props) {
super(props);
this.state = {showoffer: 0};
}
showOffers = ( offerIndex ) => {
this.setState({showoffer: offerIndex});
}
hideOffers = () => {
this.setState({showoffer: 0});
}
render() => {
...
<div className="OssofferViewbtnmain">
<article key={index} onClick={ () => this.showOffers(index) }>
{this.state.showoffer?"View Less":"View More"}
</article>
</div>
...
{
this.state.showOffer && this.state.showOffer === index
? // then show it
: ''
}
}`
Hey if you wish to have multiple items open at the same time you can do something like this where you mutate the mapped item to track show hide state. I have added a visible property to the list item that keeps track if the item is open or closed:
import React, { Component } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends Component {
state = {
items: [
{ header: "Test 1", extra: "Some extra content A" },
{ header: "Test 2", extra: "Some extra content B" },
{ header: "Test 3", extra: "Some extra content C" }
]
};
onItemClick(index) {
const selected = this.state.items[index];
this.setState({
items: [
...this.state.items.slice(0, index),
{ ...selected, visible: !selected.visible },
...this.state.items.slice(index + 1)
]
});
}
render() {
return (
<div>
<ul>
{this.state.items.map((item, index) => {
return (
<li
key={index}
style={{ cursor: "pointer" }}
onClick={() => this.onItemClick(index)}
>
<h3>{item.header}</h3>
{item.visible ? <div>{item.extra}</div> : null}
</li>
);
})}
</ul>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
https://codesandbox.io/s/busy-germain-hdmrn
I'm having an issue re-rendering the DOM after a state change, Here is the code for the component:
<ul>
{ this.state.musicLibraryItems }
</ul>
I have a filter function that takes in a value from a text input and and filters the musicLibraryItems based on if the title has the value from the text input, I am doing that in the following function:
filterMusicLibrary(value) {
let musicLibraryItems = [];
this.state.musicGet.map(music => {
if(music.title.includes(value)) {
console.log(music.title)
musicLibraryItems.push(
<MusicLibraryItem key= {music.id} music={music} history={this.props.history}/>
);
}
});
console.log(musicLibraryItems, this.state.musicLibraryItems)
this.setState((prevState) => {
return {
musicLibraryItems: musicLibraryItems
})
}
})
}
I think the problem is that I am not changing the reference to the state variable so it isn't realizing that the state has been changed. I was wondering what the best way to mutate the state variable so that the values in the musicLibraryItems is put into this.state.musicLibraryItems so that it can be rendered on the page.
you could try
<ul>
{ this.filterMusicLibrary() }
</ul>
and change the filter to not change the state
filterMusicLibrary() {
let musicLibraryItems = [];
this.state.musicGet.map(music => {
if(music.title.includes(this.state.title)) {
console.log(music.title);
musicLibraryItems.push(
<MusicLibraryItem key= {music.id} music={music} history={this.props.history}/>
);
}
});
console.log(musicLibraryItems, this.state.musicLibraryItems);
return musicLibraryItems;
}
You shouldn't store jsx elements on your state. If you are concerned about re-calculation of the list, try reselect
More like what #Murilo already mentioned, for filters you wouldnt want to add them to state, is more calculated state...just added a working sample
const MusicLibraryItem = ({music}) => {
return <li>{music.title}</li>
}
const lotMusic = [{title: 'Awesome', id: '12121'}, {title: 'Kitchen', id: '121'},
{title: 'Golden', id: '21'}, {title: 'Beach', id: '121'}];
class App extends React.Component{
constructor(){
super()
this.state = {musicGet: lotMusic,filterValue: ''}
this.updateFilter = this.updateFilter.bind(this);
}
updateFilter(event) {
const name = event.target.name;
this.setState({[name]: event.target.value});
}
filterMusicLibrary(value) {
let musicLibraryItems = [];
this.state.musicGet.map(music => {
if(music.title.includes(value)) {
console.log(music.title)
musicLibraryItems.push(
<MusicLibraryItem key= {music.id} music={music} history={this.props.history}/>
);
}
});
return musicLibraryItems;
}
render(){
return (
<div>
<h2>Test </h2>
<input type="text" onChange={this.updateFilter}
name="filterValue"
value={this.state.filterValue}/>
<ul>{this.filterMusicLibrary(this.state.filterValue)}</ul>
</div>
)
}
}
ReactDOM.render(<App/>, document.getElementById('app'))
<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="app">
</div>
I'm trying to use data from an API (https://messi.hyyravintolat.fi/publicapi/restaurant/11/) in my React project. I was able to render each "date" from the API, but how do I render each "name" for each "date" in with this kind of .json (the ones immediately inside the "data" arrays in the API)? {item.data[name]} doesn't seem to be the way to do this. Here is the component class I'm using to get and render the data:
import React from 'react';
/*eslint-env jquery*/
class TestMenu extends React.Component {
constructor(props) {
super(props);
this.state = { food: [] };
}
componentDidMount() {
this.UserList();
}
UserList() {
$.getJSON('https://messi.hyyravintolat.fi/publicapi/restaurant/11/')
.then(({ data }) => this.setState({ food: data }));
}
render() {
const foods = this.state.food.map((item, i) => (
<div>
<h1>{item.date}</h1>
<p>{item.data[name]}</p>
</div>
));
return (
<div id="layout-content" className="layout-content-wrapper">
<div className="panel-list">{foods}</div>
</div>
);
}
}
export default TestMenu;
After looking at the datasource, the JSON is in the following shape:
[
{
"date": some date,
"data": [
{
"name": some name,
...
},
{
"name": some other name,
...
}
...
]
},
{
"date": some other date,
"data": [ ... ]
},
...
]
So there are several names for a single date. You could render this like the following:
<h1>{item.date}</h1>
<ul>
{item.data.map((d, idx) => {
return <li key={idx}>{d.name}</li>
)}
</ul>
Note that I also use the indices when mapping the data in order to provide a unique key to React for each <li> element.
That's because you need a second loop as data (the nested one) is an array and not an object.
A simple example of looping with your data structure:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
list: []
};
}
componentDidMount() {
axios
.get("https://messi.hyyravintolat.fi/publicapi/restaurant/11/")
.then(({ data }) => {
this.setState({
list: data.data
});
});
}
render() {
const { list } = this.state;
return (
<div>
<ul>
{
list && list.map((obj) => {
return (
<li className="item">
<div className="date">
<span>Date: </span> <span>{obj.date}</span>
</div>
<div className="names">
{
obj.data.map((obj2) => {
return (
<div className="name">
<span>Name: </span> <span>{obj2.name}</span>
</div>
)
})
}
</div>
</li>
)
})
}
</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
.item {
list-style: none;
border: 1px solid #ccc;
padding: 10px;
}
.date {
font-weight: bold;
}
.names {
text-indent: 15px;
}
.name{
margin: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script>
<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 have a simple todolist structure into the state of component. This is an array with two field called 'content' and one called 'done'. I catch click of row item with a simple onClick() function passed from parent to TodoItem childs (like React tutorial suggest):
render() {
const tthis = this;
var myList = tthis.state.todos.map(function(todo,index){
var myOnCLick = function(){
var newTodo = {content: todo.content, done: !todo.done};
tthis.state.todos[index] = newTodo;
tthis.setState({});
}
return <Todo key={index} todo={todo} onClick={myOnCLick}/>
})
return (
<ul className="list">
{ myList }
</ul>
)
}
This code work without problems, but I don't like so much.
I would find some good solution to change a value of single item in an array. I found that in Immutability helper of React DOC:
{$set: any} replace the target entirely.
And in a good answer in this forum I saw an example:
this.setState({
todos: update(this.state.todos, {1: {done: {$set: true}}})
But I cannot use 1 in my case. I have index which give me the index of clicked todo in todos list.
You can flip the value of the done property on the individual todo object listed in the array in the following way:
flipDone(id) {
let index = Number(id);
this.setState({
todos: [
...this.state.todos.slice(0, index),
Object.assign({}, this.state.todos[index], {done: !this.state.todos[index].done}),
...this.state.todos.slice(index + 1)
]
});
}
Here is a demo: http://codepen.io/PiotrBerebecki/pen/jrdwzB
Full code:
class TodoList extends React.Component {
constructor() {
super();
this.flipDone = this.flipDone.bind(this);
this.state = {
todos: [
{content: 'Go shopping', done: false},
{content: 'Walk the dog', done: false},
{content: 'Wash the dishes', done: false},
{content: 'Learn React', done: false}
]
};
}
flipDone(id) {
let index = Number(id);
this.setState({
todos: [
...this.state.todos.slice(0, index),
Object.assign({}, this.state.todos[index], {done: !this.state.todos[index].done}),
...this.state.todos.slice(index + 1)
]
});
}
render() {
const myList = this.state.todos.map((todo, index) => {
return (
<Todo key={index}
clickHandler={this.flipDone}
content={todo.content}
done={todo.done}
id={index}
/>
);
})
return (
<ul className="list">
{myList}
</ul>
);
}
}
class Todo extends React.Component {
constructor() {
super();
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
this.props.clickHandler(event.target.id);
}
render() {
return (
<li>
<button onClick={this.handleClick}
id={this.props.id}>
Click me
</button> ---
{String(this.props.done)} ---
{this.props.content}
</li>
);
}
}
ReactDOM.render(
<TodoList />,
document.getElementById('app')
);