Having trouble with multiple active states in ReactJS - javascript

Happy Monday fellow stackOverflowers! I would just like to know if there are any ways to keep active states independent of each other when clicked. I cant seem to figure out how to make them independent of each other. I have the state as a toggle. Each button's active status is false by default. When clicked it will toggle the status to opposite. The problem is when one of the button is clicked and the status is toggled, so do the status of other buttons. Please see below for the code:
import React, {Component} from "react";
import { Button, Progress } from 'reactstrap';
import "../src/Questions.css";
const items = [
{
question:"Category",
buttonList: [
'Games',
"Business",
'Education',
'Lifestyle',
"Entertainment",
"Utility",
"Social",
"Other"
],
multiple: false,
answers: [],
},
{
question:"Platform",
buttonList: [
'Android',
'iOS',
'Both'
],
multiple: false,
answers: [],
},
{
question:"Design Type",
buttonList: [
'Barebones',
'Stock',
'Custom'
],
multiple: false,
answers: [],
},
{
question:"Development Type",
buttonList: [
'Native',
'Web',
'Hybrid'
],
multiple: false,
answers: [],
},
{
question:"Does the app connect to a server",
buttonList: [
'Yes',
'No'
],
multiple: false,
answers: [],
},
{
question:"Does the app require users to sign in",
buttonList: [
'Yes',
'No'
],
multiple: false,
answers: [],
},
{
question:"Additional Features",
buttonList: [
'Audio',
'Camera',
'3rd party API',
"Dashboard",
"Social login",
"Task list",
"Search",
"Rating system",
"Chat",
"Privacy Settings",
"Gallery",
"QR code",
"Calendar",
"Social sharing",
"Activity feed",
"Push notifications",
"Ads",
"Multilangual",
"Feedback",
"Data collection",
"Geo location",
"Office capabilities",
"Activity tracking",
"Augmented reality",
"Virtual reality",
"Data export",
"Maps",
"Backup"
],
multiple: true,
answers: [],
},
{
question:"Last Step",
buttonList: [
'Games3',
'Business3',
'Education3'
],
multiple: false,
answers: [],
},
];
function checkElementIsSelected(array, element) {
if (array.length === 0) {
return false;
}
return(array.find(item => {
return item === element;
}));
}
class Questions extends React.Component {
constructor(props) {
super(props);
this.state = {
value:0,
count:0,
display: items[0]['question'],
active:false
}};
handleClickNext = (e) => {
e.preventDefault();
this.setState({
value:this.state.value +10,
count: this.state.count + 1,
display: items[this.state.count]['question'],
email: ''
})
console.log('+++', this.state.count);
}
handleClickAnswer = (e) => {
e.preventDefault();
const question = this.state.count;
if (!items[this.state.count].multiple) {
items[this.state.count].answers = [e.target.value];
} else {
if (!checkElementIsSelected (items[this.state.count].answers, e.target.value)) {
items[this.state.count].answers.push(e.target.value);
}
}
console.log('--- answers: ', items[this.state.count].answers);
this.setState({
active:!this.state.active
})
console.log("True or False Active",this.state.active );
}
handleSubmit = (e) => {
e.preventDefault();
this.props.history.push("/Success");
console.log('***', items);
}
handleEmail = (e) => {
e.preventDefault();
items[items.length - 1].answers = [e.target.value];
console.log("$$$Emails: '", items[items.length - 1].answers);
}
render() {
let element;
let next;
if (this.state.count === items.length) {
element = <input type="text" onChange={this.handleEmail}/>
next = <Button color='primary' onClick={this.handleSubmit}>Get my estimate</Button>
} else {
element = items[this.state.count].buttonList.map(btn => {
return (
<div>
<Button outline color="primary" value={btn} onClick={this.handleClickAnswer} active={this.state.active}>{btn}</Button>
</div>
)
});
next = <Button onClick={this.handleClickNext} color="primary" size="lg" value='Next'>Next</Button>;
}
return(
<div>
<div><Progress value={this.state.count * (100 / items.length)} /></div>
<div className="howMuchText">How much does it cost to build an app</div>
<div className="questionTitle">{this.state.display}</div>
<div className="buttonChoices">
{
element
}
</div>
<div className="nextButton">
{next}
</div>
</div>
)
}
}
export default Questions;
Any help greatly appreciated. Thanks!!!!

You've got only one boolean value as a flag for active in this.state.active.
Change this.state.active to an object with keys corresponding to btn names and store there a boolean flag for each button.

The problem is this active={this.state.active}
You manage all the button with only one state.active. U need to make one state per button.
Here and example
state {
btnA:false,
btnB:false,
btnC:false
}
changeState = (btn) => {
if(btn === 'a'){
this.setState( prevState => {
return {
btnA: !prevState.btnA
}
})
}
.... same for btnB and btn C
}
...
return (
<div>
<button onClick={this.state.changeState('a')} >{this.state.btnA?'Btn A is active':'Btn A is not active'}<button>
<button onClick={this.state.changeState('b')} >{this.state.btnB?'Btn B is active':'Btn B is not active'}<button>
<button onClick={this.state.changeState('c')} >{this.state.btnB?'Btn C is active':'Btn C is not active'}<button>
</div>
)
this is just an example about how to manage buttons status with just one state now adapt this example to your code

