Reputation: 2210
I'm trying to create a Form component in react, kind of like Formik, but way simpler.
The way I'm thinking involves adding a onChange
handler to all the children. I am doing this with children.map()
. It works, however I get a key warning
Warning: Each child in a list should have a unique "key" prop.
I know there's no way to suppress this, so maybe there's a better approach to create this Form component?
Also, how should I approach the case when the <input>
is not a direct child?
Edit: I know how to avoid the problem, I mainly want the best way to approach this, including cases of nested inputs.
Here is how I want to use it:
<Form>
<label htmlFor="owner">Owner</label>
<input
type="text"
name="owner"
id="owner"
/>
<label htmlFor="description">Description</label>
<input
type="text"
name="description"
id="description"
/>
<input
type="submit"
value="Submit"
/>
</Form>
and here is my code:
import React from 'react';
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {}
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value =
target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
console.log(`${name} : ${value}`)
this.setState({
[name]: value
});
}
render() {
return (
<form>
{this.props.children.map((child) => {
if (child.type === "input") {
return (
<input
onChange={this.handleInputChange}
{...child.props}
/>
)
}
})}
</form>
)
}
}
export default Form;
Upvotes: 6
Views: 5768
Reputation: 6529
If you use a render prop, you won't run into the unique "key"
prop issue at all (This is also how Formik is implemented).
Your component would be easy to set up to pass handleChange
to its children as a render prop, and this would also not require you to have input
as a direct child.
class Form extends Component {
...
handleInputChange() {...}
render() {
// Note that we call children as a function,
// passing `handleChangeInput` as the argument.
// If you want to pass other other things to the
// children (handleSubmit, values from state), just
// add them to the argument you're passing in.
this.props.children({this.handleInputChange});
}
}
Here's how you use it:
<Form>
// Notice that <Form> needs its immediate child to be
// a function, which has your handler as the argument:
{({handeInputChange}) => {
return (
<form>
<input type="text" name="owner" onChange={handleInputChange} />
<input type="checkbox" name="toggle" onChange={handleInputChange} />
<div>
// inputs can be nested in other elements
<input name=“inner” onChange={handleInputChange} />
<div>
<form>
)
}}
</Form>
EDIT: You mentioned in a comment that you didn't want to explicitly pass the handler to each of your inputs. Another way to achieve this is with React Context, with a Provider in your Form, and each input
wrapped in a consumer:
const FormContext = React.createContext();
const FormInput = (props) => {
const {handleInputChange} = useContext(FormContext);
return <input handleInputChange={handleInputChange} {...props} />
}
class Form extends Component {
...
handleInputChange() {...}
render() {
// Pass anything you want into `value` (state, other handlers),
// it will be accessible in the consumer
<Provider value={{ handleInputChange: this.handleInputChange }}>
<form>
{this.props.children}
</form>
</Provider>
}
}
// Usage:
<Form>
<FormInput type="text" name="owner" />
<FormInput type="submit" name="submit" />
<div>
<FormInput type="checkbox" name="toggle" />
</div>
</Form>
In fact, Formik has this option as well, with either the Field
component, or the connect
function.
Upvotes: 4
Reputation: 1342
i think this is what you need, already you could add child index as key since there order won't change, and reduce here is not returning null in the array in case the type of the child is not input, map+filter could resolve it also:
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.handleInputChange = this.handleInputChange.bind(this);
// this.handleSubmit = this.handleSubmit.bind(this);
}
handleInputChange(event) {
const target = event.target;
const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name;
console.log(`${name} : ${value}`);
this.setState({
[name]: value
});
}
render() {
return (
<form>
{this.props.children.reduce((childrenAcc, child, index) => {
if (child.type === "input") {
return [
...childrenAcc,
<input
key={index}
onChange={this.handleInputChange}
{...child.props}
/>
];
}
return childrenAcc;
}, [])}
</form>
);
}
}
function App() {
return (
<Form>
<label htmlFor="owner">Owner</label>
<input type="text" name="owner" />
<label htmlFor="description">Description</label>
<input type="text" name="description" />
<input type="submit" value="Submit" />
</Form>
);
}
check this sandbox .
Upvotes: 1