More generic way to set an objects value within React handleChange - javascript

I have a component in React, I need to take a value from the radio button being checked and then set that value within an object in my Formik values. If a different radio button is selected within the group I need to set the previously selected one to false, is there a standard way of doing this? I'm using an object within my Formik values as the field holds a date as well as the attribute from the radio button, you can see where I place the date into the object using handleTime, so I can't just null the field and place the new item in.
I'm currently doing the following in my component to update the Formik time_frame value.
import React, {Component} from 'react';
import {Col, Row} from 'react-bootstrap';
import Radio from "./radio";
export default class EventTimeFrame extends Component {
state = {
eventTimeFrame: [
{id: 1, value: "one_off", label: "ONE OFF", checked: false},
{id: 2, value: "recurring", label: "RECURRING", checked: false},
]
}
handleOccurance = value => {
let timeCopy = {...this.props.values.time_frame}
if (value.target.checked) {
if (value.target.value === "one_off") {
timeCopy[value.target.value] = true
timeCopy["recurring"] = false
} else {
timeCopy[value.target.value] = true
timeCopy["one_off"] = false
}
}
this.props.onChange("time_frame", timeCopy)
this.setState(prevState => ({
eventTimeFrame: prevState.eventTimeFrame.map(
el => el.value === value.target.value ? {...el, checked: true} : el
)
}))
};
handleTime = value => {
let timeCopy = {...this.props.values.time_frame}
timeCopy["start"] = new Date(value.target.value);
this.props.onChange("time_frame", timeCopy)
};
render() {
return (
<div>
<Row>
<Col>
<h4 className="ui centered question-header text-center">ONE OFF OR RECURRING EVENT?</h4>
</Col>
</Row>
<Row>
{
this.state.eventTimeFrame.map((timeFrame) => {
return (
<Radio name="time_frame" key={timeFrame.value}
onChange={this.handleOccurance} checked={timeFrame.checked} {...timeFrame} />
)
})
}
</Row>
<Row>
<Col>
<h4 className="question-header date-text">PLEASE ENTER THE FIRST DAY OF YOUR EVENT</h4>
</Col>
<Col>
<input type="date" className="form-control date" name="start"
onChange={this.handleTime}/>
</Col>
</Row>
</div>
)
}
}
I feel like there has to be a standard way of dealing with things like this

You're correct by suspecting there's a simpler way 😃 Some feedback:
Don't store the eventTimeFrame in state, a constant will do.
The standard way of using radio input values in Formik is to define one value in initialState for the entire group. Formik will set its value to the selected option.
Storing all options in values would have been fine if you were using checkboxes instead of radio buttons.
You wrote your own custom onChange handlers, but it would be simpler to just use Formik's provided onChange handlers. I only use my own handler if I have to, for example when formatting a value before setting it with setFieldValue()
See below:
Live Demo
// outside of component
const timeFrameTypes = [
{ id: "one_off", label: "ONE OFF" },
{ id: "recurring", label: "RECURRING" }
];
// in component's render function:
<Formik
initialValues={{
time_frame: {
type: "",
start: ""
}
}}
onSubmit={async (values) => {
// do something with values
}}
>
{({ handleSubmit }) => {
return (
<form onSubmit={handleSubmit}>
<div>
<Row>
<Col>
<h4 className="ui centered question-header text-center">
ONE OFF OR RECURRING EVENT?
</h4>
</Col>
</Row>
<Row>
{timeFrameTypes.map((timeFrameType) => {
return (
<Field
key={timeFrameType.id}
component={Radio}
name="time_frame.type"
id={timeFrameType.id}
label={timeFrameType.label}
/>
);
})}
</Row>
<Row>
<Col>
<h4 className="question-header date-text">
PLEASE ENTER THE FIRST DAY OF YOUR EVENT
</h4>
</Col>
<Col>
<Field name="time_frame.start">
{({ field }) => <input type="date" {...field} />}
</Field>
</Col>
</Row>
</div>
<br />
<button type="submit">Submit</button>
</form>
);
}}
</Formik>

Related

How to get the collapse to open depending if there is an error - ReactJs

