React input elements rendering in wrong order on state change - javascript

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.

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]}
...
/>

How to hide a specific element inside .map function in React?

I am looking for a way to hide a div once the button thats in it is clicked and continue to show all other div's.
I've tried using the setState method, however when setting it to false with onClick() all of my objects disappear.
class App extends React.PureComponent {
state: {
notHidden: false,
}
constructor(props: any) {
super(props);
this.state = {search: '', notHidden: true};
this.hideObject = this.hideObject.bind(this)
}
hideThisDiv() {
this.setState({notHidden: false})
}
async componentDidMount() {
this.setState({
objects: await api.getObjects()
});
}
render = (objects: Object[]) => {
return ({Object.map((object) =>
<div key={index} className='class'>
<button className='hide' type='button' onClick={() => hideThisDiv()}>Hide</button>
<p>object.text</p>
</div>}
render() {
const {objects} = this.state;
return (<main>
<h1>Objects List</h1>
<header>
<input type="search" onChange={(e) => this.onSearch(e.target.value)}/>
</header>
{objects ? this.render(objects) : null}
</main>)
}
);
The data is a data.json file filled with many of these objects in the array
{
"uuid": "dsfgkj24-sfg34-1r134ef"
"text": "Some Text"
}
Edit: Sorry for the badly asked question, I am new to react.
Not tested, just a blueprint... is it what you want to do?
And yes I didn't hide, I removed but again, just an idea on how you can hide button separately, by keeping in state which ones are concerned.
function MagicList() {
const [hidden, hiddenSet] = useState([]);
const items = [{ id:1, text:'hello'}, { id:2, text:'from'}, { id:3, text:'the other sided'}]
const hideMe = id => hiddenSet([...hidden, id]);
return {
items.filter( item => {
return hidden.indexOf(item.id) !== -1;
})
.map( item => (
<button key={item.id} onClick={hideMe.bind(this, item.id)}>{item.text}</button>
))
};
}
Edition
const hideMe = id => hiddenSet([...hidden, id]);
It is just a fancy way to write:
function hideMe(id) {
const newArray = hidden.concat(id);
hiddenSet(newArray);
}
I suggest using a Set, Map, or object, to track the element ids you want hidden upon click of button. This provides O(1) lookups for what needs to be hidden. Be sure to render your actual text and not a string literal, i.e. <p>{object.text}</p> versus <p>object.text</p>.
class MyComponent extends React.PureComponent {
state = {
hidden: {}, // <-- store ids to hide
objects: [],
search: "",
};
// Curried function to take id and return click handler function
hideThisDiv = id => () => {
this.setState(prevState => ({
hidden: {
...prevState.hidden, // <-- copy existing hidden state
[id]: id // <-- add new id
}
}));
}
...
render() {
const { hidden, objects } = this.state;
return (
<main>
...
{objects
.filter((el) => !hidden[el.uuid]) // <-- check id if not hidden
.map(({ uuid, text }) => (
<div key={uuid}>
<button
type="button"
onClick={this.hideThisDiv(uuid)} // <-- attach handler
>
Hide
</button>
<p>{text}</p>
</div>
))}
</main>
);
}
}

Why does form.change(name, value) not update the form value

I am using FormSpy in the child file and have access to this.props.form in the current file.
The form values that I want to have are currently in the local state in the child file and I have been trying to update form values using form.change() but they are not updated.
Shortened code to highlight the issue of trying to update form values based on state.
class ABC extends Component {
this.state = {
segmentMappings: {
id: '',
name: '',
statusId: ''
}
};
handleStatusChange = (e) => {
var segmentMappings = [...this.state.segmentMappings];
var newStatusId = 8;
// Logic to update this.state.segmentMappings
var name = `segments${e}.statusId`;
console.log('Field name', name);
//Form values are not updating form value as I was expecting
this.props.form.change(name, newStatusId);
this.setState({ segmentMappings }
}
}
const SegmentTile = {
return null;
}
//I want to render the names prop and save id & statusId values to the form.
segmentsData(segmentsByApp) {
let result = [];
let i = 0;
segmentsByApp.map((seg) => {
result.push(
<div
key={seg.id} onClick={() => this.handleStatusChange(seg.id)}>
<Field
name={`segments[${seg.id}].id`} value={seg.id}
component={SegmentTile}></Field>
<Field
name={`segments${seg.id}.statusId`} value={seg.statusId}
component={SegmentTile}></Field>
<Segment>
<span>{seg.name}</span>
</Segment>
</div >
);
result.push(<SegmentDivider key={seg.id + 'divider'} />);
i++;
}
}
);
return result;
};
render() {
{this.segmentsData(this.state.segmentMappings)}
}
}

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]),
};

Storing JSX elements inside React State?

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?

Categories

Resources