Storing JSX elements inside React State? - javascript

I hope I'm not the only one who's facing with the problem of processing large amount of data to create JSX elements. The problem with that in my solution, that every time a state or prop changes inside a component, it does the whole processing to create it's children, unnecessarily.
Here's my component:
import React from "react";
import PropTypes from "prop-types";
import Input from "./Input";
import { validate } from "./validations";
class Form extends React.Component {
static propTypes = {
//inputs: array of objects which contains the properties of the inputs
//Required.
inputs: PropTypes.arrayOf(
PropTypes.shape({
//id: identifier of the input.
//Required because the form will return an object where
//the ids will show which value comes from which input.
id: PropTypes.string.isRequired,
//required: if set to false, this field will be accepted empty.
//Initially true, so the field needs to be filled.
//Not required.
required: PropTypes.bool,
//type: type of the input.
//Initially text.
//Not required.
type: PropTypes.string,
//tag: htmlTag
//Initially "input", but can be "select".
//Not required.
tag: PropTypes.oneOf(["input", "select", "radio", "custom-select"]),
//options: options for <select>
//Not required.
options: PropTypes.arrayOf(
PropTypes.oneOfType([
PropTypes.shape({
//value: value of the option
value: PropTypes.string.isRequired,
//displayValue: if defined it
//will be the displayed of the text
displayValue: PropTypes.string,
//element: for tag: custom-select
//must be a JSX element
element: PropTypes.object
}),
//if the value is equal to the display value,
//it can be declared as string
PropTypes.string
])
),
//minLen: minimum length accepted for the field.
//If the input doesn't passes, it will not be valid.
//Initially 0, not required.
minLen: PropTypes.number,
//maxLen: maximum length accepted for the field.
//The characters over the maximum will be cut.
//Initially 20000, not required.
maxLen: PropTypes.number,
//className: class of the container of the input.
//The structure of the input component is:
//<div className={className}>
//[<label>{label}</label>]
//<input>
//<p>{errorMessage}</p>
//</div>
//Not required.
className: PropTypes.string,
//placelholder: placeholder of the input field.
//Not required.
placeholder: PropTypes.string,
//label: label of the input field.
//Not required.
label: PropTypes.string,
//validation: function, which checks
//if the value entered is valid.
//Must return a string of an error message if isn't valid.
//Executes if:
//-the user clicks outside the field if it has focus
//-the user clicks on submit button
//Not required.
validation: PropTypes.func,
//process: function, which processes the input entered
//If the form is ready to submit,
//the field's value will be processed with it.
//Not required.
process: PropTypes.func,
//initValue: initial value of the field.
//Not required.
initValue: PropTypes.string,
//submitOnEnter: if the user presses the "Enter" key,
//it submits the form.
//works only with "input" tags
//Initially true, not required.
submitOnEnter: PropTypes.bool
})
).isRequired,
//onSubmit: function which processes the form.
//Must receive the form as a parameter, which is the shape of:
//{[id]: value, [id]: value}
//Required.
onSubmit: PropTypes.func.isRequired,
//otherElements: addictional elements to the form.
//The function must return JSX elements
//Not required.
otherElements: PropTypes.arrayOf(PropTypes.func),
//className: className of the form.
//Not required.
className: PropTypes.string,
//buttonTitle: the button's title.
//Initially "Submit".
//Not required.
buttonText: PropTypes.string,
//focusOn: puts focus on specified element on specified event.
//Not required.
focusOn: PropTypes.shape({
id: PropTypes.string.isRequired,
event: PropTypes.bool
}),
//collect: collects the specified element id's into one parent.
//Needs to be an object, where the key is the classname of the container,
//and the values are an array of the id's of the items needs to collect
//Not required.
collect: PropTypes.objectOf(PropTypes.array)
}
constructor (props) {
super(props);
this.state = {};
}
componentDidMount () {
const { inputs } = this.props;
const inputProps = inputs.reduce((obj, {id, initValue: value = ""}) => {
obj[id] = { value, error: null};
return obj;
}, {});
this.setState({...inputProps}) //eslint-disable-line
}
//process with max-length checking
completeProcess = (val, process, maxLen) => process(val).substr(0, maxLen)
handleSubmit = () => {
const inputProps = this.state;
const errors = {};
const processedValues = {};
this.props.inputs.forEach(
({
id,
required = true,
validation,
process = v => v,
minLen = 0,
maxLen = 20000
}) => {
const { value } = inputProps[id];
errors[id] = validate(
{value, validation, required, minLen}
);
processedValues[id] = this.completeProcess(
value, process, maxLen
);
}
);
const errorFree = Object.values(errors).every(e => !e);
if (errorFree) {
this.props.onSubmit(processedValues);
} else {
const newState = {};
Object.keys(inputProps).forEach(id => {
const { value } = inputProps[id];
const error = errors[id];
newState[id] = { value, error };
});
this.setState(newState);
}
}
renderInputs = () => {
const { collect } = this.props;
const collectors = { ...collect };
const elements = [];
this.props.inputs.forEach(({
id,
validation,
required = true,
submitOnEnter = true,
label,
initValue = "",
className = "",
placeholder = "",
type = "text",
tag = "input",
options = [],
process = v => v,
minLen = 0,
maxLen = 20000
}) => {
const value = this.state[id] ? this.state[id].value : initValue;
const error = this.state[id] ? this.state[id].error : null;
const onBlur = () => {
const { followChange } = this.state[id];
if (!followChange) {
this.setState({ [id]: {
...this.state[id],
error: validate({value, validation, required, minLen}),
followChange: true
}});
}
};
const onChange = newValue => {
const { followChange } = this.state[id];
const newState = {
...this.state[id],
value: this.completeProcess(newValue, process, maxLen)
};
if (followChange) {
newState.error = validate(
{value: newValue, validation, required, minLen}
);
}
this.setState({ [id]: newState });
};
const onEnterKeyPress = ({ key }) => submitOnEnter && key === "Enter" && this.handleSubmit(); //eslint-disable-line
const focus = () => {
const { focusOn = {} } = this.props;
if (id === focusOn.id && focusOn.event) {
return true;
} else {
return false;
}
};
const input = (
<Input
className={className}
label={label}
placeholder={placeholder}
value={value}
onBlur={onBlur}
onChange={onChange}
type={type}
tag={tag}
options={options}
key={id}
id={id}
error={error}
onEnterKeyPress={onEnterKeyPress}
focusOn={focus()}
/>
);
if (Object.keys(collectors).length) {
let found = false;
Object.keys(collect).forEach(parentId => {
const children = collect[parentId];
children.forEach((childId, i) => {
if (childId === id) {
collectors[parentId][i] = input;
found = true;
}
});
});
if (!found) {
elements.push(input);
}
} else {
elements.push(input);
}
});
const collectorElements = Object.keys(collectors).map(parentId => (
<div className={parentId} key={parentId}>
{collectors[parentId]}
</div>
));
return [
...elements,
...collectorElements
];
}
render () {
const {
className,
buttonText = "Submit",
otherElements
} = this.props;
return (
<div className={`form ${className}`}>
{this.renderInputs()}
{otherElements && otherElements.map((e, i) => e(i))}
<button onClick={this.handleSubmit}>{buttonText}</button>
</div>
);
}
}
export default Form;
Where Input is a Pure Component, but it's not relevant to the question. So as you can see, I tried to make the component as flexible as I can, where I need to define just a few attributes to create almost every type of form. But this flexibility costs much, as it needs to process the properties every time the component renders. As this.props.inputs will not change 100%, it wouldn't cause issues if it would be created just when the component mounts. Is it a good solution to move renderInputs to componentDidMount, and store the elements into this.state instead of returning them inside render? If not, what would be the most efficient solution for those problems?