Hi all I have following code.
I have two inputs and that two inputs are mandatory.
First input name is Name and when I am submit with any values it says Please input Name! . This part was working great.
Second input, which name is Short Info was located in collapse. And that collapse is <Form.Item>, it's mean that user can press + and add multiple Short Info inputs.
My problem is with that Short Info. When user press submit only first input shows error. For Short Info user should open that collapse to see error, which is not good.
Now how can I automatically open that collapse panel if there is error ?
Here is my code.
const [dataFromBackend, setDataFromBackend] = useState([]);
const addNewField = () => {
setDataFromBackend([...dataFromBackend]);
};
const submitForm = (values) => {
console.log('Received values of form: ', values);
};
const Header = ({ remove, index }) => {
return (
<Col align="center" span={1} justify="end">
<MinusCircleFilled
onClick={(e) => {
e.stopPropagation();
remove(index);
}}
/>
</Col>
);
};
return (
<>
<Form
name="validate_other"
onFinish={submitForm}
initialValues={{ values: [''] }}
>
<Form.Item
name="name"
label="Name"
rules={[
{
required: true,
message: 'Please input Name!',
},
]}
>
<Input />
</Form.Item>
<Form.List name="values">
{(fields, { add, remove }) => {
return (
<Row gutter={24}>
<Col span={24} md={24}>
<Card
title="Price"
extra={
<PlusCircleFilled
style={{
cursor: 'pointer',
fontSize: '20px',
color: '#00AEE6',
}}
onClick={() => {
add();
addNewField();
}}
/>
}
>
<div key={fields.key}>
{fields.map((field, i) => (
<div key={i}>
<Collapse accordion={true}>
<Panel
key={i}
header={<Header remove={remove} index={i} />}
>
<Col span={24} md={24}>
<Form.Item
name={[field.name, 'shortinfo']}
fieldKey={[field.fieldKey, 'shortinfo']}
label={'Short Info'}
rules={[
{
required: true,
message: 'Please input short info!',
},
]}
>
<Input />
</Form.Item>
</Col>
</Panel>
</Collapse>
</div>
))}
</div>
</Card>
</Col>
</Row>
);
}}
</Form.List>
<Button htmlType="submit">Save</Button>
</Form>
</>
)
Please help me to resolve this problem, thanks.
My solution would be as follows:
First of all, render all <Panels> inside the same <Colapse>, also remove accordion={true} from Colapse (so multiple Fields can be open at once) and add forceRender={true} to Panels, otherwise items inside a Panel are lazy loaded (if lazy load would be active, <Form.Item> wouldn't be rendered and don't show up as error)
<Collapse>
{fields.map((field, i) => {
return (
<Panel
...
forceRender={true}
...
>
...
</Panel>
);
})}
</Colapse>
Add a state to keep track of currently active keys and add it to the colapse, also remove the <div key={i}> it is not needed, since <Panel key={i}> is already a container, furthermore it will prevent onChange of Colapse to trigger and mess up your styling
const [activeKeysColapse, setactiveKeysColapse] = useState([]);
...
<Collapse
activeKey={activeKeysColapse}
onChange={(newActiveKeys) => {
// otherwise panels couldn't be open/closed by click
setactiveKeysColapse(newActiveKeys);
}}
>
{fields.map((field, i) => {
return (
<Panel
key={i}
forceRender={true}
>
...
</Panel>
);
})}
</Colapse>
Add onFinishFailed listener to form
const submitFormFailed = (errors) => {
// get all errorFields from the list and map the
// `activeKeys` to the index (because `<Panel key={i}>`)
const newShortInfoErrors = errors.errorFields
.filter((el) => el.name[0] === 'values')
.map((el) => el.name[1].toString());
// setting the state will close all fields which
// are valid and open all with an error message
setactiveKeysColapse(newShortInfoErrors);
};
...
<Form
...
onFinishFail={submitFormFailed}
>
Based on the code on your StackBlitz, this will do exactly what you want
I hope this will solve your problem, if there is anything unclear, feel free to comment

More than needed React components re-rendering when typing in input

