Open and Close a recursively nested list in React js - javascript

I'm looking to be able to open/close a nested list using React, so that when you click on the parent li the children are hidden? Here's what I used to create the list.
list.js
class List extends React.Component {
render(){
return (
<ul>
{this.props.list.map(function(item){
return (
<li>
<ListItem key={item.id} item={item} />
<List list={item.children} />
</li>
);
})}
</ul>
);
}
list-item.js
class ListItem extends React.Component {
handleCollapse(){
console.log('Open/Close: ' + this.props.item.display_name);
return false;
}
handleFilter(){
console.log('Filter id: ' + this.props.item.id);
return false;
}
render(){
return (
<div>
<a rel="{this.props.item.id}" onClick={this.handleCollapse.bind(this)}>
{this.props.item.display_name}
</a>
<input value="" type="checkbox" onClick={this.handleFilter.bind(this)} />
</div>
)
}

Are you trying to simulate accordion behaviour? If yes then you can modify your code like this. Use component's state and toggle it to open and close children. Instead of creating <List list={item.children} /> in List class, import(or use require) list.js in list-item.js and render the child List item on the basis of current ListItem's state.
list-item.js
class ListItem extends React.Component {
//Add this
constructor (){
super(...arguments);
this.state = { showChild:false};
}
handleCollapse(){
console.log('Open/Close: ' + this.props.item.display_name);
//Add this
this.setState({showChild:!this.state.showChild});
return false;
}
handleFilter(){
console.log('Filter id: ' + this.props.item.id);
return false;
}
render(){
let children;
if(this.state.showChild) {
children = (<List list={this.props.item.children} />);
}
return (
<div>
<a rel="{this.props.item.id}" onClick={this.handleCollapse.bind(this)}>
{this.props.item.display_name}
</a>
<input value="" type="checkbox" onClick={this.handleFilter.bind(this)} />
//Add this
{children}
</div>
)
};
}
list.js
class List extends React.Component {
render(){
//Removed <List .../>, rest is same just other way of doing same stuff
let LI = this.props.list.map( (item) => {
return( <li> <ListItem key={item.id} item={item} /></li>);
}
);
return ( <ul>{LI}</ul>);
}
};
dummy data to test
var data=[
{
id:"1st of Level 1",
get display_name(){return _name(this.id,this.children)},
children: [
{
id:"1st of Level 1.2",
get display_name(){return _name(this.id,this.children)},
children: [
{
id:"1st of Level 1.2.1",
get display_name(){return _name(this.id,this.children)},
children:[]
}
]
},
{
id:"2nd of Level 1.2",
get display_name(){return _name(this.id,this.children)},
children:[]
}
]
},
{
id:"2nd of Level 1",
get display_name(){return _name(this.id,this.children)},
children:[]
},
{
id:"3rd of Level 1",
get display_name(){return _name(this.id,this.children)},
children:[]
},
{
id:"4th of Level1",
get display_name(){return _name(this.id,this.children)},
children:[]
}
];
function _name(id,child) {
if(child.length>0)
return("I am "+id+" I HAVE CHILDREN");
else {
return("I am "+id+" I DON'T HAVE CHILDREN ");
}
}
ReactDOM.render(<List list={data} />,document.getElementById('root'));
`

Doing this recursively is a good way to go about this. What you can do is to give each list item element an onClick handler.
<li onClick={this._toggleChildrenInit.bind(this, item.id)}>
I don't know much about what you're id's are like. But consider the following structure:
{id: "0", isOpen: false, children: [
{id: "0.0", isOpen: false, children: [
{id: "0.0.0", isOpen: false, children: null},
{id: "0.0.1", isOpen: false, children: null}
]},
{id: "0.1", isOpen: false, children: null}
]}
The id for each child is the id of the previous parent, plus a "." and the number of order within that level.
You can then, with recursion, check if the desired item with some X id could be a child of an item, and start traversing that child the same way. This way, you don't have to traverse all items in the tree.
Of course, this may vary from application to application, but hopefully it provides a clue of how you could approach this.
The code for the recursion triggered by the click is as follows:
_toggleChildrenInit = (data_id) => {
var result = this._toggleChildren(0, data_id, this.state.listObject);
this.setState({listObject: result});
}
_toggleChildren = (i, data_id, arr) => {
if (i <= arr.length-1) {
if (arr[i].id == data_id && arr[i].children.length > 0) {
arr[i].isOpen = !arr[i].isOpen;
} else {
var newArr = this._toggleChildren(0, data_id, arr[i].children);
arr[i].children = newArr;
if (arr[i].id.charAt(0) != data_id.charAt(0)) {
arr[i].isOpen = false;
}
}
arr = this._toggleChildren(++i, data_id, arr);
}
return arr;
}
EDIT - alternative structure
If your object looks like this:
{id: "0", isOpen: false, children: [
{id: "1", isOpen: false, children: [
{id: "2", isOpen: false, children: null},
{id: "3", isOpen: false, children: null}
]},
{id: "4", isOpen: false, children: null}
]}
You can use this instead:
_toggleChildrenInit = (data_id) => {
var result = this._toggleChildren(0, data_id, this.state.listObject);
this.setState({listObject: result});
}
_toggleChildren = (i, data_id, arr) => {
if (i <= arr.length-1) {
if (arr[i].id == data_id && arr[i].children.length > 0) {
arr[i].isOpen = !arr[i].isOpen;
} else {
var newArr = this._toggleChildren(0, data_id, arr[i].children);
arr[i].children = newArr;
if (arr[i].id < data_id) {
arr[i].isOpen = false;
}
}
arr = this._toggleChildren(++i, data_id, arr);
}
return arr;
}

Related

Table Row Update/Remove Data in State(Array) and Toggle Add/Remove Class in React

I'm having list of table and each row is clickable. Once I onclick the row need to add corresponding data to array. If I again click the same row need to remove the data from array. Likewise i need to add an toggle Active class for selected row.
const data = [
{
name: "Name1",
foramt: "pdf",
version: 0
},
{
name: "Name2",
foramt: "pdf",
version: 0
},
{
name: "Name3",
foramt: "pdf",
version: 2
},
{
name: "Name4",
foramt: "pdf",
version: 5
},
]
this.state = {
active: false,
activeIndex: null,
selectedRow: []
}
<Table>
data.map((item, index) => {
const rowSelected = this.state.activeIndex === index ? "row-selected" : "";
return
<TableRow className = {rowSelected} onClick={this.handleClick(item,index)}>
<Cell>{item.name}</Cell>
<Cell>{item.format}</Cell>
</TableRow>
})
</Table>
handleClick = (item,index) => {
const {activeIndex} = this.state;
let array = []
if(activeIndex !== index) {
array.push(item);
}
this.setState({
selectedRows: array
})
}
For the TableRow onCLick event:
Change this:
onClick={this.handleClick(item,index)}
To
onClick={() => this.handleClick(item, index)}
The first case will run immediately instead of waiting for the event to be called.
And for the className
Change this:
className={rowSelected}
To:
className={rowSelected ? "row-selected" : null}
In the first one when the rowSelected === true you'd get className={true} which doesn't really point to any class name.
In the second example though you'd get className="selected"

ReactJS How to use Refs on components rendered dinamically by another render function to focus elements?

I have a class component that Renders a list of elements and I need to focus them when an event occurs.
Here is an example code
class page extends React.Component {
state = {
items: [array of objects]
}
renderList = () => {
return this.state.items.map(i => <button>{i.somekey}</button>)
}
focusElement = (someitem) => {
//Focus some item rendered by renderList()
}
render(){
return(
<div>
{this.renderList()}
<button onClick={() => focusElement(thatElement)}>
</div>
)
}
}
I know that I need to use refs but I tried several ways to do that and I couldn't set those refs properly.
Can someone help me?
you should use the createRefmethod of each button that you would like to focus, also you have to pass this ref to the focusElement method that you have created:
const myList = [
{ id: 0, label: "label0" },
{ id: 1, label: "label1" },
{ id: 2, label: "label2" },
{ id: 3, label: "label3" },
{ id: 4, label: "label4" },
{ id: 5, label: "label5" }
];
export default class App extends React.Component {
state = {
items: myList,
//This is the list of refs that will help you pick any item that ou want to focus
myButtonsRef: myList.map(i => React.createRef(i.label))
};
// Here you create a ref for each button
renderList = () => {
return this.state.items.map(i => (
<button key={i.id} ref={this.state.myButtonsRef[i.id]}>
{i.label}
</button>
));
};
//Here you pass the ref as an argument and just focus it
focusElement = item => {
item.current.focus();
};
render() {
return (
<div>
{this.renderList()}
<button
onClick={() => {
//Here you are able to focus any item that you want based on the ref in the state
this.focusElement(this.state.myButtonsRef[0]);
}}
>
Focus the item 0
</button>
</div>
);
}
}
Here is a sandbox if you want to play with the code

Comparing objects, checkbox checking, edit checkboxs

In the following line:
 
checked={this.state.peopleChecked.some(({ asset}) => asset['object'] ['user']['id'] === person.userCompetences.map((user, index) => {
user['asset']['id']
})
)}
I have a problem comparing two objects.
Compares a property from the array people ->userCompetences -> asset ->id with an object from the array peopleChecked ->asset -> object ->user - > asset_id.
if id from arraypeople and asset_id, id === asset_id are equal to returnstrue. Checkbox is checked
Code here: https://stackblitz.com/edit/react-n2zkjk
class App extends Component {
constructor() {
super();
this.state = {
people: [
{
firstname: "Paul",
userCompetences: [
{ asset:{
id: "12345"
}
}
]
},
{
firstname: "Victor",
userCompetences: [
{ asset: {
id: "5646535"
}
}
]
},
{
firstname: "Martin",
userCompetences: [
{ asset: {
id: "097867575675"
}
}
]
},
{
firstname: "Gregor",
userCompetences: [
{ asset: {
id: "67890"
}
}
]
}
],
peopleChecked: [
{
amount: 0,
asset: {
id: "fgfgfgfg",
object: {
competence: null,
id: "dsdsdsdsd",
user: {
firstname: "Gregor",
asset_id: "67890"
}
}
}
},
{
amount: 0,
asset: {
id: "dsdsdsd",
object: {
competence: null,
id: "wewewe",
user: {
firstname: "Paul",
asset_id: "12345"
}
}
}
},
],
selectPeopleId: []
}
}
/*handleSelect = (person) => {
//Check if clicked checkbox is already selected
var found = this.state.peopleChecked.find((element) => {
return element.id == person.id;
});
if(found){
//If clicked checkbox already selected then remove that from peopleChecked array
this.setState({
peopleChecked: this.state.peopleChecked.filter(element => element.id !== person.id),
selectPeopleId: this.state.selectPeopleId.filter(element => element !== person.id)
}, () => console.log(this.state.peopleChecked))
}else{
//If clicked checkbox is not already selected then add that in peopleChecked array
this.setState({
selectPeopleId: [...this.state.selectPeopleId, person.id],
peopleChecked: [...this.state.peopleChecked,person]
}, () => {console.log(this.state.selectPeopleId);console.log(this.state.peopleChecked);})
}
}*/
render() {
return (
<div>
{this.state.people.map(person => (
<div key={person.id} className="mb-1">
<input
type={'checkbox'}
id={person.id}
label={person.firstname}
checked={this.state.peopleChecked.some(({ asset}) => asset['object']['user']['id'] === person.userCompetences.map((user, index) => {
user['asset']['id']
})
)}
onChange = {() => this.handleSelect(person)}
/> {person.firstname}
</div>
))}
</div>
);
}
}
Your correct checked code syntax would be below based on your data structure:
Issue was asset_id correct key was missing and map returns an array thus you would need its index, however in your case you can simply swap it with person.userCompetences.[0]['asset']['id'] but I kept your syntax in case you want it for some other purpose.
checked={
this.state.peopleChecked.some(
({ asset }) => asset['object']['user']['asset_id'] === person.userCompetences.map(
(user, index) => user['asset']['id']
)[0]
)}
However its inherently complicated and you should focus on untangling it by placing some cached const in your map function to keep track of what you are looking at. I would also advice to introduce some child component to render in the first map to make your life easier maintaining this code in the future.
Edited code: https://stackblitz.com/edit/react-ptsnbc?file=index.js

Nested React list not working

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>

React: how to work with the database?

I'm studying react.js.
How to correctly add class 'use' to the element where the click occurs? From other elements it needs to be removed.
How to get rid of the index, but be able to handle and dispose of the items?
var DB = [
{
name: 'Имя 1', url: 'http://localhost:1', use: true
},
{
name: 'Имя 2', url: 'http://localhost:2', use: false
},
{
name: 'Имя 3', url: 'http://localhost:3', use: false
}
];
class SideBarEl extends React.Component {
hoverLi(t){
if(t.target.id !== ''){
for (var i = 0; i < DB.length; i++){
if(t.target.id == i){
DB[i].use = true;
} else {
DB[i].use = false;
}
}
}
}
render(){
var newsTemplate = DB.map(function(item, index) {
return (
<li key={ index } id={ index } onClick={ this.hoverLi.bind(this)} className={ item.use ? 'use' : '' }>
{ item.name }
<span>
{ item.url }
</span>
</li>
)
}, this);
return(
<ul>{newsTemplate}</ul>
)
}
}
1 Set this.state
You need to use React state to handle such things and rerender when action occurs. If you just use a variable, React doesn't know when something should be rerendered.
this.state = {
links: [
{
name: "Имя 1",
url: "http://localhost:1",
use: true
},
{
name: "Имя 2",
url: "http://localhost:2",
use: false
},
{
name: "Имя 3",
url: "http://localhost:3",
use: false
}
]
};
Read more about state on https://facebook.github.io/react/docs/state-and-lifecycle.html
2 Update state by using onClick
handleClick(item) {
this.setState(prevState => ({
links: prevState.links.map(link => {
link.use = link === item;
return link;
})
}));
}
render() {
// rest of code...
<li
key={item.url}
id={index}
onClick={() => this.handleClick(item)}
className={item.use ? "use" : ""}
>
// rest of code...
}
For only 3 links it's okay to have such non-optimized code. If you would like to apply this for big collection of links (hundreds or thousands of links), it will require a bit more work but probably it's out of you question's scope.
3 Demo
https://codesandbox.io/s/mQoomVOmA
If you click on a link, it will be red and rest will be black because I added this small CSS .use { color: red; }
Have fun and happy coding.

Categories