React nested components with checkboxes - javascript

I have a website that contains more than a dozen components. Each parent component contains about 5 other components. Each 5 components contains a form. Parent components has 3 checkboxes. 1: Expand all components in that offer (expand all), 2: assign everything in that component (assign all), 3: Expand the component (expand product). Each child component has two checkboxes.1: Expand the component (also expand product), 2: assign everything from the component (also assign all). All of these options works on a state change. They all work correctly. The problem is that when I click parent checkbox, I can not deselect the child's single component. So I can not uncheck the checkbox inside child's component (child's expand product or child's assign all)
closed components
expand product on parent
expand product in parent, expand product in child
expand all in parent,triggers expand product in parent and it triggers expand product in childs component
here's first file that renders parent component :
class Offer extends React.Component {
constructor(props){
super(props);
this.state = {
checkedItems: new Map()
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
const item = e.target.name;
const isChecked = e.target.checked;
this.setState(prevState => ({
checkedItems: prevState.checkedItems.set(item, isChecked)
}));
}
render(){
const { data, country_offer, country_offer_tab, countries, } = this.props;
return (
<div className="offer-wrapper">
<div className={"offer-header " + data[2] }>
<div>
<div className="custom_checkbox">
<div className="custom_checkbox">
<input
type={"checkbox"}
name={`${data[2]}_expand_all`}
className="expand_all"
checked={this.state.checkedItems.get(`${data[2]}_expand_all`) || false}
onChange={this.handleChange}
id={`${data[2]}_expand_all`}
/>
<label htmlFor={`${data[2]}_expand_all`}>Expand all</label>
</div>
<div className="custom_checkbox">
<input
type={"checkbox"}
name={`${data[2]}_assign_all`}
className="assign_all"
checked={this.state.checkedItems.get(`${data[2]}_assign_all`) || false}
onChange={this.handleChange}
id={`${data[2]}_assign_all`}
/>
<label htmlFor={`${data[2]}_assign_all`}>Assign all products</label>
</div>
<div className="custom_checkbox">
<input
type={"checkbox"}
name={`${data[2]}_expand_product`}
className="expand_product"
checked={( this.state.checkedItems.get(`${data[2]}_expand_product`) || this.state.checkedItems.get(`${data[2]}_expand_all`) ) || false}
onChange={this.handleChange}
id={`${data[2]}_expand_product`}
/>
<label htmlFor={`${data[2]}_expand_product`}>Expand product</label>
</div>
</div>
</div>
</div>
{
(this.state.checkedItems.get(`${data[2]}_expand_all`) || this.state.checkedItems.get(`${data[2]}`+'_expand_product')) &&
<CountryOffer
country_offer_tab={country_offer_tab}
countries={countries}
data={data}
expand_all={this.state.checkedItems.get(`${data[2]}_expand_all`)}
expand_product={this.state.checkedItems.get(`${data[2]}_expand_product`)}
assign_all={this.state.checkedItems.get(`${data[2]}_assign_all`)}
/>
}
</div>
)
}
}
export default Offer;
and here is child component
class CountryOffer extends React.Component{
constructor(props){
super(props);
this.state = {
checkedItems: new Map(),
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
if (!this.props.expand_all){
const item = e.target.name;
const isChecked = e.target.checked;
this.setState(prevState => ({
checkedItems: prevState.checkedItems.set(item, isChecked)
}));
} else {
console.log('cant uncheck/check this')
}
}
render(){
const { country_offer_tab, countries, data, expand_all, expand_product, assign_all} = this.props;
return (
<div className={"offer-details " + data[2]}>
{country_offer_tab[data[2]].map((country, k) => {
return (
<div key={k}>
<div className="offer-country-header" >
<div>
<span className={"iti-flag "+(countries[k].symbol).toLowerCase()}/>
<span>{countries[k].name}</span>
</div>
<div className='custom_checkbox'>
<input
checked={assign_all ? assign_all : (this.state.checkedItems.get(`intersections_for ${data[2]}|${countries[k].id}`) || false)} onChange={this.handleChange}
type="checkbox"
name={"intersections_for "+ data[2] + '|' + countries[k].id}
id={"intersections_for "+ data[2] + '|' + countries[k].id}/>
<label className="intersections_group_label" htmlFor={"intersections_for "+ data[2] + '|' + countries[k].id}>Check/uncheck all</label>
</div>
<div className='custom_checkbox'>
<input
checked={expand_all ? expand_all : (this.state.checkedItems.get(`expand_geo ${data[2]}|${countries[k].id}`) || false)} onChange={this.handleChange}
type="checkbox"
name={"expand_geo " + data[2] + '|' + countries[k].id}
id={"expand_geo " + data[2] + '|' + countries[k].id}/>
<label htmlFor={"expand_geo "+ data[2] + '|' + countries[k].id}>Expand GEO</label>
</div>
</div>
{
(expand_all || this.state.checkedItems.get(`expand_geo ${data[2]}|${countries[k].id}`)) &&
<CountryOfferIntersections
country_offer_tab={country_offer_tab}
offer_id={data[2]}
country_id={countries[k].id}
check_all={this.state.checkedItems.get(`intersections_for ${data[2]}|${countries[k].id}`)}
assign_all={assign_all}
expand_product={expand_product}
/>
}
</div>
)
})}
</div>
)}
}
export default CountryOffer;
I will be grateful for any help

If true, the assign_all prop overrides any local state in CountryOffer
checked={assign_all ? assign_all : ...
This is a problem with not having a single source of truth. I would refactor your code so that CountryOffer is a stateless component, and keep all state in the parent Offer component.
Alternatively lifting management of shared state out of the components altogether. (For example with react context, redux, rxjs)

Related

Issue with unique key props for child elements

I have a generic Todo List built in React. Each task from user input is stored in tasks array declared in parent component <ReactTodoApp />. Tasks are rendered in child component <TodoList />. A unique key is assigned to each task in DOM element <label />. When inspecting dev tools unique ids are generating, however error is still present.
Would anyone know why I am still getting the "unique key prop" error?
Link to working application: https://codesandbox.io/s/todo-list-34udn?file=/src/App.js
JS file:
import React, { Component } from "react";
import "./styles.css";
export default class ReactTodoApp extends Component {
constructor(props) {
super(props);
this.state = {
//container for new task
input: "",
tasks: []
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleRemove = this.handleRemove.bind(this);
}
handleChange(event) {
this.setState({ input: event.target.value });
}
handleSubmit(event) {
event.preventDefault();
//condition for empty empty
if (!this.state.input) {
return;
}
//declare object to store
const newTask = {
input: this.state.input,
id: 1 + Math.random()
};
//request update to current tasks state
this.setState((state) => ({
tasks: state.tasks.concat(newTask),
input: ""
}));
}
//updater function to remove task
handleRemove(props) {
//create new task list
const newTasksList = this.state.tasks;
//remove selected item from new task list
newTasksList.splice(props, 1);
//update state for tasks
this.setState({ tasks: newTasksList });
}
render() {
return (
<div>
<h1>React Todo</h1>
<form onSubmit={this.handleSubmit} className="add-item">
<input
type="text"
value={this.state.input}
onChange={this.handleChange}
className="add-item__input"
placeholder="new item"
/>
<button type="submit" className="submit">
add item
</button>
</form>
<TodoList tasks={this.state.tasks} handleRemove={this.handleRemove} />
</div>
);
}
}
class TodoList extends React.Component {
render() {
return (
<div className="list-container">
{this.props.tasks.map((task) => (
<label keys={task.id} className="item-container">
<input type="checkbox" />
<p className="item__text">{task.input}</p>
<button onClick={this.props.handleRemove} className="remove-button">
x
</button>
<span className="custom-checkbox" />
</label>
))}
</div>
);
}
}
Just change keys={task.id} to key={task.id}

Can I decouple boolean state in React component

Like below simple react component code:
class Test extends React.Component {
constructor(props){
super(props)
this.c1 = this.c1.bind(this);
this.c2 = this.c2.bind(this);
this.state = {
a:false,
b:false
}
}
c1(e) {
this.setState({a:true, b:false})
}
c2(e) {
this.setState({a:false, b:true})
}
render() {
return (
<div>
<div>
<input name="n" type="radio" onChange={this.c1} />
<input name="n" type="radio" onChange={this.c2} />
</div>
<div>
{
this.state.a && "aa"
}
{
this.state.b && "bb"
}
</div>
</div>
)
}
}
The code simply switch displaying 'aa' or 'bb' while click the radio button. But if I add a new radio button showing 'cc' to achieve the same function. I should:
Add new state like 'c'
Add new input HTML and implement its callback
setState 'c' in this callback
All of those is ok, But I have to change the 'c1','c2' function that make my code coupling like:
class Test extends React.Component {
constructor(props){
super(props)
this.c1 = this.c1.bind(this);
this.c2 = this.c2.bind(this);
this.c3 = this.c3.bind(this);
this.state = {
a:false,
b:false,
c:false,
}
}
c1(e) {
this.setState({a:true, b:false, c:false}) // add 'c:false' which coupled
}
c2(e) {
this.setState({a:false, b:true, c:false}) // add 'c:false' which coupled
}
c3(e) {
this.setState({a:false, b:false, c:true})
}
render() {
return (
<div>
<div>
<input name="n" type="radio" onChange={this.c1} />
<input name="n" type="radio" onChange={this.c2} />
<input name="n" type="radio" onChange={this.c3} />
</div>
<div>
{
this.state.a && "aa"
}
{
this.state.b && "bb"
}
{
this.state.c && "cc"
}
</div>
</div>
)
}
}
I think this situation is very common in React. So I want decoupling my code no matter how many radio buttons I add. I do not need to change the code just add code to satisfy the 'Open Closed Principle'.
Do you have any recommendation? Thanks in advance.
I think you can do like this
class Test extends React.Component {
constructor(props){
super(props)
this.change = this.change.bind(this);
this.state = {
a:false,
b:false,
c:false,
}
}
change = statename => e => {
this.setdefault();
this.setState({
[statename]: true
});
};
setdefault(){
this.setState({
a:false,
b:false,
c:false,
});
}
render() {
return (
<div>
<div>
<input name="n" type="radio" onChange={this.change("a")} />
<input name="n" type="radio" onChange={this.change("b")} />
<input name="n" type="radio" onChange={this.change("c")} />
</div>
<div>
{
this.state.a && "aa"
}
{
this.state.b && "bb"
}
{
this.state.c && "cc"
}
</div>
</div>
)
}
}
The way that you are storing the state as keyed boolean values doesn't feel right to me given that the properties are codependent rather than independent.
Right now, you have four options for state:
{ a:false, b:false, c:false } // initial state from constructor
{ a:true, b:false, c:false } // from c1
{ a:false, b:true, c:false } // from c2
{ a:false, b:false, c:true } // from c3
Based on your setState callbacks, no more than 1 property can be true at a time.
If each property was independent, you would have 8 options (2^3) and this setup would make sense.
As it is, I recommend that you instead just store which of the values is the one that is true. You can check if an individual option is true by seeing if it matches the stored selected value.
You want your Component to work no matter how many buttons you have, so let's pass the button options as props. In terms of design patterns, this is "dependency injection". We can create a SelectOne that doesn't need to know what its options are. Here I am just expecting the options to be a string like "aa" or "bb" but you can refactor this to take an object with properties label and value.
We want to loop through the array of options and render each one. Sometimes you see this extracted into a method of the component, like this.renderOption(i), but you can also do the mapping inline inside your render().
You aren't actually using the event e in your callbacks. You could use the event to get e.target.value, assuming that you are setting the value property on your input (see MDN docs regarding the HTML markup). You could also pass the value to the callback by creating an anonymous arrow function for onChange which calls it with the correct value. I'm doing that.
Here it as a class component, since that's what you were using previously.
class SelectOne extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: undefined // this.state.selected will initially be undefined
}
}
render() {
return (
<div>
<div>
{this.props.options.map((optionName) => (
<label htmlFor={optionName}>
{optionName}
<input
type="radio"
id={optionName}
name="n"
value={optionName}
checked={this.state.selected === optionName}
onChange={() => this.setState({selected: optionName})}
/>
</label>
))}
</div>
{this.state.selected !== undefined && (
<h2>Selected: {this.state.selected}</h2>
)}
</div>
);
}
}
Here it is as a function Component. This is the newer syntax, so if you are just learning then I recommend learning with function components and hooks. Destructring the props makes it really easy to accept optional props and set their default value. I'm allowing the name property of the input to be passed in here, but defaulting to your "n" if not provided.
const SelectOne = ({options, name = "n"}) => {
// will be either a string or undefined
const [selected, setSelected] = React.useState(undefined);
return (
<div>
<div>
{options.map((optionName) => (
<label htmlFor={optionName}>
{optionName}
<input
type="radio"
id={optionName}
name={name} // from props
value={optionName}
checked={selected === optionName}
onChange={() => setSelected(optionName)}
/>
</label>
))}
</div>
{selected !== undefined && (
<h2>Selected: {selected}</h2>
)}
</div>
);
}
Either way, you would call it like this:
<SelectOne options={["aa", "bb", "cc"]} />
You could also create a specific components for a given set of options, which you can now call with no props.
const SelectABC = () => <SelectOne options={["aa", "bb", "cc"]} />;
<SelectABC/>

Get prop value from div in React

A working example of my problem can be found at:
https://codepen.io/RyanCRickert/pen/vYYQeaW
I am prop drilling a function two levels and passing that function along with an index to a rendered component. When a name is submitted it renders a new component which shows the name and div which has an onClick (X). I am trying to receive the index of where the name is located in the array which it lives so that I may splice it out when the button is clicked.
If I enter the name "Bob" for example, then click the div with the listener I can console log the event.target. Using the above example I get "<div class='person-item__X' value='0'>X</div>" for event.target and undefined for event.target.value. The value is being assigned as <div onClick={props.removeName} class="person-item__X" value={props.value}>X</div>.
Am I just unable to grab the value of a div in such a manor? Or is there something that I am missing? Thank you
Change these to your code
const PersonListItem = props => (
<div class="person-item">
<div class="person-item__name">{props.name}</div>
<div onClick={() => props.removeName(props.value)} class="person-item__X" value={props.value}>X</div>
</div>
);
Inside PeopleList replace this line
<PersonListItem key={index} name={person} value={index} removeName={(id) => props.removeName(id)} />
Inside TeamGenerator replace this line
<PeopleList people={this.state.names} removeName={(id) => this.handleRemoveName(id)} />
now in handleRemoveName you will recieve a id of the item on which X was clicked
handleRemoveName = id => {
const currentArr = this.state.names;
console.log(id);
}
In your case, to grab the value inside this div, you should use ref API.
Your code should look like this:
TeamGenerator.js
import React, { Component } from "react";
import CustomModal from "./Modal";
import PeopleList from "./PeopleList";
import "./index.css";
export default class App extends Component {
constructor(props) {
super(props);
// Create a ref
this.divTextRef = React.createRef();
this.state = {
names: [],
selectedName: ""
};
}
handleCloseModal = () => {
this.setState({
selectedName: ""
});
};
handleChange = e => {
this.setState({ name: e.target.value });
};
handleRemoveName = index => {
// Get your name and index this way
console.log("Your text: ", this.divTextRef.current.innerHTML);
console.log("Your index: ", index);
};
handleSubmit = e => {
e.preventDefault();
const currentNames = this.state.names;
if (this.state.name)
currentNames.push(
this.state.name[0].toUpperCase() + this.state.name.slice(1)
);
this.setState({
name: "",
names: currentNames
});
};
render() {
return (
<div className="container">
<CustomModal
selectedName={this.state.selectedName}
closeModal={this.handleCloseModal}
/>
<form onSubmit={this.handleSubmit}>
<label>
Add name:
<input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
</label>
<input type="submit" value="Submit" />
</form>
<div className="people-list-container">
<PeopleList
people={this.state.names}
removeName={this.handleRemoveName}
upperRef={this.divTextRef} // Pass the ref down from your Component tree
/>
</div>
</div>
);
}
}
PeopleList.js
import React from "react";
import PersonListItem from "./PersonListItem";
export default class PeopleList extends React.Component {
render() {
return (
<div className="people-container">
<div className="people-title">List of people</div>
<div className="people-list">
{this.props.people.length === 0 ? (
<div className="people-item">
<span>No people added</span>
</div>
) : (
this.props.people.map((person, index) => (
<PersonListItem
key={index}
name={person}
value={index}
removeName={() => this.props.removeName(index)} // Passing index to the removeName function of Parent
upperRef={this.props.upperRef} // Continue passing it down to PersonListItem
/>
))
)}
</div>
</div>
);
}
}
PersonListItem.js
import React from "react";
const PersonListItem = props => (
<div className="person-item">
<div ref={props.upperRef} className="person-item__name"> // Use the passed ref
{props.name}
</div>
<div
onClick={props.removeName}
className="person-item__X"
value={props.value}
>
X
</div>
</div>
);
export default PersonListItem;
The div node does not have the value like input, so you can not grab it by your old way.

Mapping Nested Checkbox not working ReactJS

I have a function which triggers children checkboxes once main checkbox is checked, and all these checkboxes are mapped from JSON. The main checkboxes (Highest order) and all of its children checkboxes (2nd order) under them are shown on check and its working great, what i am trying to show is the children of those children of the main checkboxes (3rd order).
Basically to show all three orders under each other on check, and add the 3rd order to my current code, so Options Group shows Options, and under Options is what i want to show, which are Option 1, Option 2, option 3 and so on..
The checkbox values are passed as props from Checkbox.js to Itemlist.js where the fetch/map happens.
As I was trying to achieve this, I have been able to show each of these 3rd order checkboxes but each one alone instead of showing them as nested checkboxes. I've tried to include it in the existing mapping function to show them all as nested checkboxes as mentioned but it couldn't work it only works if it was mapped alone instead of the current mapped path which is const selectedItem. while the mapping path of the targeted checkboxes (3rd level) are added in Itemlist.js as const selectedMod,
Main Snippet : https://codesandbox.io/embed/6jykwp3x6n?fontsize=14
What I reached for so far to show the targeted checkboxes but individually: https://codesandbox.io/embed/o932z4yr6y?fontsize=14
Checkbox.js
import React from "react";
import "./Checkbox.css";
class Checkboxes extends React.Component {
constructor(props) {
super(props);
this.state = {
currentData: 0,
limit: 2,
checked: false
};
}
selectData(id, event) {
let isSelected = event.currentTarget.checked;
if (isSelected) {
if (this.state.currentData < this.props.max) {
this.setState({ currentData: this.state.currentData + 1 });
} else {
event.preventDefault();
event.currentTarget.checked = false;
}
} else {
if (this.state.currentData >= this.props.min) {
this.setState({ currentData: this.state.currentData - 1 });
} else {
event.preventDefault();
event.currentTarget.checked = true;
}
}
}
render() {
const input2Checkboxes =
this.props.options &&
this.props.options.map(item => {
return (
<div className="inputGroup2">
{" "}
<div className="inputGroup">
<input
id={this.props.childk + (item.name || item.description)}
name="checkbox"
type="checkbox"
onChange={this.selectData.bind(
this,
this.props.childk + (item.name || item.description)
)}
/>
<label
htmlFor={this.props.childk + (item.name || item.description)}
>
{item.name || item.description}{" "}
</label>
</div>
</div>
);
});
return (
<form className="form">
<div>
{/** <h2>{this.props.title}</h2>*/}
<div className="inputGroup">
<input
id={this.props.childk + this.props.name}
name="checkbox"
type="checkbox"
checked={this.state.checked}
onChange={this.selectData.bind(
this,
this.props.childk + this.props.uniq
)}
onChange={() => {
this.setState({
checked: !this.state.checked,
currentData: 0
});
}}
/>
<label htmlFor={this.props.childk + this.props.name}>
{this.props.name}{" "}
</label>
</div>{" "}
{this.state.checked ? input2Checkboxes : undefined}
</div>
</form>
);
}
}
export default Checkboxes;
Itemlist.js Where the mapping function happen
...
const selectedItem =
selectedChild.children && selectedChild.children.length
? selectedChild.children[this.state.itemSelected]
: null;
...
<div>
{selectedItem &&
selectedItem.children &&
selectedItem.children.map((item, index) => (
<Checkboxes
key={index}
name={item.name || item.description}
myKey={index}
options={item.children}
childk={item.id}
max={item.max}
min={item.min}
/>
))}
</div>
...

Referencing data in stateless React components

I made a stateless component with an internal variable to reference an input, as below. This is working fine.
const MyStatelessComp = ({ team, teamProgress, onSet, editing, enableEdit }) => {
let input
return (
<div>
<div className="team__goal-target_header" >Team's Savings Target</div>
<div className="team__goal-target_value" >
M$
<input
ref={ el => input = el }
style={{width: '75px', border: 'none'}}
onChange={() => onSet({teamId: team.id, goalValue: parseInt(input.value, 10) || 0}) }
/>
<div
ref={ el => input }
style={{
display: !input || (!isNaN(parseFloat(input.value)) && isFinite(input.value)) ? 'none' : 'block'
}}
>Must be numeric</div>
</div>
</div>
)
}
I want to validate input and display a notification Must be numeric is the anything that cannot be converted to a number is entered into my input field. That is not working however. How do I make input in the context of the "warning div" reference the value of the input?
Realize that this is not an unorthodox way to working with stateless components, but it would save me lots of pain.
Thank you.
Why use a stateless component when he can be a simple statefull component ?
class App extends Component {
constructor(props) {
super(props);
this.state = {
value: null
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({ value: event.target.value });
}
render() {
const isNumber = !isNaN(this.state.value);
return (
<div>
<div className="team__goal-target_header">Team's Savings Target</div>
<div className="team__goal-target_value">
M$
<input
style={{ width: "75px", border: "none" }}
onChange={this.handleChange}
value={this.state.value}
/>
{isNumber ? "" : <div>Must be numeric</div>}
</div>
</div>
);
}
}
You can also toggle the div content or create a new alert component and toggle this component.

Categories

Resources