Related

Material UI Textfield 'error' and 'helper text' for elements in a loop

I am trying to create an application that has dynamic text field input using MUI textfield. There are two fields - From and To. When the "Add New Field" button is clicked, it generates two new fields. These two are part of the state object. Now, if the user enters a value in "To" field which is lesser than "from" field, it's supposed to display an error below the field as defined in the 'helper text'. However, in my case, the error appears in all the 'To' fields even though the error is supposed to appear only in the row where the input is wrong. It's repeating it in all the rows. How do I fix this?
The code is as follows. It can be reproduced in sandbox directly.
import "./styles.css";
import React from "react";
import { Button, Grid, Paper } from "#mui/material";
import { TextField } from "#mui/material";
interface Props {}
interface State {
serialInputObjects: any;
}
var fromErrorMessage = "";
var toErrorMessage = "";
class SerialQRScanClass extends React.PureComponent<Props, State> {
state = {
serialRegistrationTracker: [],
serialInputObjects: {
//0: { from: "", to: "", except: "" } }
}
};
calculation = (key) => {
let errors = this.getFromToSerialErrorMessages(
this.state.serialInputObjects[key]["from"],
this.state.serialInputObjects[key]["to"]
);
fromErrorMessage = errors.fromErrorMessage;
toErrorMessage = errors.toErrorMessage;
console.log(`Key ${key} From error message - ` + fromErrorMessage);
console.log("To error message - " + toErrorMessage);
};
getSerialCodeErrorMessage = (serialCode) => {
if (!serialCode) return "";
if (String(serialCode).match(/[^0-9,]+/)) {
return "Enter only numbers";
}
return "";
};
getFromToSerialErrorMessages = (fromSerial, toSerial) => {
const fromErrorMessage = this.getSerialCodeErrorMessage(fromSerial);
let toErrorMessage = this.getSerialCodeErrorMessage(toSerial);
if (!fromErrorMessage && !toErrorMessage) {
const diff = parseInt(toSerial) - parseInt(fromSerial);
if (diff < 0) toErrorMessage = "To lower than starting point";
}
return { fromErrorMessage, toErrorMessage };
};
handleAdd = () => {
const objectLength = Object.keys(this.state.serialInputObjects).length;
console.log(objectLength);
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[objectLength]: { from: "", to: "", except: "" }
}
}));
console.log(this.state.serialInputObjects);
};
handleChangeFromSerials = (key: any, data: string) => {
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[key]: { ...prevState.serialInputObjects[key], from: data }
}
}));
console.log(this.state.serialInputObjects);
this.calculation(key);
};
handleChangeToSerials = (key: any, data: string) => {
this.setState((prevState) => ({
...prevState,
serialInputObjects: {
...prevState.serialInputObjects,
[key]: { ...prevState.serialInputObjects[key], to: data }
}
}));
console.log(this.state.serialInputObjects);
this.calculation(key);
};
render() {
return (
<Paper elevation={3} className="abc">
<Button onClick={this.handleAdd}>ADD NEW FIELD</Button>
{Object.keys(this.state.serialInputObjects).map((key) => (
<div key={key}>
<Grid container alignItems="flex-end">
<Grid item className="bcd">
<TextField
fullWidth
label={"FROM"}
placeholder={"Ex.100"}
value={this.state.serialInputObjects[key]["from"]}
onChange={(e) =>
this.handleChangeFromSerials(key, e.target.value)
}
error={Boolean(fromErrorMessage) || false}
helperText={fromErrorMessage}
margin="none"
size="small"
/>
</Grid>
<Grid item className="bcd">
<TextField
fullWidth
label={"To"}
placeholder={"Ex.100"}
value={this.state.serialInputObjects[key]["to"]}
onChange={(e) =>
this.handleChangeToSerials(key, e.target.value)
}
error={Boolean(toErrorMessage) || false}
helperText={toErrorMessage}
margin="none"
size="small"
/>
</Grid>
</Grid>
</div>
))}
</Paper>
);
}
}
export default function App() {
return (
<div className="App">
<SerialQRScanClass />
</div>
);
}
I want to be able to print the error only in that corresponding field in the loop.
We can do this by tracking all the errors for every individual input. First update the state with a fromErrorMessage and a toErrorMessage object. These will hold the errors for the inputs.
state = {
serialRegistrationTracker: [],
serialInputObjects: {
//0: { from: "", to: "", except: "" } }
},
fromErrorMessage: {},
toErrorMessage: {},
};
Then we can update the calculation function to store the errors for the specific input. I added 2 new arguments fromValue and toValue these will help with checking the up-to-date value and prevent the error messages to be one state behind.
calculation = (key, fromValue, toValue) => {
let errors = this.getFromToSerialErrorMessages(fromValue, toValue);
this.setState((prevState) => ({
...prevState,
fromErrorMessage: {
...prevState.fromErrorMessage,
[key]: errors.fromErrorMessage,
},
toErrorMessage: {
...prevState.toErrorMessage,
[key]: errors.toErrorMessage,
},
}));
console.log(
`Key ${key} From error message - ` + this.state.fromErrorMessage[key]
);
console.log("To error message - " + this.state.toErrorMessage[key]);
};
Now we need to update the handlers to work with the new calculation function. We pass the current state from the to and the new data to check against and vice versa.
handleChangeFromSerials = (key: any, data: string) => {
...
this.calculation(key, data, this.state.serialInputObjects[key]["to"]);
};
handleChangeToSerials = (key: any, data: string) => {
...
this.calculation(key, this.state.serialInputObjects[key]["from"], data);
};
Finally update the TextField components. And the same for the to input.
<TextField
...
error={Boolean(this.state.fromErrorMessage[key]) || false}
helperText={this.state.fromErrorMessage[key]}
...
/>