I am taking input from a search input field using searchInput and setSearchInput useState hook and after I press submit button, I call fetchSearchData function providing it the input text and setCompanies hook, where companies are updated with the fetched list of companies from the API.
Then companies are passed to another component CompanyList where a map function is called there.
The problem is whenever I type in the search field, the CompanyList component is re-rendered although I did not press submit. I understand that setSearchInput will re-render SearchBar component whenever I type in it, but I don't get why CompanyList re-renders.
Search page source code:
const Search = () => {
const [companies, setCompanies]=useState([]); //List of companies returned from searching
const [searchInput, setSearchInput] = useState(""); //Search field input
//Update search text whenever the user types in
const onSearchChange = (e) => {
setSearchInput(e.target.value)
}
//use the API providing it the search input, and
//setCompanies hook to update list of companies
const onSearchSubmit = (e) => {
e.preventDefault()
fetchSearchData(searchInput, setCompanies)
}
return (
<div>
<Container>
<Row className={"searchFilterBar"}>
<Col sm={6} md={8} className={"searchBar"}>
<SearchBar onSubmit={onSearchSubmit} onChange={onSearchChange} value={searchInput} />
</Col>
<Col sm={6} md={4} className={"filterBar"}>
</Col>
</Row>
<CompanyList companies={companies} ></CompanyList>
<Row>
</Row>
</Container>
</div>
)
}
export default Search;
SearchBar component source code:
const SearchBar = ({value,onSubmit, onChange}) => {
return (
<Form
className="search-form"
onSubmit={onSubmit}
>
<div className="input-group">
<span className="input-group-text rubik-font">
<i className="icon ion-search"></i>
</span>
<input
className="form-control rubik-font"
type="text"
placeholder="Search for companies that start with..."
onChange={onChange}
value={value}
/>
<Button className="btn btn-light rubik-font" type="submit">Search </Button>
</div>
</Form>
)
}
CompanyList component source code:
function MapDataToCompanyList(response) {
console.log(response); //Logging occurs here
if(!response || response===undefined || response.length===0)
{
return (<ErrorBoundary message={noCompaniesError.message}></ErrorBoundary>)
}
return response.map((company) => {
return (
<Col key={company._id} xs={12} md={6} lg={4} className="mt-2">
<CompanyCard
id={company._id}
logo={company.logo}
title={company.name}
logoBackground={company.logoBackground}
progLangs={company.progLangs}
backend={company.backend}
frontend={company.frontend}
url={company.url}
>
</CompanyCard>
</Col>
)
})
}
const CompanyList = (props) => {
const {companies} = props
return (
<div>
<Container className="mt-3">
<Row>
{
MapDataToCompanyList(companies)
}
</Row>
</Container>
</div>
)
}
export default CompanyList;
FetchSearchData function source code:
export const fetchSearchData = (query, cb)=>{
const uri = process.env.NODE_ENV === 'development' ?
`http://localhost:3000/api/companies/name/${query}` :
``;
axios.get(uri, {
timeout: MAX_TIMEOUT
})
.then((response)=>{
cb(response.data.data)
})
.catch((error)=>{
console.log(error)
})
}
As seen above, empty list of companies is logged when the page first loads, then I typed three characters and the it logged three time which means the map function called three times.
Even then if I pressed submit and retrieved list of companies normally, whenever I type it will keep printing the array of companies that was fetched.
Sorry if I missed something, I am still new to React.
When you call setSearchInput(e.target.value), Search component will re-render cause its state has changed. Search component re-renders means every tag nested in it will re-render (except the ones passed via children). That is the normal behaviour of React. If you want to avoid that, you would wanna use React.memo for CompanyList. Or you could use useRef to bind the input like so:
const Search = () => {
const [companies, setCompanies] = useState([]); //List of companies returned from searching
const inputRef = React.useRef(null);
//use the API providing it the search input, and
//setCompanies hook to update list of companies
const onSearchSubmit = (e) => {
e.preventDefault();
fetchSearchData(inputRef.current.value, setCompanies);
inputRef.current.value = "";
};
return (
<div>
<Container>
<Row className={"searchFilterBar"}>
<Col sm={6} md={8} className={"searchBar"}>
<SearchBar inputRef={inputRef} onSubmit={onSearchSubmit} />
</Col>
<Col sm={6} md={4} className={"filterBar"}></Col>
</Row>
<CompanyList companies={companies}></CompanyList>
<Row></Row>
</Container>
</div>
);
};
export default Search;
const SearchBar = ({ onSubmit, inputRef }) => {
return (
<Form className="search-form" onSubmit={onSubmit}>
<div className="input-group">
<span className="input-group-text rubik-font">
<i className="icon ion-search"></i>
</span>
<input
ref={inputRef}
className="form-control rubik-font"
type="text"
placeholder="Search for companies that start with..."
/>
<Button className="btn btn-light rubik-font" type="submit">
Search
</Button>
</div>
</Form>
);
};
I don't get why CompanyList re-renders.
Because it's nested in your Search component, and it's not React.memo'd (or a PureComponent).
Yes, the component is updated, but that doesn't mean it necessarily causes a DOM reconciliation.
In any case, React is completely at liberty of calling your component function as many times as it likes (and indeed, in Strict Mode it tends to call them twice per update to make sure you're not doing silly things), so you should look at side effects (such as console logging) in your component function (which you shouldn't have in the first place) as performance guidelines.
You do not need to maintain a state for input field. You can use useRef and pass it to input like below.
<input
ref={inputRef}
className="form-control rubik-font"
type="text"
placeholder="Search for companies that start with..."
/>
And you can get get value inside onSearchSubmit using inputRef.current.value
This will not re-render you component on input change.

