I'm using React v16 (latest) and I'm trying to create a general form component, that uses props.children.
export class MyForm extends React.Component {
render() {
return (
<div>
<div className="form">
<h3>{this.props.formName}</h3>
<div>
{React.Children.map(this.props.children, t => {return <span>{t}<br></br></span>;})}
</div>
<input type="button" value={this.props.formName} onClick={this.handleClick}/>
</div>
</div>
)
}
}
I want to create small form that just are able to create a meaningful json object and send it in POST
This is an example of such usage:
<MyForm>
<input type="text" name="a1"></input>
<input type="text" name="a2"></input>
</MyForm>
And I want to have many such small forms. Problem is I want each child (props.children) to have an onChange event -
onChange(event) // name is "a1" or "a2", like in the example aboce
{
var obj = {};
obj[name]=event.target.value;
this.setState(obj);
}
-so that I don't need to manually add onChange for each such child
-I guess another solution is to create a component, but I want the form to be flexible for each kind of sub-element (input text, text area, radio buttons,...) and I just want them all to have similar onChange that will set the name of the component and its value to the state...
I tried adding an onChange property in consturctor and in different hooks, but got:
cannot define property 'onChange', object is not extensible
So when are where (if at all) can I add an onChange dynamically to props.children
This is a great use case for a Higher Order Component. You can use a HOC to wrap and add the onChange prop to any component:
const WithOnChange = WrappedComponent => {
return class extends Component {
onChange = e => {
const obj = {};
obj[name]=e.target.value;
this.setState(obj);
}
render() {
return <WrappedComponent {...this.props} onChange={this.onChange} />
}
}
}
...
import Input from './Input';
class MyForm extends Component {
render() {
return (
<form>
...
<Input type="text" name="a1" />
...
</form>
)
}
}
export default MyForm;
....
import WithOnChange from './WithOnChange';
const Input = (props) => (
<input {...props} />
);
export default WithOnChange(Input);
EDIT:
Another option is to move your children map into a higher order component and then create a custom <Form /> component:
const Form = () => {
return <form>{this.props.children}</form>
};
export default WithOnChange(Form);
const WithOnChange = WrappedComponent => {
return class extends Component {
onChange = e => {
const obj = {};
obj[name] = e.target.value;
this.setState(obj);
}
render () {
const children = React.Children.map(this.props.children, child => {
return React.cloneElement(child, { onChange: this.onChange });
});
return <WrappedComponent {...this.props}>{children}</WrappedComponent>
}
}
}
#user967710, can you please test the following code:
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
onChange(event) {
debugger;
var obj = {};
obj.name = event.target.value;
this.setState(obj);
}
<MyForm formName="myForm">
<input type="text" name="a1" onChange={this.onChange}></input>
<input type="text" name="a2" onChange={this.onChange}></input>
</MyForm>
Related
I am trying to display input value on submit. But it seems to be not working. I don't have any errors but nothing being rendered. What is wrong with the code?
import React from 'react';
import { Component } from 'react';
class App extends Component {
constructor (props) {
super(props)
this.state = {
city : ""
}
}
handleSubmit = (event)=> {
event.preventDefault();
this.setState ({
city : event.target.value
})
}
render () {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input type = "text" city = "city_name" />
<button type="submit">Get Weather</button>
{this.state.city}
</form>
</div>
)
}
}
export default App;
<form
onSubmit={e=>{
e.preventDefault()
console.log(e.target[0].value)
}}>
<input type="text"/>
<button type="submit">dg</button>
</form>
that works for me very well
Remember onSubmit target:
Indicates where to display the response after submitting the form. So you can get inner elements (and their corresponding values) like normal javascript code.
const city = event.target.querySelector('input').value
handleSubmit = (event) => {
event.preventDefault();
this.setState ({ city })
}
I guess you want it to get work like below. But this is not the only way to get it done.
import React from "react";
import { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
city: ""
};
}
handleSubmit = (event) => {
const formData = new FormData(event.currentTarget);
event.preventDefault();
let formDataJson = {};
for (let [key, value] of formData.entries()) {
formDataJson[key] = value;
}
this.setState(formDataJson);
};
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input type="text" name="city" />
<button type="submit">Get Weather</button>
{this.state.city}
</form>
</div>
);
}
}
export default App;
Code sandbox => https://codesandbox.io/s/eager-oskar-dbhhu?file=/src/App.js
constructor() {
super();
this.state = {
name: "",
name1: "",
};
}
change = () => {
this.setState({ name: this.state.name1 });
};
handleChange = (e) => {
this.setState({ name1: e.target.value });
};
render() {
return (
<div>
<input
type="text"
placeholder="Enter your name"
onChange={this.handleChange}
></input>
<button onClick={this.change}>Click Me!</button>
<h4>Hello! {this.state.name}</h4>
</div>
);
}
This is what I did but feels like it is nonsense on actual webpage even it works. Is there a better way to take input from user?
Why you need name and name1 in state. Just name should be fine. See the below code if that helps
I am not sure why you handle a event in button. May you can use a form with OnSubmit.
import React from "react";
import "./style.css";
export default class App extends React.Component {
constructor() {
super();
this.state = {
name: "",
};
}
render() {
return (
<div>
<input
type="text"
placeholder="Enter your name"
onChange={(e) => this.setState({name: e.target.value})}
/>
<button>Click Me!</button>
<h4>Hello! {this.state.name}</h4>
</div>
);
}
}
Your onChange in the input would be
onChange={event => this.handleChange(event)}
And for the button it would be
onChange{() => this.change()}
We would not require 2 states for the name but we would need one variable to store the name and second variable to let the component know that name has been update. We need the second variable because on button click only the name has to be updated(as per the code mentioned).The component would re-render when a state is updated. Below code might be helpful.
class Content extends React.Component {
constructor(){
super()
this.state = {
name: "",
}
this.name=''
}
change = () => {
this.setState({name: this.name})
}
handleChange = (e) => {
this.name=e.target.value
}
render(){
return(
<div>
<input type = "text" placeholder="Enter your name" onChange={this.handleChange}></input>
<button onClick={this.change}>Click Me!</button>
<h4>Hello! {this.state.name}</h4>
</div>
)
}}
Here is a version with React Hooks:
import React, { useState } from 'react';
export default function App() {
const [name, setName] = useState('');
const handleNameChange = (e) => {
const nameInput = e.target.value;
setName(nameInput);
};
return (
<div>
<input
type="text"
placeholder="Enter your name"
onChange={(e) => handleNameChange(e)}
></input>
<button>Click Me!</button>
<h4>Hello! {name}</h4>
</div>
);
}
I have two components in two files: Firebase and Recipe. I call in Recipe a function createRecipe from Firebase file.
When I call this.setState({ recipies }) an error occurs. I searched a solution and tried to bind context according to results here.
Firebase file:
class Firebase {
constructor () {
app.initializeApp(config)
// TRIED
this.createRecipe = this.createRecipe.bind(this)
this.auth = app.auth()
this.db = app.database()
}
state = {
recipies: {}
}
createRecipe = recipe => {
const recipies = {...this.state.recipies}
recipies[`recipe-${Date.now()}`] = recipe
this.setState({ recipies })
}
}
export default Firebase
Recipe file:
import { withAuthorization } from '../Session'
import { withFirebase } from '../Firebase'
class Recipe extends Component {
state = {
name: '',
image: '',
ingredients: '',
instructions: ''
}
handleChange = event => {
const { name, value } = event.target
this.setState({ [name]: value })
}
handleSubmit = event => {
event.preventDefault()
const recipe = { ...this.state }
// TRIED
this.props.firebase.createRecipe(recipe)
this.props.firebase.createRecipe(recipe).bind(this)
this.resetForm(recipe)
}
render () {
return (
<div>
<div className='card'>
<form
// TRIED
onSubmit={this.handleSubmit>
onSubmit={() => this.handleSubmit>
onSubmit={this.handleSubmit.bind(this)}>
<input value={this.state.name} type='text' name='nom' onChange={this.handleChange} />
<input value={this.state.image} type='text' name='image' onChange={this.handleChange} />
<textarea value={this.state.ingredients} rows='3' name='ingredients' onChange={this.handleChange} />
<textarea value={this.state.instructions} rows='15' name='instructions' onChange={this.handleChange} />
<button type='submit'>Add recipe</button>
</form>
</div>
</div>
)
}
}
const condition = authUser => !!authUser
export default withAuthorization(condition)(withFirebase(Recipe))
Do you have an idea about what's going wrong ? Many thanks.
class Firebase doesn't extend React.component so it doesn't know what state is. This is expected, extend React.component or use state hooks useState().
You are not using react in your Firebase component
How to fix:
import React, {Component }from 'react';
class Firebase extends Component {
constructor(props){
super(props)
// your code
}
// your code
render(){
return null; // this won't change anything on your UI
}
}
I am adding a feature to the survey form review for a user to be able to upload files and my concern is that I do not want to mutate state with this implementation, how do I refactor the below to ensure this? Is my only option refactoring it to a class-based component?
// SurveyFormReview shows users their form inputs for review
import _ from "lodash";
import React from "react";
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import formFields from "./formFields";
import * as actions from "../../actions";
export const onFileChange = event => {
this.setState({ file: event.target.files });
};
const SurveyFormReview = ({ onCancel, formValues, submitSurvey, history }) => {
this.state = { file: null };
const reviewFields = _.map(formFields, ({ name, label }) => {
return (
<div key={name}>
<label>{label}</label>
<label>{formValues[name]}</label>
</div>
);
});
return (
<div>
<h5>Please confirm your entries</h5>
{reviewFields}
<h5>Add an Image</h5>
<input
onChange={this.onFileChange.bind(this)}
type="file"
accept="image/*"
/>
Or do I have no choice except to refactor this to a class-based component as a best course?
You should refactor this into a class. Something on the lines of this should work
class SurveyFormReview extends React.Component {
state = { file: null };
onFileChange = event => {
this.setState({ file: event.target.files });
};
render() {
const { onCancel, formValues, submitSurvey, history } = this.props
const reviewFields = _.map(formFields, ({ name, label }) => {
return (
<div key={name}>
<label>{label}</label>
<label>{formValues[name]}</label>
</div>
);
});
return (
<div>
<h5>Please confirm your entries</h5>
{reviewFields}
<h5>Add an Image</h5>
<input
onChange={this.onFileChange}
type="file"
accept="image/*"
/>
</div>
)
}
}
just as a note about optimizations and stuff.
Because this is a form, I'd recommend you use better html elements.
const reviewFields = _.map(formFields, ({ name, label }) => {
return (
<fieldset key={name}>
<span>{label}</span>
<span>{formValues[name]}</span>
</fieldset>
);
});
label elements are usually used with input elements.
fieldset elements are usually for form groups of data
you could use a legend element for the title of your fieldset if you wanted :)
If you don't want to touch existing code, you can create HOC
const withFile = (Component) => class extends React.Component {
state = { file: null }
render() {
return <Component {...this.props} file={file} onAttach={files => this.setState({ file: files }) />
}
}
export default withFile(SurveyForm)
Now your form will receive file and onAttach as props.
I tried to create a custom input component with inputRef (material ui Input component). Looks like the component reference is working but I'm unable to enter any value in the text field after i set the value attribute. I think it's because of the way i implemented the onchange event. I'm not sure what am i missing. Please help.
Here is the codesandbox url
https://codesandbox.io/s/pjlwqvwrvm
Actually you don't need a onChange prop to for get the changed value..
Just get the value from onchange and set the value in state value.
Another mistake is you are not created the constructor, and gave this.props.value to the value prop. That's it not get updated..
Now I created the constructor and give the this.state.value to the value props.
Now you get your onchanged value in custominput component and your submit function also..
import React from "react";
import { render } from "react-dom";
import { Input } from "material-ui-next";
import trimStart from "lodash/trimStart";
import PropTypes from "prop-types";
const defaultProps = {
state: "",
onChange: () => {} // no need
};
const propTypes = {
state: PropTypes.string,
onChange: PropTypes.func
};
class App extends React.Component {
constructor() {
super();
this.state = {
value:''
}
}
handleSubmit(event) {
event.preventDefault();
console.log("state: " + this.state.value); //shows onChanged value in console
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit.bind(this)}>
<CustomInput
labelText="State"
id="state"
value={this.state.value}
onChange={e=> {
this.setState({value:e.target.value})
}}
/>
</form>
</div>
);
}
}
App.propTypes = propTypes;
App.defaultProps = defaultProps;
class CustomInput extends React.Component {
render() {
const {
classes,
formControlProps,
value,
onChange,
labelText,
id,
labelProps,
inputRef,
inputProps
} = this.props;
return (
<div {...formControlProps}>
{labelText !== undefined ? (
<div htmlFor={id} {...labelProps}>
{labelText}
</div>
) : null}
<Input
classes={{
root: labelText !== undefined ? "" : classes.marginTop
}}
id={id}
value={value} ///////// Fixed ////////
onChange={onChange}
inputRef={inputRef}
{...inputProps}
/>
</div>
);
}
}
render(<App />, document.getElementById("root"));
Here is code in sandbox check it..
https://codesandbox.io/s/84rjk4m8l8
You can either go on inputRef - then your value and onChange event are extra - it is called uncontrolled Component. You can see more about it here: https://reactjs.org/docs/uncontrolled-components.html
Or you can do it with value & onChange event - and work with controlled components, you can find more about controlled components here: https://reactjs.org/docs/forms.html#controlled-components
How to solve it (uncontrollable) with inputRef:
class App extends React.Component {
handleSubmit = (event) => {
event.preventDefault();
console.log("input value: ", this.input.value); // will now show you correct input value
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<CustomInput
labelText="State"
id="state"
inputRef={input => {
this.input = input;
}}
/>
<Button onClick={this.handleSubmit} color='primary'>Submit</Button>
</form>
</div>
);
}
}
Instead of "this.state = input", bind input to something else, because this.state is reserved for local state of React Component, and it won't work with it, not like that.
How to solve it (controllable) with state, value & onChange event:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: props.state || ''
}
}
handleSubmit = (event) =>{
event.preventDefault();
console.log("state: ", this.state.value); // will now show you correct input value
}
handleChange = (event) => {
this.setState({value: event.target.value});
}
render() {
const {value} = this.state;
return (
<div>
<form onSubmit={this.handleSubmit}>
<CustomInput
labelText="State"
id="state"
onChange={this.handleChange}
value={value}
/>
<Button onClick={this.handleSubmit} color='primary'>Submit</Button>
</form>
</div>
);
}
}
Note that I added the constructor and defined the local state for component, and I'm changing value inside of state with this.setState (because state is immutable, and that's the right way to update it).
In both examples, you are able to get input value inside of handleSubmit method, will you work with controllable or uncontrollable components, it's up to you :)
Use value={this.state} or value={this.value}
Here is Your sandbox code Updated
https://codesandbox.io/s/qqk2qoxmlj
In my case, I was mistakenly using state from parent component in child component, since I had declared child component inside parent itself. Moving whole state to child component solved the problem.