React input elements rendering in wrong order on state change

I have a list of inputs with an empty input at the end.
If a user types in this empty input, the state is changed to reflect this, and a new blank input is added to the end.
If the user deletes the value in the input at the end (not the blank one), this input is removed and we still have a blank input at the end.
The issue: There are at least 4 inputs (first 3 with values, last blank as expected). The user makes input 2 blank.
Expected result: Inputs are re-rendered in the new order (Input1, Input3, BlankInput)
Actual result: Inputs rendered in this order (Input 1, BlankInput, Input3)
Code:
type Props = {}
const InitialState = {
data: [],
}
type State = typeof InitialState
export default class List extends React.Component<Props, State> {
public state = InitialState
componentDidMount() {
var inputs = [];
//Call API to get data
getList().then((dataArray) => {
inputs = JSON.parse(dataArray)
this.setState({ data: inputs })
});
}
generateInput(input: any) {
var listArray = input.config.order.split(',');
listArray.push('');
return listArray;
}
delaySaveOfList(e: React.ChangeEvent<any>, input: any, name: string) {
var listArray = input.config.order.split(',')
var arrTarget = Number(e.target.name);
var inputLength = e.target.parentElement.querySelectorAll("#ListInput").length - 1
if (e.target.value == "" && inputLength > 1) {
var newInputArray = [] as any;
this.state.data.forEach((inputState: any) => {
if (inputState === input) {
listArray.splice(arrTarget, 1);
input.config.order = listArray.toString();
newInputArray.push(input)
}
else {
newInputArray.push(inputState)
}
})
this.setState({ data: newInputArray })
//Save this new list to API
updateList(input);
}
}
onChange(e: React.ChangeEvent<any>, input: any, name: string) {
e.persist();
if (e.target.refreshTimer) {
clearTimeout(e.target.refreshTimer);
}
e.target.refreshTimer = setTimeout(() => {
this.delaySaveOfList(e, input, name)
}, 1000);
};
render() {
return (
<div>
<h1>Inputs</h1>
{this.state.data.map((input: any) => (
<div className="card">
<div className="card-body">
<p className="card-text">Inputs:</p>
{this.generateInput(input).map((name: any, index: number) => (
<>
<input id="ListInput" type="text" defaultValue={name} name={index.toString()} key={index} onChange={(e) => { this.onChange(e, input, name) }} />
<br />
</>
))}
</div>
</div>
))}
</div>
)
}
}
I will say, once I refresh the page, the order of the inputs is correct, as it pulls data from the API again, so I assume I'm doing something wrong with the state data.