Why is my page reloading after onChange trigger update?

I have added different forms in different methods but when I type anything in input fields, the page reloads keeping the states and again I have to click on the field and type and same cycle happens. It is working fine if I add everything in return. Can somebody tell explain why is this happening and how to stop it?
I am also sharing a piece of code.
function MyForm() {
const [commentForm, setCommentForm] = useState({
Comment: "",
});
const onCommentChange = (obj) => {
setCommentForm((prevState) => {
return {
...prevState,
...obj,
};
});
};
const IForm = () => (
<Table>
<CardBody>
<Row>
<Col className="col-2">
<Label>Comment: </Label>
</Col>
<Col className="col-1">
<Input type="text"
value={commentForm.Comment}
onChange={(e) =>
onCommentChange({ Comment: e.target.value })} />
</Col>
</Row>
</CardBody>
</Table>
);
return (
<div>
<IForm />
</div>
)
}
export default MyForm
that's because you define IForm as A component inside the current component which is not correct. so you have two solutions.
1 - move IFORM Component outside the current react.
function MyForm() {
const [commentForm, setCommentForm] = React.useState({
Comment: ""
});
const onCommentChange = (obj) => {
setCommentForm((prevState) => {
return {
...prevState,
...obj
};
});
};
return (
<div>
<IForm commentForm={commentForm} onCommentChange={onCommentChange} />
</div>
);
}
export default MyForm;
const IForm = ({ commentForm, onCommentChange }) => (
<Table>
<CardBody>
<Row>
<Col className="col-2">
<Label>Comment: </Label>
</Col>
<Col className="col-1">
<Input type="text"
value={commentForm.Comment}
onChange={(e) =>
onCommentChange({ Comment: e.target.value })} />
</Col>
</Row>
</CardBody>
</Table>
);
2 - declare the IForm as a normal function inside the current component.
function MyForm() {
const [commentForm, setCommentForm] = React.useState({
Comment: ""
});
const onCommentChange = (obj) => {
setCommentForm((prevState) => {
return {
...prevState,
...obj
};
});
};
const form = () => (
<Table>
<CardBody>
<Row>
<Col className="col-2">
<Label>Comment: </Label>
</Col>
<Col className="col-1">
<Input type="text"
value={commentForm.Comment}
onChange={(e) =>
onCommentChange({ Comment: e.target.value })} />
</Col>
</Row>
</CardBody>
</Table>
);
return <div> {form()} </div>;
}
export default MyForm;
The reason is that the IForm component is declared inside the MyForm Component. Which means that whenever the state of MyForm Component changes it will refresh its dom tree. And when the dom will rerender the functional component IForm will be executed again that's why you'll always lose the focus of the input but you never lose the state of the MyForm component.
To stop it from being happening either declare the IForm Component outside of the MyForm component or move the jsx of the IForm inside the Return of MyFOrm component.
You should just setCommentForm the value. I don't think you need to spread the prevState.
What you want to achieve is to set the state value to the new one.
Also, you don't have any useEffect right?

How to display and handle dynamic checkoxes that are dependent on Task array value in the backend (Mongodb) in react js?