Related

How to checkbox filtering in reactjs and handle state? and show the available item after the checkbox

I want to make a filter system using multiple checkbox. But when i checked one checkbox it filter the state but when i unchecked it how i can get back the all data in state . Also if i select multiple checkbox then it will filter from the filtered item.
Here is my code.
state = {
restaurant : [
{name: 'La mesa', cuisine: ['italian', 'indian']},
{name: 'Red Bull', cuisine: ['chiness', 'french']}
{name: 'Purnima', cuisine: ['thai', 'arabic']}
]
cuisine: [
{id: 1, name: 'italian'},
{id: 2, name: 'indian'},
{id: 3, name: 'chiness'}
{id: 4, name: 'french'},
{id: 4, name: 'arabic'},
]
}
handleCuisineFilter = (e) => {
if (e.target.checked) {
const filter =
this.state.restaurant.length &&
this.state.restaurant.filter((rest) => rest.cuisine.includes(e.target.value));
this.setState({ restaurant: filter });
} else {
Now when unchecked how i can get previous state???
}
};
render() {
return (
<div>
{this.state.cuisine.length && this.state.cuisine.map(
cuisine=> (<li>
<input
id={cuisine.id}
type='checkbox'
onChange={this.handleCuisineFilter}
name='check'
value={cuisine.name}
/>
{cuisine.name } {here will be count of number of restaurant}
</li>
))}
{this.state.restaurant.length && this.state.restaurant.map(rest=> <h5>rest.name</h5>)}
</div>
I tried to explain best via my code . Help me please. Thank you in advance
You have to keep track of checked state for each filter and then filter against all filters at once every time.
Here is the solution
EDIT
import React, { Component } from "react";
import "./App.css";
class App extends Component {
state = {
restaurant: [
{ name: "La mesa", cuisine: ["italian", "indian"] },
{ name: "Red Bull", cuisine: ["chiness", "french"] },
{ name: "Purnima", cuisine: ["thai", "arabic"] },
],
// maintain a checked state for each filter
cuisine: [
{ id: 1, name: "italian", checked: false },
{ id: 2, name: "indian", checked: false },
{ id: 3, name: "chiness", checked: false },
{ id: 4, name: "french", checked: false },
{ id: 5, name: "arabic", checked: false },
],
};
setFilter = (cuisine, flag) => {
this.setState((prevState) => ({
cuisine: prevState.cuisine.map((c) =>
// check state for the selected cuisine
c.id === cuisine.id ? { ...c, checked: flag } : c
),
}));
};
handleCuisineFilter = (e, cuisine) => {
if (e.target.checked) {
this.setFilter(cuisine, true);
} else {
this.setFilter(cuisine, false);
}
};
filterRestaurants = (restaurant) => {
const checkedFilters = this.state.cuisine.filter((c) => c.checked);
const noFiltersChecked = checkedFilters.length === 0;
if (noFiltersChecked) {
return true;
} else {
// EDITED:
const tmp = checkedFilters.reduce(
(hasRestaurantAllTheseCuisines, nextCuisine) =>
(hasRestaurantAllTheseCuisines =
hasRestaurantAllTheseCuisines &&
restaurant.cuisine.includes(nextCuisine.name)),
true
);
return tmp;
}
};
render() {
return (
<div>
{this.state.cuisine.length &&
this.state.cuisine.map((cuisine) => (
<li key={cuisine.id}>
<input
id={cuisine.id}
type="checkbox"
onChange={(e) => this.handleCuisineFilter(e, cuisine)}
name="check"
value={cuisine.name}
/>
{cuisine.name} {/* here will be count of number of restaurant */}
</li>
))}
{/* Use .filter() with cuisine state */}
{this.state.restaurant.length &&
this.state.restaurant
.filter(this.filterRestaurants)
.map((rest) => <h5 key={rest.name}>{rest.name}</h5>)}
</div>
);
}
}
export default App;
Edited the code. The only change was the filter check here
...
const tmp = checkedFilters.reduce(
(hasRestaurantAllTheseCuisines, nextCuisine) =>
(hasRestaurantAllTheseCuisines =
hasRestaurantAllTheseCuisines &&
restaurant.cuisine.includes(nextCuisine.name)),
true
);
...

How to append data in json using map function?

How can i add additional parameter inside of json using map function
How can I add selected false properties inside of menus array?
Here is the example of Json
const X = [
{
"detail1": "FirstJob",
"menus": [
{
"Order": 1,
"El": " Record Management",
"subSection": [
{
"El": "Check Notification",
"Order": "CheckNotification"
},
{
"El": "Check Record",
"Order": "CheckRecord"
}
]
},
{
"Order": 2,
"El": "Management",
"subSection": [
{
"El": "Notification",
"Order": "Notification"
},
{
"El": "Check",
"Order": "Check"
}
]
},
]
}
]
I tried this but it does add selected inside of the menus array
const filtered = X.map((item) => { return { ...item, selected: false }});
What if i want to add selected properties in subSection array ? how can that be implemnted?
You might need some deeper map
const newX = X.map((el) => ({
...el,
menus: el.menus.map((menuEl) => ({
...menuEl,
selected: false,
})),
}))
const X = [
{
detail1: "FirstJob",
menus: [
{
Order: 1,
El: " Record Management",
subSection: [
{
El: "Check Notification",
Order: "CheckNotification",
},
{
El: "Check Record",
Order: "CheckRecord",
},
],
},
{
Order: 2,
El: "Management",
subSection: [
{
El: "Notification",
Order: "Notification",
},
{
El: "Check",
Order: "Check",
},
],
},
],
},
]
const newX = X.map((el) => ({
...el,
menus: el.menus.map((menuEl) => ({
...menuEl,
selected: false,
})),
}))
console.log(newX)
You just have to extend the map to the subSection array as well.
const X = [{detail1:'FirstJob',menus:[{Order:1,El:' Record Management',subSection:[{El:'Check Notification',Order:'CheckNotification',},{El:'Check Record',Order:'CheckRecord',},],},{Order:2,El:'Management',subSection:[{El:'Notification',Order:'Notification',},{El:'Check',Order:'Check',},],},],},];
const getFormattedData = data => {
return data.map(d => {
return {
...d,
menus: d.menus.map(m => {
return {
...m,
subSection: m.subSection.map(s => {
return {
...s,
selected: false
}
})
}
})
}
})
}
console.log(getFormattedData(X));
.as-console-wrapper {
max-height: 100% !important;
}
Try this for adding selected at the top level
X.map(
item => {
item["selected"]=false;
return item;
}
);
and this for adding the selected field in the subSection
X.map(
item => {
item["menus"].map(
menuItem => {
menuItem["selected"] = true;
}
);
return item;
}
);

how to filter multi array list in reactjs

I have tried to implement a search functionality from a list that has multi-array. So I want to search the particular keyword from the array. I have tried but it throws an error.
Array to search from:
const listComponent = [{
id: 0,
title: "Common",
subMenu: [{
image: "",
secTitle: "",
subTitle: ""
}]
},
{
id: 1,
title: "Compute",
subMenu: [{
image: require("../../assets/images/scaling.png"),
secTitle: "one comp",
subTitle: ""
},
{
image: require("../../assets/images/ec2.png"),
secTitle: "two comp",
subTitle: ""
},
{
image: require("../../assets/images/lambda.png"),
secTitle: "three comp",
subTitle: ""
},
{
image: require("../../assets/images/zone.png"),
secTitle: "four comp",
subTitle: ""
}
]
},
{
id: 2,
title: "Second",
subMenu: [{
image: "",
secTitle: "",
subTitle: ""
}]
},
{
id: 3,
title: "Third",
subMenu: [{
image: "",
secTitle: "",
subTitle: ""
}]
},
{
id: 4,
title: "Fourth",
subMenu: [{
image: "",
secTitle: "",
subTitle: ""
}]
}
];
Class Declaration:
constructor(props) {
super(props);
this.state = {
components: listComponent,
filterResult: listComponent
};
this.filterComponents = this.filterComponents.bind(this);
}
UNSAFE_componentWillReceiveProps(nextProps) {
this.setState({
components: nextProps.components
});
}
Fiter logic:
filterComponents = event => {
let value = event.target.value;
let components = this.state.components;
let filterResult = components.filter(
component =>
component.subMenu.findIndex(sub =>
sub.secTitle.toLowerCase().includes(value)
) !== -1
);
console.log(filterResult);
this.setState({ filterResult });
};
JSX:
<ul className="listAll">
{this.state.filterResult.map((items, key) => (
<li key={key}>
<div className="compTitle" onClick={()=> this.openCloseComponent(items.id)} >
<p>{items.title}</p>
{this.state.isComOpen === items.id && this.state.isOpen ? (
<KeyboardArrowDown className="compArrow" /> ) : (
<KeyboardArrowUp className="compArrow" /> )}
</div>
<ul className={ this.state.isComOpen===i tems.id && this.state.isOpen ? "displayBlock secondDrop" : "displayNone" }>
{items.subMenu.map((submenu, i) => (
<li key={i}>
<img src={submenu.image} alt="" />
<div>
<p className="secTitle">{submenu.secTitle}</p>
<p className="subTitle">{submenu.subTitle}</p>
</div>
</li>
))}
</ul>
</li>
))}
</ul>
{this.state.filterResult.length === 0 && (
<p className="noComp">No components found</p>
)}
Problem
I want to search secTitle in subMenu. The keyword that matches any secTitle should be displayed and other should be hidden. The about code throws an error.
So how do I fix it, any answers will be appreciated!
If I search one comp the output should be
{
id: 1,
title: "Compute",
subMenu: [
{
image: require("../../assets/images/scaling.png"),
secTitle: "one comp",
subTitle: ""
}
]
}
but now the output is,
{
id: 1,
title: "Compute",
subMenu: [
{
image: require("../../assets/images/scaling.png"),
secTitle: "one comp",
subTitle: ""
},
{
image: require("../../assets/images/ec2.png"),
secTitle: "two comp",
subTitle: ""
},
{
image: require("../../assets/images/lambda.png"),
secTitle: "three comp",
subTitle: ""
},
{
image: require("../../assets/images/zone.png"),
secTitle: "four comp",
subTitle: ""
}
]
}
instead of
filterResult = components.filter((component, index) => {
console.log(component.subMenu[index].secTitle);
return component.subMenu[index].secTitle.toLowerCase().search(value) !== -1;
});
The code should look something like this:
const listComponent = [
{
id: 0,
title: "Common",
subMenu: [
{
image: "",
secTitle: "abc",
subTitle: ""
}
]
},
{
id: 1,
title: "Common",
subMenu: [
{
image: "",
secTitle: "def",
subTitle: ""
}
]
}
]
let value = "abc";
let filterResult = listComponent.filter((component) => {
return component.subMenu.findIndex((sub) => {
return sub.secTitle === value;
}) >= 0;
});
console.log(filterResult);
try changing the key for subMenu. Ideally, you should not use an index as a key as the length and the order of subMenu might change. As Keys help React identify which items have changed, are added, or are removed.
trying changing to this.
...
{items.subMenu.map((submenu, i) => (
<li
key={`${submenu.secTitle}-${i}`}
>
<img src={submenu.image} alt="" />
<div>
<p className="secTitle">{submenu.secTitle}</p>
<p className="subTitle">{submenu.subTitle}</p>
</div>
</li>
))}
...
// For case-insensitive search
const result = [];
listComponent.forEach((component, index)=> {
const comp = {...component};
if(Array.isArray(comp.subMenu)) {
comp.subMenu = comp.subMenu.filter((subMenu)=> {
return subMenu.secTitle.toLowerCase().includes(value.toLowerCase());
});
if( comp.subMenu.length > 0) {
result.push(comp);
}
}
});
// For case-sensitive search
const result = [];
listComponent.forEach((component, index)=> {
const comp = {...component};
if(Array.isArray(comp.subMenu)) {
comp.subMenu = comp.subMenu.filter((subMenu)=> {
return subMenu.secTitle.includes(value);
});
if( comp.subMenu.length > 0) {
result.push(comp);
}
}
});
Okay here's the solution
Change your filterComponents method as this
const filterComponents = (e) => {
let value = e.target.value;
let components = [ ...this.state.components];
const filterData = []
components.map(item => {
item.subMenu.reduce((acc, cur) => {
if (cur.secTitle === value) {
acc.push(cur)
}
}, filterData)
})
this.setState({ filterResult: filterData });
};
filterData will have filtered result
Hope it helps
To solve your problem you have first find the top-level item with the item that you want:
From Glen K answer:
let aux = listComponent.filter((component) => {
return component.subMenu.findIndex((sub) => {
return sub.secTitle === value;
}) >= 0;
});
Then you filter the subMenu on the filtered list.
aux = aux.map(item => {
item.subMenu = item.subMenu.filter((component) => {
return component.secTitle === value;
})
return item;
})
Then save it to your state
filterResult = [...aux];
this.setState({ filterResult });
You are only filtering the Components, so run the filter again on the subMenus to filter the submenus:
let value = event.target.value;
let components = JSON.parse(JSON.stringify(this.state.components));
// This filter only targets the components themselves...
let filterResult = components.filter(
component =>
component.subMenu.findIndex(sub =>
sub.secTitle.toLowerCase().includes(value)
) !== -1
);
// ...so, run the filter again, but this time filter the subMenus
for (let i = 0; i < filterResult.length; i++) {
filterResult[i].subMenu = [ ...filterResult[i].subMenu];
filterResult[i].subMenu = filterResult[i].subMenu.filter(
sub =>
sub.secTitle.toLowerCase().includes(value)
);
}
console.log(filterResult);
Other answers provide cleaner solutions, but I feel like you're misunderstanding how the filter function works. So, I simply reuse your filter code on the subMenus -- hopefully this illustrates the concept that you are missing.
The Snippet below WORKS! So, please take a look... (I had to make a few assumptions about the rest of your code).
'use strict';
function require(url){ return url; };
const listComponent = [
{
id: 0,
title: "Common",
subMenu: [
{
image: "",
secTitle: "",
subTitle: ""
}
]
},
{
id: 1,
title: "Compute",
subMenu: [
{
image: require("../../assets/images/scaling.png"),
secTitle: "one comp",
subTitle: ""
},
{
image: require("../../assets/images/ec2.png"),
secTitle: "two comp",
subTitle: ""
},
{
image: require("../../assets/images/lambda.png"),
secTitle: "three comp",
subTitle: ""
},
{
image: require("../../assets/images/zone.png"),
secTitle: "four comp",
subTitle: ""
}
]
},
{
id: 2,
title: "Second",
subMenu: [
{
image: "",
secTitle: "",
subTitle: ""
}
]
},
{
id: 3,
title: "Third",
subMenu: [
{
image: "",
secTitle: "",
subTitle: ""
}
]
},
{
id: 4,
title: "Fourth",
subMenu: [
{
image: "",
secTitle: "",
subTitle: ""
}
]
}
];
function KeyboardArrowUp(props) {
return <span>up</span>;
}
class AppComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
components: listComponent,
filterResult: listComponent
};
this.filterComponents = this.filterComponents.bind(this);
}
filterComponents = event => {
let value = event.target.value;
let components = JSON.parse(JSON.stringify(this.state.components));
// This filter only targets the components themselves...
let filterResult = components.filter(
component =>
component.subMenu.findIndex(sub =>
sub.secTitle.toLowerCase().includes(value)
) !== -1
);
// ...so, run the filter again, but this time filter the subMenus
for (let i = 0; i < filterResult.length; i++) {
filterResult[i].subMenu = [ ...filterResult[i].subMenu];
filterResult[i].subMenu = filterResult[i].subMenu.filter(
sub =>
sub.secTitle.toLowerCase().includes(value)
);
}
console.log(filterResult);
this.setState({ filterResult });
};
UNSAFE_componentWillReceiveProps(nextProps) {
this.setState({
components: nextProps.components
});
}
openCloseComponent(id) {
console.log(id);
}
render() {
return <div><input type="text" onChange={this.filterComponents} /><br/><ul className="listAll">
{this.state.filterResult.map((items, key) => (
<li key={key}>
<div
className="compTitle"
onClick={() => this.openCloseComponent(items.id)}
>
<p>{items.title}</p>
{this.state.isComOpen === items.id && this.state.isOpen ? (
<KeyboardArrowDown className="compArrow" />
) : (
<KeyboardArrowUp className="compArrow" />
)}
</div>
<ul
className={
this.state.isComOpen === items.id && this.state.isOpen
? "displayBlock secondDrop"
: "displayNone"
}
>
{items.subMenu.map((submenu, i) => (
<li
key={i}
>
<img src={submenu.image} alt="" />
<div>
<p className="secTitle">{submenu.secTitle}</p>
<p className="subTitle">{submenu.subTitle}</p>
</div>
</li>
))}
</ul>
</li>
))}
</ul></div>
{this.state.filterResult.length === 0 && (
<p className="noComp">No components found</p>
)};
}
}
const domContainer = document.querySelector('#app');
ReactDOM.render(<AppComponent />, domContainer);
<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">
</div>
Use a combination of Array.filter and Array.findIndex to get the desired result.
Use String.includes to match the substring in secTitle
filterResult = filterResult.filter((component) => {
const hasMatchingSubMenu = component.subMenu.findIndex((sub) => sub.secTitle.includes(value)) !== -1;
if(hasMatchingSubMenu) {
component.subMenu = component.subMenu.filter((sub) => sub.secTitle.includes(value));
}
return hasMatchingSubMenu;
});
First use filter to filter the components:
let filterResult = components.filter(c => c.subMenu.filter(hasRequiredSecTitle).length > 0);
And then filter the subMenu collection of filtered components:
filterResult = filterResult.map(c => ({...c, subMenu: c.subMenu.filter(hasRequiredSecTitle)}))
So complete function will be:
filterComponents = event => {
let value = event.target.value;
let components = this.state.components;
const hasRequiredSecTitle = (subMenu) => subMenu.secTitle === value;
let filterResult = components.filter(c => c.subMenu.filter(hasRequiredSecTitle).length > 0);
filterResult = filterResult.map(c => ({...c, subMenu: c.subMenu.filter(hasRequiredSecTitle)}))
console.log(filterResult);
this.setState({ filterResult });
};
There are some good answers here! Figured I would chime in with mine..
This allows for case insensitive searches, and is fairly straight forward..
const { Component } = React;
const { render } = ReactDOM;
class App extends Component {
state = {
filtered: [],
listComponents: this.props.listComponent || [],
searchText: this.props.searchText || ""
};
componentDidMount = () =>
this.props.searchText &&
this.filterComponents();
handleChange = event =>
this.setState({
searchText: event.target.value
}, () => {
this.state.searchText
? this.filterComponents()
: this.setState({ filtered: [] })
});
filterComponents = () => {
const { searchText, listComponents } = this.state;
const searchExp = new RegExp(searchText, "i");
const listCopy = JSON.parse(JSON.stringify(listComponents));
const result = listCopy.filter(item => {
item.subMenu = item.subMenu.filter(sub => searchExp.test(sub.secTitle));
return item.subMenu.length && item;
});
this.setState({ filtered: result })
};
render() {
const { filtered, listComponents, searchText } = this.state;
return (
<div>
<input onChange={this.handleChange} value={searchText} type="text" />
<pre>{JSON.stringify(filtered, null, 2)}</pre>
</div>
);
}
}
const listComponent = [
{
id: 0,
title: "Common",
subMenu: [
{
image: "",
secTitle: "",
subTitle: ""
}
]
},
{
id: 1,
title: "Compute",
subMenu: [
{
image: 'require("../../assets/images/scaling.png")',
secTitle: "one comp",
subTitle: ""
},
{
image: 'require("../../assets/images/ec2.png")',
secTitle: "two comp",
subTitle: ""
},
{
image: 'require("../../assets/images/lambda.png")',
secTitle: "three comp",
subTitle: ""
},
{
image: 'require("../../assets/images/zone.png")',
secTitle: "four comp",
subTitle: ""
}
]
},
{
id: 2,
title: "Second",
subMenu: [
{
image: "",
secTitle: "",
subTitle: ""
}
]
},
{
id: 3,
title: "Third",
subMenu: [
{
image: "",
secTitle: "",
subTitle: ""
}
]
},
{
id: 4,
title: "Fourth",
subMenu: [
{
image: "",
secTitle: "",
subTitle: ""
}
]
}
];
let index = <App listComponent={listComponent} searchText="oNe" />
render(index, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.2/umd/react-dom.production.min.js"></script>

React Context initial state is being mutated - JavaScript / React

I am making a guide me section. What this guide me section does - displays an array of processes. Within each process is an array of steps, within each step is an array of options. The user selects an option from one of the steps, it takes them to the next correlating step. If the user selects the option on step 2, it could take them to step 3 or back to step 1. It depends on the id.
With all that said I'm having issues with my Process mutating on me. I'm using React Context as a global state. When a user selects an option, I'm grabbing that id, then filtering the designated object by that id. So I should only be left is that processes with that specific step. What's happening is my initial global state is mutating somehow. I'm missing something here as I'm new to React.
P.s - I'm not using any services at this moment, so I just copied some JSON over to my initial state in context.js
context.js
import React, { Component } from 'react'
// import axios from 'axios'
const Context = React.createContext()
const reducer = (state, action) => {
switch(action.type){
case 'SEARCH_PROCESS':
return {
...state,
guides: action.payload
}
default:
return state
}
}
export class Provider extends Component {
state = {
guides: [
{
"processName": "Support Messaging",
"steps": [{
"id": "15869594739302",
"title": "step one",
"options": [{
"nextStep": "4767fn-47587n-2819am-9585j,04956840987",
"text": "Option 1 text",
"type": "option"
},
{
"nextStep": "4767fn-47587n-2819am-9585j,04956840987",
"text": "Option 2 text",
"type": "option"
},
{
"nextStep": "",
"text": "Option 3 text",
"type": "option"
}
]
},
{
"id": "04956840987",
"title": "step two",
"options": [{
"nextStep": "4767fn-47587n-2819am-9585j,15869594739302",
"text": "Return to step1",
"type": "option"
},
{
"nextStep": "",
"text": "Option 2 text",
"type": "option"
},
{
"nextStep": "",
"text": "Option 3 text",
"type": "option"
}
]
}
],
"owner": "bob",
"id": "4767fn-47587n-2819am-9585j",
"lastUpdated": 154222227099000,
"tags": ["Tag1", "Tag2", "Tag3"]
}
],
"owner": "bob",
"id": "4767fn-47587n-2819am-9585x",
"lastUpdated": 154222227099000,
"tags": ["Tag1", "Tag2", "Tag3"]
}
],
initialGuide: [
{
"processName": "Support Messaging",
"steps": [{
"id": "15869594739302",
"title": "step one",
"options": [{
"nextStep": "4767fn-47587n-2819am-9585j,04956840987",
"text": "Option 1 text",
"type": "option"
},
{
"nextStep": "4767fn-47587n-2819am-9585j,04956840987",
"text": "Option 2 text",
"type": "option"
},
{
"nextStep": "",
"text": "Option 3 text",
"type": "option"
}
]
},
{
"id": "04956840987",
"title": "step two",
"options": [{
"nextStep": "4767fn-47587n-2819am-9585j,15869594739302",
"text": "Return to step1",
"type": "option"
},
{
"nextStep": "",
"text": "Option 2 text",
"type": "option"
},
{
"nextStep": "",
"text": "Option 3 text",
"type": "option"
}
]
}
],
"owner": "bob",
"id": "4767fn-47587n-2819am-9585j",
"lastUpdated": 154222227099000,
"tags": ["Tag1", "Tag2", "Tag3"]
}
],
dispatch: action => this.setState(state => reducer(state, action))
}
render() {
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}
export const Consumer = Context.Consumer;
Guides.js
import React, { Component } from 'react'
import { Consumer } from '../../context'
import Process from './Process'
class Guides extends Component {
constructor (props) {
super(props)
this.state = {
contextValue: [],
searchData: props.location.data
}
}
render () {
console.log(this.props.location.data, this.state, 'logging state and props on guides')
// this.state.searchData = this.props.location.data
return (
<Consumer>
{value => {
return (
<React.Fragment>
<div className="content-wrapper">
<h1>Guide Me</h1>
<div className="ms-Grid times--max-width" dir="ltr">
<div className="ms-Grid-row">
<div className="profile--wrapper ms-Grid-col ms-sm12 ms-md12 ms-lg5">
{value.guides.map(item => {
return <Guide key={item.id} guide={item} processValue={value.guides} initialGuide={value.initialGuide}/>
})}
</div>
</div>
</div>
</div>
</React.Fragment>
)
}}
</Consumer>
)
}
}
export default Guides
Process.js
import React, { Component } from 'react'
import GuideSteps from './Guide-Steps'
import { Consumer } from '../../context'
class Process extends Component {
constructor(props) {
super(props)
this.state = {
processName: this.props.guide.processName,
process: this.props.guide,
steps: this.props.guide.steps,
selectedIndex: 0,
selectedStep: '',
processValue: this.props.processValue,
initialGuide: this.props.initialGuide
}
this.displayStep = this.displayStep.bind(this)
}
displayStep = (res, dispatch) => {
this.setState({ selectedStep: res })
}
render() {
const { steps, selectedIndex, process, processName, processValue, initialGuide } = this.state
return (
<Consumer>
{value => {
return (
<div>
<h2 className="profile--sub-header--bold">{processName}</h2>
<GuideSteps
key={this.props.guide.steps[selectedIndex].id}
selectedStep={this.props.guide.steps[selectedIndex]}
stepValue={this.displayStep}
process={process}
processValue={processValue}
initialGuide={initialGuide}
/>
</div>
)
}}
</Consumer>
)
}
}
export default Process
Guide-Steps.js
import React, { Component } from 'react'
import { ChoiceGroup } from 'office-ui-fabric-react/lib/ChoiceGroup'
import { Consumer } from '../../context'
class GuideSteps extends Component {
constructor(props) {
super(props);
this.state = {
process: [],
selectedStep: this.props.selectedStep,
dispatch: '',
processValue: this.props.processValue,
initialGuide: ''
}
this._onChange = this._onChange.bind(this)
}
_onChange = (ev, option) => {
// this.props.stepValue(option.value.nextStep)
const { dispatch , initialGuide } = this.state
let optionArray = option.value.nextStep.split(',')
let processArray = this.state.process.filter(item => {
return item.id === optionArray[0]
})
let stepArray = processArray[0].steps.filter(item => {
return item.id === optionArray[1]
})
console.log(stepArray, processArray, this.state.process, 'logging step array before setting')
processArray[0].steps = stepArray
console.log(stepArray, processArray, this.state.process, 'logging step array after setting')
dispatch({
type: 'SEARCH_PROCESS',
payload: processArray
})
}
render() {
let options = []
{
this.props.selectedStep.options.map(item => {
return options.push({
key: item.text,
text: item.text,
value: item
})
})
}
return (
<Consumer>
{value => {
const { dispatch, guides, initialGuide } = value
this.state.dispatch = dispatch
console.log(value, 'logging initial guide in render')
this.state.process = initialGuide
return (
<div>
<ChoiceGroup
className="defaultChoiceGroup"
options={options}
onChange={this._onChange}
/>
</div>
)
}}
</Consumer>
)
}
}
export default GuideSteps
On change in GuideSteps is where I'm doing the logic for filtering and setting up my new object.
EDIT
This fixed the issue but I think it's too expensive. How would I go about solving this issue without having to reparse the array.
update: (ev, option) => {
const { initialGuide } = this.state
if (option.value.nextStep !== null && option.value.nextStep !== '') {
//split string
const optionArray = option.value.nextStep.split(',')
//filter process array
const processArray = initialGuide.filter(process => {
return process.id === optionArray[0]
})
//filter step array
const stepArray = processArray[0].steps.filter(
item => item.id === optionArray[1]
)
if(stepArray.length > 0 && stepArray !== null) {
//get a copy of the process array so original is not mutated by the steps
let stringC = JSON.stringify(processArray)
let stringD = JSON.parse(stringC)
stringD[0].steps = stepArray
//issue might be here visually where setting the state happens quickly, therefore radio button visual does not display in time.
setTimeout(() => {
this.setState({ guides: stringD })
}, 200)
}
}
},
this.state.process = initialGuide
let processArray = this.state.process.filter...
processArray[0].steps = stepArray
So it looks like you're mutating initialGuide via reference.