Set initialVariables in Formik from state if it is in edit mode

I'm using Formik for validating some data. It works fine when it should create new entity, but there are problems when I want to edit an entity.
The edit mode must be activated from the state (this.state.edit === true), also the data of the entity is stored on the state, for example this.state.name has a string value there.
I put a console log in render, the problem is that the log is printed several times, the first time with empty string on this.sate.name and the value of this.state.edit is false. The next prints it is correct, this edit on true and name containing a value.
Here is the code:
import React from 'react';
import { Redirect } from 'react-router-dom';
import { Formik, Form, Field } from 'formik';
import { Input, Button, Label, Grid } from 'semantic-ui-react';
import { connect } from 'react-redux';
import * as Yup from 'yup';
import { Creators } from '../../../actions';
class CreateCompanyForm extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
name: '',
redirectCreate: false,
redirectEdit: false,
edit: false,
};
}
componentDidMount() {
const {
getCompany,
getCompanies,
location: { pathname },
} = this.props;
getCompanies({
name: '',
});
if (pathname.substring(11) !== 'create') {
getCompany(pathname.substring(16));
this.setState({
edit: true,
});
this.setState({
name: this.props.company.name,
});
}
}
handleSubmitCreate = e => {
e.preventDefault();
const { createCompany, getCompanies } = this.props;
createCompany(this.state);
this.setState({ redirectCreate: true });
getCompanies(this.props.query);
};
handleSubmit = values => {
const { createCompany, getCompanies } = this.props;
createCompany(values);
this.setState({ redirectCreate: true });
getCompanies(this.props.query);
};
handleSubmitEdit = e => {
e.preventDefault();
const { name } = this.state;
const { updateCompany } = this.props;
updateCompany(this.props.company._id, {
name,
});
this.setState({ redirectEdit: true });
};
render() {
let title = 'Create company';
let buttonName = 'Create';
let submit = this.handleSubmitCreate;
const { redirectCreate, redirectEdit } = this.state;
if (redirectCreate) {
return <Redirect to="/companies" />;
}
if (redirectEdit) {
return <Redirect to={`/companies/${this.props.company._id}`} />;
}
if (this.state.edit) {
title = 'Edit company';
buttonName = 'Edit';
submit = this.handleSubmitEdit;
}
console.log('state: ', this.state); // first time it is empty, next times it has data
let initialValues = {};
if (this.state.edit) {
initialValues = {
name: this.state.name,
};
} else {
initialValues = {
name: '',
};
}
const validationSchema = Yup.object({
name: Yup.string().required('This field is required'),
});
return (
<>
<Button type="submit" form="amazing">
create company
</Button>
<Formik
htmlFor="amazing"
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={values => this.handleSubmit(values)}>
{({ values, errors, touched, setValues, setFieldValue }) => (
<Form id="amazing">
<Grid>
<Grid.Column>
<Label>Company name</Label>
<Field name="name" as={Input} placeholder="write a name" />
<div>{touched.name && errors.name ? errors.name : null}</div>
</Grid.Column>
</Grid>
<Button type="submit" floated="right" form="amazing">
{buttonName} company
</Button>
</Form>
)}
</Formik>
</>
);
}
}
const mapStateToProps = state => ({
companies: state.companies.companies,
company: state.companies.selectedCompany,
query: state.companies.query,
});
const mapDispatchToProps = {
getCompanies: Creators.getCompaniesRequest,
createCompany: Creators.createCompanyRequest,
getCompany: Creators.getCompanyRequest,
updateCompany: Creators.updateCompanyRequest,
};
export default connect(mapStateToProps, mapDispatchToProps)(CreateCompanyForm);
I put the whole file here to have more context. Is it a way to set the initialValue of name with the value from this.state.name and put it inside the input field?
By default Formik does not re-render if the initial values change. You can pass enableReinitialize prop to Formik component to allow it.
As you said in the comment, first time it renders, it has no data, hence it does initialise Formik with empty values. With that prop, it should re-render if the initial values change.
https://formik.org/docs/api/formik#enablereinitialize-boolean