I working on a react project where I have requirement like,
I have array inside contain, 1 Object and 1 Array named Task[]
"contractor": [
{
"contractGivenBy": -1,
"contractorID": 0,
"contractorName": "contractor1",
"reviewedByAssigner": false,
"submitReviewToAssigner": false,
"tasks": [ 2, 4, 6 ],
"tasksDone": false
},
Now, I want to display the Tasks array as Checkboxes in the page.
That is nice, I displayed all checkboxes using map() method, But the problem is, How to handle (get values from those checkboxes) when user checked or unchecked the specific checkbox.
I'm using React functional component with React hooks.
Here is what is tried..
<form onSubmit={onSubmitHandler}>
{
projectData.contractor[0].tasks.map((task, index) => {
return (
<div style={{ flexDirection: "column" }}>
<FormControlLabel
control={
<Checkbox
checked={false}
value={task}
onChange={handleTask} />
}
label={`task ${task}`}
/>
</div>
)
})
}
<Button
type="submit"
style={{
backgroundColor:"rgba(25,123,189)",
color: "white"
}}>
Assgin
</Button>
</form>
UPDATED
Here you go , it uses react hooks with checkbox implementation, i have tweaked it a little with <input type /> but you will get the idea
import React, { useState } from "react";
import ReactDOM from "react-dom";
const Checkbox = ({ type = "checkbox", name, checked = false, onChange }) => {
console.log("Checkbox: ", name, checked);
return (
<input type={type} name={name} checked={checked} onChange={onChange} />
);
};
const CheckboxExample = () => {
const [checkedItems, setCheckedItems] = useState({});
const handleChange = event => {
setCheckedItems({
...checkedItems,
[event.target.name]: event.target.checked
});
console.log("checkedItems: ", checkedItems);
};
const checkboxes = [
{
name: "check-box-1",
key: "checkBox1",
label: "Check Box 1"
},
{
name: "check-box-2",
key: "checkBox2",
label: "Check Box 2"
}
];
return (
<div>
<lable>Checked item name : {checkedItems["check-box-1"]} </lable> <br />
{checkboxes.map(item => (
<label key={item.key}>
{item.name}
<Checkbox
name={item.name}
checked={checkedItems[item.name]}
onChange={handleChange}
/>
</label>
))}
</div>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<CheckboxExample />, rootElement);

How to prevent setState changing input field?

I am using ant design form and trying to prevent clearing input after I check a checkbox.
Here is the code :
this.state = {
externalOptionsArray: [],
}
// onClick function
optionOnChange = (e, index, array) => {
const { externalOptionsArray } = this.state
let externalOptionCurrentState = externalOptionsArray
externalOptionCurrentState[index].is_enabled = !externalOptionCurrentState[index].is_enabled;
this.setState({
externalOptionsArray: externalOptionCurrentState
})
}
// Components
<Form onSubmit={this.onSubmit}>
<FormContainerWithDescription
direction="vertical"
title="Product"
description="Product Information">
<FormItemRow>
<Col span={24} style={colStyle}>
<FormItem label={'Product name'} colon={false} style={{ marginBottom: 0 }}>
{getFieldDecorator('title', {
rules: [
{ required: true, message: 'name is required' },
],
})(<Input />)}
</FormItem>
</Col>
</FormItemRow>
<FormItemRow>
<Col span={24} style={colStyle}>
<FormItem label={'external_options'} colon={ false } style={{ marginBottom: 0 }}>
{ externalOptionsArray.map((option, index, array) => {
return (
<Col style={{ float: 'left', width: '50%' }} key={option.name}>
<Row>
<Checkbox defaultChecked={option.is_enabled} onChange={() => this.optionOnChange(index, array)}>{option.name}</Checkbox>
</Row>
</Col>
)
})}
</FormItem>
</Col>
</FormItemRow>
</FormContainerWithDescription>
</Form>
And here is the image for better understanding
The problem is when I input a text in a input field and click one of checkboxes, input field automatically cleared up, it goes blank input field again.
I believe this is happening due to setState inside of optionOnChange function. Whenever I click one of checkboxes, setState takes place and it rerenders the DOM.
So I used e.preventDefault() and e.stopPropagaion inside of Checkbox component like this below.
<Checkbox defaultChecked={option.is_enabled} onChange={() => this.optionOnChange(index, array)} onClick={e => e.preventDefault()}>{option.name}</Checkbox>
// and
optionOnChange = (e, index, array) => {
e.preventDefault()
e.stopPropagation()
const { externalOptionsArray } = this.state
let externalOptionCurrentState = externalOptionsArray
externalOptionCurrentState[index].is_enabled = !externalOptionCurrentState[index].is_enabled;
this.setState({
externalOptionsArray: externalOptionCurrentState
})
}
But neither of them work
How can I make Checkbox works without rerender the input field?
You need to make it a controlled component. If you set the value of the Input component using a state variable then it remains even after state change.
So looking at your code above, you may need to have another variable like productName in the state and a onChange event handler to set the state when the input is changed
this.state = {
externalOptionsArray: [],
productName: '',
}
onChange = (e) => {
this.setState({productName: e.target.value});
}
And pass this to the Input like
<Input value={this.state.productName} onChange={this.onChange} />
You can store product's name in state and change it when input changes.
this.state = {
externalOptionsArray: [],
productName: '',
}
then define onChange function where you handle updating the state, like you did in optionOnChange()
and pass it to your Input component
<Input onChange={this.onChange} />

Categories

Resources