I'm struggling to create multiple Checkbox filtering in React with Material-UI. The difficult is that checkbox options are created dynamically from received data and should be split by each type of Select components
But I can't properly create states to manage them in the App.
Any ideas how to do it correctly?
Options Component
function filterDuplicate(arr) {
return arr.filter((elem, index, array) => array.indexOf(elem) === index);
}
export default function Options({ stat }) {
const [form, setForm] = useState({
publicationType: "",
termType: "",
reportGroup: "",
reportState: "",
reportFormat: ""
});
const publicationTypes = filterDuplicate(
stat.map((data) => data.publicationType)
);
const termTypes = filterDuplicate(stat.map((data) => data.termType));
const reportGroups = filterDuplicate(stat.map((data) => data.reportGroup));
const reportStates = filterDuplicate(stat.map((data) => data.reportState));
const reportFormats = filterDuplicate(stat.map((data) => data.reportFormat));
function handleSubmit(e) {
e.preventDefault();
console.log(form);
}
return (
<>
<form onSubmit={handleSubmit} className="options">
<Select type="Publication type" options={publicationTypes} />
<Select type="Term type" options={termTypes} />
<Select type="Report group" options={reportGroups} />
<Select type="Status" options={reportStates} />
<Select type="File Type" options={reportFormats} />
<Button variant="contained" color="secondary" type="submit">
RESET
</Button>
</form>
</>
);
}
Options.propTypes = {
stat: PropTypes.arrayOf(PropTypes.shape({})).isRequired
};
Select Component
export default function Select({ type, options }) {
const [check, setCheck] = useState([]);
const [value, setValue] = useState("");
const classes = useStyles();
const handleChange = (e) => {
if (e.target.checked) {
setCheck([...check, e.target.value]);
} else {
setCheck(check.filter((id) => id !== e.target.value));
}
const str = check.join(", ");
setValue(str);
};
return (
<>
<FormControl className={classes.formControl}>
<InputLabel id="select-label" className={classes.label}>
{type}
</InputLabel>
<MaterialSelect labelId="select-label" id="input-select">
{options.map((option) => (
<Checkbox option={option} key={option} onChange={handleChange} />
))}
</MaterialSelect>
</FormControl>
</>
);
}
Checkbox Component
const Checkbox = React.forwardRef(({ option, onChange }, ref) => {
return (
<div ref={ref}>
<FormControlLabel
control={<MaterialCheckbox onChange={onChange} color="primary" />}
label={option}
value={option}
/>
</div>
);
});
Checkbox.propTypes = {
option: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
};
export default Checkbox;
https://codesandbox.io/s/checkbox-filter-vqex7?file=/src/Options.js:0-1593
The problem is you only pass data from parent to child. On App.js they are two separate components. You should write a function on App.js that connects these two components.
Your App.js
export default function App() {
const [stat, setState] = useState([]);
useEffect(() => {
setState(data);
}, []);
return (
<div className="App">
<div>
<div>
<h1>Information</h1>
</div>
<Options stat={stat} />
<Data info={stat} />
</div>
</div>
);
}
App.js when filtering method added
export default function App() {
const [stat, setState] = useState([]);
useEffect(() => {
setState(data);
}, []);
const handleFilter = (selectedValue) => {
console.log(selectedValue);
//apply filtering here and setState again
}
return (
<div className="App">
<div>
<div>
<h1>Information</h1>
</div>
<Options stat={stat} handleFilter={handleFilter} />
<Data info={stat} />
</div>
</div>
);
}
Options.js
export default function Options({ stat, handleFilter }) {
const [form, setForm] = useState({
publicationType: "",
termType: "",
reportGroup: "",
reportState: "",
reportFormat: ""
});
const publicationTypes = filterDuplicate(
stat.map((data) => data.publicationType)
);
const termTypes = filterDuplicate(stat.map((data) => data.termType));
const reportGroups = filterDuplicate(stat.map((data) => data.reportGroup));
const reportStates = filterDuplicate(stat.map((data) => data.reportState));
const reportFormats = filterDuplicate(stat.map((data) => data.reportFormat));
function handleSubmit(e) {
e.preventDefault();
console.log(form);
}
return (
<>
<form onSubmit={handleSubmit} className="options">
<Select
type="Publication type"
options={publicationTypes}
handleFilter={handleFilter}
/>
<Select
type="Term type"
options={termTypes}
handleFilter={handleFilter}
/>
<Select
type="Report group"
options={reportGroups}
handleFilter={handleFilter}
/>
<Select
type="Status"
options={reportStates}
handleFilter={handleFilter}
/>
<Select
type="File Type"
options={reportFormats}
handleFilter={handleFilter}
/>
<Button variant="contained" color="secondary" type="submit">
RESET
</Button>
</form>
</>
);
}
Select.js
export default function Select({ type, options, handleFilter }) {
const [check, setCheck] = useState([]);
const [value, setValue] = useState("");
const classes = useStyles();
const handleChange = (e) => {
if (e.target.checked) {
handleFilter(e.target.value);
setCheck([...check, e.target.value]);
} else {
setCheck(check.filter((id) => id !== e.target.value));
}
const str = check.join(", ");
setValue(str);
};
return (
<>
<FormControl className={classes.formControl}>
<InputLabel id="select-label" className={classes.label}>
{type}
</InputLabel>
<MaterialSelect labelId="select-label" id="input-select">
{options.map((option) => (
<Checkbox option={option} key={option} onChange={handleChange} />
))}
</MaterialSelect>
</FormControl>
</>
);
}
With that structure, you can receive checked value on App.js and filter data.
Related
I want to add a new input field on button click and add the integer value of that input field to an array in react
const [price, setPrice] = useState([])
const [count, setCount] = useState([1])
const addNewTextField = () => setCount(prev => [...prev,1])
const addInputValue= () => {
setPrice()
console.log(price)
}
<Button onClick={addNewTextField}>Add TextField</Button >
{
count.map((item, i) => {
return (
<TextField key={i} value={item.value} id={i} type='text' />
)
})
}
<Button onClick={addInputValue}>submit</Button >
first input value is 100,
second input value is 200,
result should be like this when I add new input field:
[100,200]
Try like below. You can keep only price state.
import { useState } from "react";
const App = () => {
const [price, setPrice] = useState([""]);
const addNewTextField = () => setPrice((prev) => [...prev, ""]);
const addInputValue = (i, newValue) => {
console.log(i, newValue);
setPrice((prevState) =>
prevState.map((value, valueIndex) =>
valueIndex === i ? newValue : value
)
);
};
console.log(price);
return (
<>
<button onClick={addNewTextField}>Add TextField</button>;
{price.map((item, i) => {
return (
<input
key={i}
placeholder={`input ${i}`}
// value={item}
id={i}
type="text"
onChange={(e) => addInputValue(i, e.target.value)}
/>
);
})}
<button onClick={addInputValue}>submit</button>
</>
);
};
export default App;
Code sandbox
const [price, setPrice] = useState([]);
const [count, setCount] = useState([1]);
const [value, setValue] = useState("");
const addNewTextField = () => setCount(prev => [...prev,prev + 1]);
const addInputValue= () => {
setPrice(price.concat(value));
console.log(price)
}
return(
<div>
<Button onClick={addNewTextField}>Add TextField</Button >
{
count.map((item, i) => {
return (
<TextField key={i} value={value} id={i} type='text'
onChange={e => setValue(e.target.value)} />
);
})
}
<Button onClick={addInputValue}>submit</Button >
</div>
);
Button and TextField components:
function Button(props){
return(
<button onClick={props.onClick}>{props.children}</button>
);
}
function TextField(props){
return(
<input type="text" id={props.id} value={props.value}
onChange={e=>props.onChange(e)}>
);
}
I want to add to Chip an startIcon={<Icon />}
when click on a Chip.
The state of the icon is managed by chipsState.
In this code,
the state of all chips would change.
How can I change only the chipsState of the element that is clicked?
In this code, the state of all chips will change.
How can I change only the chipsState of the element that is clicked?
const Modal:React.FC<Props>= (props) => {
const {modalData} = props;
const [chipsState, setChipsState] = useState(false);
const onChipClick = (element:any) => {
setChipsState(chipsState => !chipsState);
}
return (
<div>
{
modalData.symtoms.map((element:any, index:number) => (
<div key={index}>
<Chip onClick={() => onChipClick(element)} startIcon={chipsState && <Icon />}>{element.description}</Chip>
</div>
))}
</div>
);
}
export default Modal;
To handle local state (and better testing), you should create a new custom Chip component with dedicated chipState.
interface CustomChipProps {
description: string
}
const CustomChip = (props: CustomChipProps) => {
const [chipState, setChipState] = useState(false);
return <Chip onClick={() => setChipState(prev => !prev)} startIcon={chipState && <Icon />}>{props.description}</Chip>;
}
const Modal:React.FC<Props>= (props) => {
const {modalData} = props;
return (
<div>
{
modalData.symtoms.map((element:any, index:number) => (
<div key={index}>
<CustomChip description={element.description} />
</div>
))}
</div>
);
}
export default Modal;
You can achieve your desired output by changing chipState state from boolean to object.
So first let's change to object state instead of boolean
const [chipsState, setChipsState] = useState({});
Now we will change onChipClick function to change value of selected chip state
const onChipClick = (element:any) => {
setChipsState({...chipsState, chipsState[element]: !chipsState[element]});
}
And finally we will read correct value of each chipsState element.
<Chip onClick={() => onChipClick(element)} startIcon={chipsState[element] && <Icon />}>{element.description}</Chip>
You can try like the following
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import { Grid, Row } from "react-flexbox-grid";
const ChipSet = ({ symtomsData }) => {
const data = symtomsData.map((symtom) => ({ ...symtom, isSelcted: false }));
const [chipSets, setChipSets] = useState(data);
const onSelectChipSet = useCallback(
(e, index) => {
const updatedChipSets = chipSets.map((chip, i) =>
i === index ? { ...chip, isSelcted: e.target.checked } : chip
);
setChipSets(updatedChipSets);
},
[chipSets]
);
console.log("chipSets", chipSets);
return (
<div>
<h1>Symtoms Data</h1>
{chipSets.map((x, i) => (
<div key={i}>
<label>
<input
onChange={(e) => onSelectChipSet(e, i)}
type="checkbox"
value={x.isSelcted}
/>
{x.description}
</label>
</div>
))}
</div>
);
};
class App extends React.Component {
render() {
const symtomsData = [
{
description: "mild"
},
{
description: "cold"
}
];
return (
<Grid>
<Row>
<ChipSet symtomsData={symtomsData} />
</Row>
</Grid>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
Full error message:
Error: Element type is invalid: expected a string (for built-in
components) or a class/function (for composite components) but got:
object. You likely forgot to export your component from the file it's
defined in, or you might have mixed up default and named imports.
usually this is something to do with components not being imported or exported correctly but everything looks fine to me. Any ideas what the issue could be.
import React, { useRef, useState, useEffect } from 'react';
import Form from 'react-bootstrap/Form';
import ScrollToTop from './ScollToTop';
const StoreDirectory = (props) => {
const [filteredValues, setValues] = useState(props.storesData);
const [initialValues, setInitialValues] = useState(props.storesData);
const [storeCategorys, setStoreCategories] = useState(props.storesCategorysData);
const alphabet = "abcdefghijklmnopqrstuvwxyz";
const alphabetIntoArray = alphabet.split("");
const itemsRef = useRef([]);
useEffect(() => {
itemsRef.current = itemsRef.current.slice(0, alphabetIntoArray.length);
}, [alphabetIntoArray])
const AlphaButtons = () => {
const goToSelection = (event, index) => {
if(itemsRef.current[index]) {
itemsRef.current[index].scrollIntoView({
behaviour: "smooth",
block: "nearest"
})
}
}
return (
<>
{alphabet.split("").map((item, index) => (
<button key={index} onClick={(e) => goToSelection(e, index)}>{item}</button>
))}
</>
)
}
const DropDown = () => {
const handleOnchange = (event) => {
const filter = event.target.id;
const initialState = [...initialValues]
setValues(initialState.filter(store =>
{return (store.store-category.indexOf(filter) >= 0)}
));
}
const defaultSelectMessage = 'select a category';
return (
<Form>
<Form.Group controlId="exampleForm.SelectCustom">
<Form.Label>Custom select</Form.Label>
<Form.Control defaultValue={defaultSelectMessage} onChange={(e) => handleOnchange(e)} as="select" custom>
<option hidden disabled value={defaultSelectMessage}>{defaultSelectMessage}</option>
{storeCategorys.map((item, index) => (
<option id={item.id} key={index}>{item.name}</option>
))}
</Form.Control>
</Form.Group>
</Form>
);
}
const Filter = () => {
return (
<div>
{alphabet.split("").map((c, i) => {
return (
<>
{filteredValues
.filter(store => store.title.rendered.startsWith(c)).length === 0
? <h1 ref={ el => itemsRef.current[i] = el } className={'Grey'}>{c}</h1>
: <h1 ref={ el => itemsRef.current[i] = el } className={'notGrey'}>{c}</h1>
}
{filteredValues
.filter(store => store.title.rendered.startsWith(c))
.map((item, index) => (
<li key={index}>{item.title.rendered}</li>
))}
</>
);
})}
</div>
);
}
return (
<>
<DropDown />
<AlphaButtons />
<Filter />
<ScrollToTop />
</>
)
}
export default StoreDirectory;
I have a React Form app with name and description fields.
The form data is held in a local state object using Hooks:
const [data,setData] = useState({name: '', description: ''}).
The <Form /> element creates inputs and passes their value using <Field initialValue ={data.name} />
Within the <Field /> element, this initialValue is passed to the state, which controls the input value (updated onChange):
const [value,setValue] = useState(initialValue).
But if I reset the data object (see handleResetClick function), the inputs don't clear (even though the data object clears). What am I doing wrong? I thought that changing the data would cause a re-render and re-pass initialValue, resetting the input.
Codepen example here - when I type in the inputs, the data object updates, but when I click Clear, the inputs don't empty.
function Form() {
const [data, setData] = React.useState({name: '', description: ''});
React.useEffect(() => {
console.log(data);
},[data]);
const onSubmit = (e) => {
// not relevant to example
e.preventDefault();
return;
}
const handleResetClick = () => {
console.log('reset click');
setData({name: '', description: ''})
}
const onChange = (name, value) => {
const tmpData = data;
tmpData[name] = value;
setData({
...tmpData
});
}
return (
<form onSubmit={onSubmit}>
<Field onChange={onChange} initialValue={data.name} name="name" label="Name" />
<Field onChange={onChange} initialValue={data.description} name="description" label="Description" />
<button type="submit" className="button is-link">Submit</button>
<button onClick={handleResetClick} className="button is-link is-light">Clear</button>
</form>
)
}
function Field(props) {
const {name, label, initialValue, onChange} = props;
const [value, setValue] = React.useState(initialValue);
return (
<div>
<div className="field">
<label className="label">{label}</label>
<div className="control">
<input
name={name}
className="input"
type="text"
value={value}
onChange={e => {
setValue(e.target.value)
onChange(name, e.target.value)
}}
/>
</div>
</div>
</div>
)
}
class App extends React.Component {
render() {
return (
<div className="container">
<Form />
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
)
On handleResetClick you change the data state of Form, but it doesn't affect its children.
Try adding a listener for initialValue change with useEffect:
function Field(props) {
const { name, label, initialValue, onChange } = props;
const [value, setValue] = React.useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return ...
}
You may be better off having Field as a controlled component (ie it's state is managed by the parent component rather than maintaining its own state). In this example I've swapped in value instead of initialValue and simply passed that down as props to the field. onChange then calls the parent method and updates the state there (which is automatically passed back down to the field when it renders):
const { useState, useEffect } = React;
function Form() {
const [data, setData] = React.useState({
name: '',
description: ''
});
useEffect(() => {
console.log(data);
}, [data]);
const onSubmit = (e) => {
e.preventDefault();
return;
}
const handleResetClick = () => {
setData({name: '', description: ''})
}
const onChange = (e) => {
const { target: { name, value } } = e;
setData(data => ({ ...data, [name]: value }));
}
return (
<form onSubmit={onSubmit}>
<Field onChange={onChange} value={data.name} name="name" label="Name" />
<Field onChange={onChange} value={data.description} name="description" label="Description" />
<button type="submit" className="button is-link">Submit</button>
<button onClick={handleResetClick} className="button is-link is-light">Clear</button>
</form>
)
}
function Field(props) {
const {name, label, value, onChange} = props;
return (
<div>
<div className="field">
<label className="label">{label}</label>
<div className="control">
<input
name={name}
className="input"
type="text"
value={value}
onChange={onChange}
/>
</div>
</div>
</div>
)
}
function App() {
return (
<div className="container">
<Form />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm trying to add a whole array to useState variable
import React, { Fragment, useState, useEffect } from 'react';
import { Form, Button, Popover, OverlayTrigger } from 'react-bootstrap';
const Filter = props => {
const [formData, setFormData] = useState({
filter: ''
});
const [items, setItems] = useState([]);
const [retrievedItems, setRetrievedItems] = useState([]);
const addToFilter = newFilter => {
let retrievedFilter = ["da vinci","paris", "london"];
console.log(retrievedFilter);
if (retrievedFilter.length > 0) {
setRetrievedItems([...retrievedItems, retrievedFilter]);
retrievedFilter = 0;
setRetrievedItems([...retrievedItems, newFilter]);
} else {
setItems([...items, newFilter]);
}
console.log('items are: ', items);
console.log('retrieve filter', props.retrievedFilter);
console.log('retrieved items: ', retrievedItems);
};
useEffect(() => {
console.log('useEffect ', retrievedItems);
}, [retrievedItems]);
const deleteFilter = index => {
// props.retrievedFilter.splice(index, 1);
items.splice(index, 1);
setItems([...items]);
// setItems([...props.retrievedFilter, ...items]);
console.log(items);
};
const { filter } = formData;
const onChange = e => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const onSubmit = e => {
e.preventDefault();
addToFilter(filter);
// Passing filter data up (i.e: to components that use <Filter />)
props.filterData(filter);
//Close the Popover
document.body.click();
};
const popover = (
<Popover id="popover-basic">
<Form>
<Form.Group controlId="formGroupEmail">
<Form.Label>Add New Filter</Form.Label>
<Form.Control
type="text"
placeholder="New Filter"
name="filter"
onChange={e => onChange(e)}
/>
</Form.Group>
<Button variant="dark" type="submit" onClick={e => onSubmit(e)}>
Add
</Button>
</Form>
</Popover>
);
return (
<Fragment>
<label>
<p className="filter-title">{props.title}</p>
</label>
<div className={props.className ? props.className : 'filter'}>
{!props.retrievedFilter
? items.map((item, index) => {
return (
<div className="filter-text" key={index}>
{item}
<Button
className="filter-button"
size="sm"
onClick={() => deleteFilter(index)}
>
X
</Button>
</div>
);
})
: props.retrievedFilter.map((item, index) => {
return (
<div className="filter-text" key={index}>
{item}
<Button
className="filter-button"
size="sm"
onClick={() => deleteFilter(index)}
>
X
</Button>
</div>
);
})}
<OverlayTrigger
trigger="click"
placement="right"
rootClose
overlay={popover}
>
<p className="text-field">Type new one</p>
</OverlayTrigger>
</div>
</Fragment>
);
};
export default Filter;
however retrievedItems shows as an empty array in the console.
any help would be appreciated.
setState is async. You have to console.log inside an effect hook with the array as a parameter.
useEffect(() => console.log(retrieved_items), [ retrievedItems ])
The second parameter ensures that the effect fires in repose to a change in the values passed to it.
Per my comment, here is a code snippet that I think does what you want.
I couldn't get it running in SO but here's a codepen: https://codepen.io/anon/pen/PrYYmz?editors=1010 (watch the chrome console as you add items)
import React, {
Fragment,
useState,
useEffect
} from 'react';
const Filter = props => {
const [formData, setFormData] = useState({filter: ''});
const [items, setItems] = useState([]);
const [retrievedItems, setRetrievedItems] = useState([]);
const addToFilter = newFilter => {
let retrievedFilter = ["da vinci", "paris", "london"];
console.log('add', retrievedFilter);
if (retrievedFilter.length > 0) {
setRetrievedItems([...retrievedItems, retrievedFilter]);
retrievedFilter = 0;
setRetrievedItems([...retrievedItems, newFilter]);
} else {
setItems([...items, newFilter]);
}
console.log('items are: ', items);
console.log('retrieve filter', props.retrievedFilter);
console.log('retrieved items: ', retrievedItems);
};
useEffect(() => {
console.log('useEffect ', retrievedItems);
}, [retrievedItems]);
const deleteFilter = index => {
// props.retrievedFilter.splice(index, 1);
items.splice(index, 1);
setItems([...items]);
// setItems([...props.retrievedFilter, ...items]);
console.log(items);
};
const {filter} = formData;
const onChange = e => {
setFormData({ ...formData,
[e.target.name]: e.target.value
});
};
const onSubmit = e => {
e.preventDefault();
addToFilter(filter);
// Passing filter data up (i.e: to components that use <Filter />)
//props.filterData(filter);
//Close the Popover
document.body.click();
};
return (
<Fragment >
<label >
<p className = "filter-title" > {
props.title
} </p> </label> <
div className = {
props.className ? props.className : 'filter'
} > {!props.retrievedFilter ?
items.map((item, index) => {
return ( <
div className = "filter-text"
key = {index} > {item} <button className = "filter-button" size = "sm" onClick = {() => deleteFilter(index)}>X</button></div>
);
}) :
props.retrievedFilter.map((item, index) => {
return ( <div className = "filter-text" key = {index} > {item} <button className = "filter-button" size = "sm" onClick = {() => deleteFilter(index)} >X</button></div>);})} <input type = "text" placeholder = "New Filter" name = "filter" onChange = {e => onChange(e) }/>
<button variant = "dark" type = "submit" onClick = {e => onSubmit(e)} >Add</button>
</div>
</Fragment>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>