How to dynamically fill a state based on textfield input?

I have a form that is rendered using a JavaScript object called model and a text-field that takes in an input. Based on this the state needs to be updated.
Unfortunately the state is updated to the last entry input. And not all the values in the form that have been filled in.
I haven't managed to get the methods mentioned here to work for me: How to fill a specific state with a dynamic key?
constructor(props) {
super(props);
this.state = {
Form:{},
}
}
onChange = (e, key, type) => {
let list = [];
if (type === "text") {
list[key] = e.target.value;
}
else {
// LOGIC TO HANDLE NON TEXT VALUES
let found = this.state.Form[key]? true : false;
if (found) {
list[key] = false;
} else {
list[key]= true;
}
}
this.setState({ Form: list });
};
renderform = () => {
return model.map(m => {
<TextField
type={m.type}
key={m.key}
onChange={event => { this.onChange(event, m.key, "text"); }}
/>
})
}
Try this:
this.setState(prevState => ({ Form: {...prevState.Form, ...list} }));
because when you use this.setState({ Form: list }); your Form state is overwritten with a new array everytime.

Prevent child's state from reset after parent component state changes also get the values of all child components:ReactJS+ Typescript

I am a bit new to react and I am stuck in this situation where I am implementing custom dropdown filter for a table in react. I have set of dropdown values for each column and there is a Apply button.
I have maintained a child component for this which takes in drop down values and sends the selected one's back to parent. Then I call a back-end API that gives me filtered data which in-turn sets parents state . The problem here is the checkbox values inside dropdown is lost after I get the data and set the parent state.
Each child components has as a set of checkboxes , an Apply and a clear button. So on click of Apply , I have to send the checked one's to the parent or in general whichever the checked one's without losing the previous content.
I am unable to understand why am I losing the checkbox values?
It would be of great help if someone can help me out with this
Sand box: https://codesandbox.io/s/nervous-elgamal-0zztb
I have added the sandbox link with proper comments. Please have a look. I am a bit new to react.
Help would be really appreciated
Parent
import * as React from "react";
import { render } from "react-dom";
import ReactTable from "react-table";
import "./styles.css";
import "react-table/react-table.css";
import Child from "./Child";
interface IState {
data: {}[];
columns: {}[];
selectedValues: {};
optionsForColumns: {};
}
interface IProps {}
export default class App extends React.Component<IProps, IState> {
// Here I have hardcoded the values, but data and optionsForColumns comes from the backend and it is set inside componentDidMount
constructor(props: any) {
super(props);
this.state = {
data: [
{ firstName: "Jack", status: "Submitted", age: "14" },
{ firstName: "Simon", status: "Pending", age: "15" }
],
selectedValues: {},
columns: [],
optionsForColumns: {
firstName: [{ Jack: "4" }, { Simon: "5" }],
status: [{ Submitted: "5" }, { Pending: "7" }]
}
};
}
// Get the values for checkboxes that will be sent to child
getValuesFromKey = (key: any) => {
let data: any = this.state.optionsForColumns[key];
let result = data.map((value: any) => {
let keys = Object.keys(value);
return {
field: keys[0],
checked: false
};
});
return result;
};
// Get the consolidated values from child and then pass it for server side filtering
handleFilter = (fieldName: any, selectedValue: any, modifiedObj: any) =>
{
this.setState(
{
selectedValues: {
...this.state.selectedValues,
[fieldName]: selectedValue
}
},
() => this.handleColumnFilter(this.state.selectedValues)
);
};
// Function that will make server call based on the checked values from child
handleColumnFilter = (values: any) => {
// server side code for filtering
// After this checkbox content is lost
};
// Function where I configure the columns array for the table . (Also data and column fiter values will be set here, in this case I have hardcoded inside constructor)
componentDidMount() {
let columns = [
{
Header: () => (
<div>
<div>
<Child
key="firstName"
name="firstName"
options={this.getValuesFromKey("firstName")}
handleFilter={this.handleFilter}
/>
</div>
<span>First Name</span>
</div>
),
accessor: "firstName"
},
{
Header: () => (
<div>
<div>
<Child
key="status"
name="status"
options={this.getValuesFromKey("status")}
handleFilter={this.handleFilter}
/>
</div>
<span>Status</span>
</div>
),
accessor: "status",
},
{
Header: "Age",
accessor: "age"
}
];
this.setState({ columns });
}
//Rendering the data table
render() {
const { data, columns } = this.state;
return (
<div>
<ReactTable
data={data}
columns={columns}
/>
</div>
);
}
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);
Child
import * as React from "react";
import { Button, Checkbox, Icon } from "semantic-ui-react";
interface IProps {
options: any;
name: string;
handleFilter(val1: any, val2: any, val3: void): void;
}
interface IState {
showList: boolean;
selected: [];
checkboxOptions: any;
}
export default class Child extends React.Component<IProps, IState> {
constructor(props: any) {
super(props);
this.state = {
selected: [],
showList: false,
checkboxOptions: this.props.options.map((option: any) => option.checked)
};
}
// Checkbox change handler
handleValueChange = (event: React.FormEvent<HTMLInputElement>, data: any) => {
const i = this.props.options.findIndex(
(item: any) => item.field === data.name
);
const optionsArr = this.state.checkboxOptions.map(
(prevState: any, si: any) => (si === i ? !prevState : prevState)
);
this.setState({ checkboxOptions: optionsArr });
};
//Passing the checked values back to parent
passSelectionToParent = (event: any) => {
event.preventDefault();
const result = this.props.options.map((item: any, i: any) =>
Object.assign({}, item, {
checked: this.state.checkboxOptions[i]
})
);
const selected = result
.filter((res: any) => res.checked)
.map((ele: any) => ele.field);
console.log(selected);
this.props.handleFilter(this.props.name, selected, result);
};
//Show/Hide filter
toggleList = () => {
this.setState(prevState => ({ showList: !prevState.showList }));
};
//Rendering the checkboxes based on the local state, but still it gets lost after filtering happens
render() {
let { showList } = this.state;
let visibleFlag: string;
if (showList === true) visibleFlag = "visible";
else visibleFlag = "";
return (
<div>
<div style={{ position: "absolute" }}>
<div
className={"ui scrolling dropdown column-settings " + visibleFlag}
>
<Icon className="filter" onClick={this.toggleList} />
<div className={"menu transition " + visibleFlag}>
<div className="menu-item-holder">
{this.props.options.map((item: any, i: number) => (
<div className="menu-item" key={i}>
<Checkbox
name={item.field}
onChange={this.handleValueChange}
label={item.field}
checked={this.state.checkboxOptions[i]}
/>
</div>
))}
</div>
<div className="menu-btn-holder">
<Button size="small" onClick={this.passSelectionToParent}>
Apply
</Button>
</div>
</div>
</div>
</div>
</div>
);
}
}
This appears to be a case of state being managed in an inconvenient way. Currently, the state is managed at the Child level, but it would be easier to manage at the Parent level. This is known as lifting state up in React.
The gist - the shared state is managed in the Parent component, and it's updated by calling a function passed to the Child component. When Apply is clicked, the selected radio value is passed up to the Parent, which merges the new selection into the shared state.
I have created a minimal example of your code, showing how we can lift state up from the Child to the Parent component. I'm also using a few new-ish features of React, like useState to simplify the Child component.
// Child Component
const Child = ({name, options, updateSelections}) => {
const [selected, setSelected] = React.useState([]);
const handleChange = (event) => {
let updated;
if (event.target.checked) {
updated = [...selected, event.target.value];
} else {
updated = selected.filter(v => v !== event.target.value);
}
setSelected(updated);
}
const passSelectionToParent = (event) => {
event.preventDefault();
updateSelections(name, selected);
}
return (
<form>
{options.map(item => (
<label for={name}>
<input
key={name}
type="checkbox"
name={item}
value={item}
onChange={handleChange}
/>
{item}
</label>
))}
<button onClick={passSelectionToParent}>Apply</button>
</form>
)
}
// Parent Component
class Parent extends React.Component {
constructor(props) {
super(props);
this.fields = ["firstName", "status"],
this.state = {
selected: {}
};
}
getValuesFromKey = (data, key) => {
return data.map(item => item[key]);
}
updateSelections = (name, selection) => {
this.setState({
selected: {...this.state.selected, [name]: selection}
}, () => console.log(this.state.selected));
}
render() {
return (
<div>
{this.fields.map(field => (
<Child
key={field}
name={field}
options={this.getValuesFromKey(this.props.data, field)}
updateSelections={this.updateSelections}
/>
))}
</div>
)
}
}
const data = [
{ firstName: "Jack", status: "Submitted" },
{ firstName: "Simon", status: "Pending" },
{ firstName: "Pete", status: "Approved" },
{ firstName: "Lucas", status: "Rejected" }
];
ReactDOM.render(<Parent data={data}/>, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Your checkbox values are only lost when you hide/show the table, as the table goes out of
DOM the state of it and it's children are lost. When the table is mounted to DOM, Child
component is mounted again initializing a new state taking checkbox values from
getValuesFromKey method of which returns false by default clearing checkbox ticks.
return {
field: keys[0],
checked: false
};
Stackblitz reproducing the issue.
You have to set checkbox values checking the selectedValues object to see if it was selected.
return {
field: keys[0],
checked: this.state.selectedValues[key] && this.state.selectedValues[key].includes(keys[0]),
};

Categories

Resources