Indeterminate checkboxes with Vue.js

I just started out working with Vue and I'm trying to visualise a nested list.
The list-items should contain triple-state checkboxes:
When a child item is checked, the parent item's checkbox should become 'indeterminate'. When all child-checkboxes are checked, the parent checkbox should also become checked.
When a parent item checkbox is checked, all child item checkboxes (also the ones nested deeper) should be selected too.
I kind of have a working solution (check out this pen or the code below) but the checkbox-logic is still flawed. For this example, checked boxes are green, indeterminate ones are orange and unchecked ones are red.
I've run out of ideas how to fix it. Could someone shed some light on how to accomplish this in Vue?
'use strict';
Vue.component("book-chapter", Vue.extend({
name: "book-chapter",
props: ["data", "current-depth"],
data: function() {
return {
checked: this.data.checked,
indeterminate: this.data.indeterminate || false
};
},
methods: {
isChecked: function() {
return this.checked && !this.indeterminate;
},
isIndeterminate: function(){
return this.indeterminate;
},
toggleCheckbox: function(eventData) {
if (this.currentDepth > 0){
if (!this.data.children) {
this.checked != this.children
} else {
this.indeterminate = !this.indeterminate;
}
}
if (eventData) {
// fired by nested chapter
this.$emit('checked', eventData);
} else {
// fired by top level chapter
this.checked = !this.checked;
this.$emit('checked', {
data: this.data
});
}
},
isRootObject: function() {
return this.currentDepth === 0;
},
isChild: function() {
return this.currentDepth === 2;
},
isGrandChild: function() {
return this.currentDepth > 2;
}
},
template: `
<div class='book__chapters'>
<div
class='book__chapter'
v-bind:class="{ 'book__chapter--sub': isChild(), 'book__chapter--subsub': isGrandChild() }"
v-show='!isRootObject()'>
<div class='book__chapter__color'></div>
<div
class='book__chapter__content'
v-bind:class="{ 'book__chapter__content--sub': isChild(), 'book__chapter__content--subsub': isGrandChild() }">
<div class='book__chapter__title'>
<span class='book__chapter__title__text'>{{data.title}}</span>
</div>
<div class='book__chapter__checkbox triple-checkbox'>
<div class='indeterminatecheckbox'>
<div
class='icon'
#click.stop="toggleCheckbox()"
v-bind:class="{'icon--checkbox-checked': isChecked(), 'icon--checkbox-unchecked': !isChecked(), 'icon--checkbox-indeterminate': isIndeterminate()}">
</div>
</div>
</div>
</div>
</div>
<book-chapter
ref='chapter'
:current-depth='currentDepth + 1'
v-for='child in data.children'
key='child.id'
#checked='toggleCheckbox(arguments[0])'
:data='child'>
</book-chapter>
</div>
`
}));
Vue.component("book", Vue.extend({
name: "book",
props: ["data"],
template: `
<div class='book'>
<book-chapter
:data='this.data'
:currentDepth='0'>
</book-chapter>
</div>
`
}));
var parent = new Vue({
el: "#container",
data: function() {
return {
book: {}
};
},
mounted: function() {
this.book = {
"title": "Book",
"children": [{
"title": "1 First title",
"children": [{
"title": "1.1 Subtitle"
}, {
"title": "1.2 Subtitle"
}]
}, {
"title": "2 Second title",
"children": [{
"title": "2.1 Subtitle",
"children": [{
"title": "2.1.1 Sub-Sub title"
}, {
"title": "2.1.2 Another sub-sub title"
}]
}]
}]
}
}
});
Update: fixed a bug found by #PhillSlevin. See pen here
Check this pen, is it what you want to achieve?
I think you can use either eventbus or vuex to solve this problem,
if you treated every 's section as a component.
'use strict';
var bus = new Vue();
var book = {
"title": "Book",
"children": [{
"title": "1 First title",
"children": [{
"title": "1.1 Subtitle"
}, {
"title": "1.2 Subtitle"
}]
}, {
"title": "2 Second title",
"children": [{
"title": "2.1 Subtitle",
"children": [{
"title": "2.1.1 Sub-Sub title"
}, {
"title": "2.1.2 Another sub-sub title"
}]
}]
}]
};
Vue.component('book', {
template: `
<div class="book__chapter">
<p :class="'book__title ' + status" #click="clickEvent">{{title}} {{parent}}</p>
<book v-for="child in children" :key="child" :info="child"></book>
</div>
`,
props: ['info'],
data() {
return {
parent: this.info.parent,
title: this.info.title,
children: [],
status: this.info.status,
};
},
created() {
const info = this.info;
if(info.children) {
info.children.forEach(child => {
child.status = "unchecked";
// use title as ID
child.parent = info.title;
});
this.children = info.children;
}
},
mounted() {
const vm = this;
bus.$on('upside', (payload) => {
const targetArr = vm.children.filter((child) => child.title === payload.from);
if (targetArr.length === 1) {
const target = targetArr[0];
target.status = payload.status;
if (vm.children.every(ele => ele.status === 'checked')) {
vm.status = 'checked';
} else if (vm.children.every(ele => ele.status === 'unchecked')) {
vm.status = 'unchecked';
} else {
vm.status = 'indeterminate';
}
bus.$emit('upside', {
from: vm.title,
status: vm.status,
});
}
});
bus.$on('downside', (payload) => {
if (payload.from === this.parent) {
if (payload.status === 'checked') {
vm.status = 'checked';
vm.children.forEach(child => child.status = 'checked');
} else if (payload.status === 'unchecked') {
vm.status = 'unchecked';
vm.children.forEach(child => child.status = 'unchecked')
}
bus.$emit('downside', {
from: vm.title,
status: vm.status,
})
}
});
},
methods: {
clickEvent() {
if (this.status === 'checked') {
this.status = 'unchecked';
this.children.forEach(child => child.status = 'unchecked');
} else {
this.status = 'checked';
this.children.forEach(child => child.status = 'checked');
}
const vm = this;
bus.$emit('upside', {
from: vm.title,
status: vm.status,
});
bus.$emit('downside', {
from: vm.title,
status: vm.status,
});
},
}
});
var parent = new Vue({
el: "#container",
data: function() {
return {
book
};
},
});
.book__title.unchecked::after {
content: '□';
}
.book__title.indeterminate::after {
content: '△';
}
.book__title.checked::after {
content: '■';
}
.book__chapter {
display: block;
position: reletive;
margin-left: 40px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.js"></script>
<div id="container">
<book :info="book" :parent="'container'"></book>
</div>

Categories