Hi i'm newbie learning to code and start with Js, Reactjs. I convert my code from class component to functional component and then my validation stop working. i have looking around but yet to find answer, can someone help? ^^"
export default function AddComment() {
const [formValue, setFormValue] = useState ({
rating: '5',
yourname: '',
comment: '',
})
const {rating, yourname, comment} = formValue
const [isModalOpen, setIsModalOpen] = useState(false)
const [touched, setTouched] = useState({yourname: false})
const handleInputChange = (event) => {
const {name, value} = event.target
setFormValue((prevState) => {
return {
prevState,
[name]:value,
};})
}
const handleBlur = (field) => (evt) => {
setTouched({
touch: {...touched, [field]: true }
})
};
function validate(yourname) {
const errors = {
yourname: ''
};
if (touched.yourname && yourname.length <2)
errors.yourname = 'Your name should be atleast 2 characters';
else if (touched.yourname && yourname.length >= 10)
errors.yourname = 'Your name should be less than 10 characters';
return errors
}
const errors = validate(yourname)
return (
<ModalBody>
<Form onSubmit={handleSubmit}>
<FormGroup row>
<Label htmlFor='yourname' md = {12}>Your Name</Label>
<Col md = {12}>
<Input type= 'text' id= 'yourname' name= 'yourname' value= {yourname}
valid={errors.yourname === ''} invalid= {errors.yourname !== ''}
onBlur= {handleBlur('yourname')} onChange= {handleInputChange}/>
<FormFeedback>{errors.yourname}</FormFeedback>
</Col>
</FormGroup>
</Form>
</ModalBody>
)
}
Not sure this will solve your problem, but notice on handleInputChange you are not spreading the previous state, so the object you're setting state with looks like:
{
prevState: {...the actual previous state},
[name]: ...what you wanted to change
}
Related
I am trying to make a form where inputs fields can be generated dynamically. While running it runs but react throws a warning about not being a controlled component. Any example for the solution I look online uses classes and constructors, is there any other way to do it without using classes?
import { useState } from 'react';
function Try() {
const [formFields, setFormFields] = useState([
{ StepNo: '', StepDisc: '' },
])
const handleFormChange = (event, index) => {
let data = [...formFields];
data[index][event.target.name] = event.target.value;
setFormFields(data);
}
const submit = (e) => {
e.preventDefault();
console.log(formFields)
}
const addFields = () => {
let object = {
StepNo: '',
StepDiscs: ''
}
setFormFields([...formFields, object])
}
const removeFields = (index) => {
let data = [...formFields];
data.splice(index, 1)
setFormFields(data)
}
return (
<div className="">
<form onSubmit={submit}>
{formFields.map((form, index) => {
return (
<div key={index}>
<input
name='StepNo'
placeholder='StepNo'
onChange={event => handleFormChange(event, index)}
value={form.StepNo}
/>
<input
name='StepDisc'
placeholder='StepDisc'
onChange={event => handleFormChange(event, index)}
value={form.StepDisc}
/>
<button onClick={() => removeFields(index)}>Remove</button>
</div>
)
})}
</form>
<button onClick={addFields}>Add More..</button>
<br />
<button onClick={submit}>Submit</button>
</div>
);
}
export default Try;
It's not related to the error you have, but I suggest you bind the key to a unique key as it can generate unpredictable issues when you remove the items.
const addFields = () => {
// Id should has a unique a id to be used in as a key.
const id = generateUniqueKey()
let object = {
StepNo: '',
StepDiscs: '',
id
}
setFormFields([...formFields, object])
}
{formFields.map((form, index) => {
return (
<div key={form.id}>
<input
name='StepNo'
placeholder='StepNo'
onChange={event => handleFormChange(event, index)}
value={form.StepNo}
/>
<input
name='StepDisc'
placeholder='StepDisc'
onChange={event => handleFormChange(event, index)}
value={form.StepDisc}
/>
<button onClick={() => removeFields(index)}>Remove</button>
</div>
)
})}
There's just typo in your code when you initialize new form data when adding. Instead of
let object = {
StepNo: '',
StepDiscs: ''
}
It should be
let object = {
StepNo: '',
StepDisc: ''
}
You need to change StepDiscs to StepDisc :-)
const addFields = () => {
let object = {
StepNo: '',
StepDiscs: ''
}
So I was learning my React course today on Udemy and came across an error where the react app renders [object Object] instead of the JSX (which is supposed to be an empty input box), also, since the input box has onChange method, The input value is also unable to change. Here is the image of the application and also the code given below:-
import React, { useState, useEffect, useReducer } from "react";
import Card from "../UI/Card/Card";
import classes from "./Login.module.css";
import Button from "../UI/Button/Button";
const emailReducer = (state, action) => {
if(action.type === 'USER_INPUT'){
return {value: action.value, isValid: action.value.includes('#')};
}
if(action.type === 'INPUT_BLUR'){
return {value: state.value, isValid: state.value.includes('#')};
}
return { value: '', isValid: false };
};
const Login = (props) => {
// const [enteredEmail, setEnteredEmail] = useState("");
// const [emailIsValid, setEmailIsValid] = useState();
const [enteredPassword, setEnteredPassword] = useState("");
const [passwordIsValid, setPasswordIsValid] = useState();
const [formIsValid, setFormIsValid] = useState(false);
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: '',
isValid: false
});
useEffect(() => {
console.log("EFFECT RUNNING");
return () => {
console.log("EFFECT CLEANUP");
};
}, []);
const emailChangeHandler = (event) => {
dispatchEmail({type: 'USER_INPUT', value: event.target.value});
setFormIsValid(
emailState.isValid && enteredPassword.trim().length > 6
);
};
const passwordChangeHandler = (event) => {
setEnteredPassword(event.target.value);
setFormIsValid(
emailState.isValid && event.target.value.trim().length > 6
);
};
const validateEmailHandler = () => {
dispatchEmail({type: 'INPUT_BLUR'});
};
const validatePasswordHandler = () => {
setPasswordIsValid(enteredPassword.trim().length > 6);
};
const submitHandler = (event) => {
event.preventDefault();
props.onLogin(emailState.value, enteredPassword);
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailState.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
id="email"
value={emailState}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
<div
className={`${classes.control} ${
passwordIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={enteredPassword}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
</div>
<div className={classes.actions}>
<Button type="submit" className={classes.btn} disabled={!formIsValid}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
Your emailState is an object with the following shape:
{ value: string, isValid: boolean }
So in your input field you need to use the value attribute:
<input
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
You are passing the entire emailState object to the input when you likely want the nested value property. You can pass emailState.value to the input's value prop, or destructure the state beforehand and pass value directly.
const [{ isValid, value }, dispatchEmail] = useReducer(emailReducer, {
value: '',
isValid: false
});
...
<input
id="email"
value={value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
const { useReducer } = React
const InputWithLabelAbove = ({
labelText,
id,
onChange,
pattern,
value,
}) => {
return (
<label htmlFor={id}>
{labelText}
<div>
<input
type="text"
id={id}
pattern={pattern}
value={value}
onChange={onChange}
/>
</div>
</label>
)
}
const MemoInputWithLabelAbove = React.memo(InputWithLabelAbove)
const Component = () => {
// calc object
const calculatorObj = {
tax: '',
water: '',
energy: '',
internet: '',
transport: '',
food: '',
education: '',
childcare: '',
otherInsurance: '',
otherCosts: ''
}
// state
const inputValues = {
total: '',
showCalculator: false,
calculatedTotal: 0,
calc: {
...calculatorObj
}
}
// reducer for form states
const [values, setValues] = useReducer(
(state, newState) => ({...state, ...newState}),
inputValues
)
// on change function to handle field states.
const handleChange = React.useCallback((e, type) => {
const { value, id } = e.target
console.log('onchange')
const re = /^[0-9\b]+$/
const converted = !re.test(value) || value.length === 0 ? '' : parseInt(value, 10)
if (type === 'calculator') {
const obj = {
...values.calc,
[id]: converted
}
setValues({ calc: { ...obj }})
}
}, [values.calc])
const calcLabelArr = ['Council tax', 'Water', 'Energy (gas and/or electricity)', 'Internet', 'Transport', 'Food', 'Children\'s education', 'Childcare', 'Other insurance', 'Other essential costs']
return (
<div
style={{ width: '60%', marginBottom: '20px', position: 'relative' }}
>
{ Object.entries(values.calc).map((i, index) => {
return <div key={calcLabelArr[index]}>
<MemoInputWithLabelAbove
id={i[0]}
type="text"
labelText={calcLabelArr[index]}
onChange={(e) => handleChange(e, 'calculator')}
value={i[1]}
/>
</div>
}
)}
</div>
)
}
ReactDOM.render(
<Component />,
document.getElementById('reactBind')
)
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="reactBind"></div>
Below is the rendering of the inputs using an array (with 10 elements) so 10 input elements are rendered.
// calculator object typically populated but for this example its empty for ease.
const calcLabelArr = []
// function to return calculator fields
const buildView = () => {
return (
<Col
xs="12"
md={{ span: 6, offset: 3 }}
style={{ marginBottom: '20px', position: 'relative' }}
>
{ Object.entries(values.calc).map((i, index) => {
return <div key={calcLabelArr[index]}>
<InputWithLabelAbove
id={i[0]}
type="text"
labelPosition="top"
labelText={calcLabelArr[index]}
onChange={(e) => handleChange(e, 'calculator')}
value={i[1]}
/>
</div>
}
)}
</Col>
)
}
Below is the onChange function used to set that state of each input.
const handleChange = React.useCallback((e, type) => {
const { value, id } = e.target
const re = /^[0-9\b]+$/
const converted = !re.test(value) || isEmpty(value) ? '' : parseInt(value, 10)
if (type === 'calculator') {
const obj = {
...values.calc,
[id]: converted
}
setValues({ calc: { ...obj }})
} else {
setValues({
total: converted,
})
}
}, [values.calc])
Below is the component that is memoized.
import React from 'react'
import { join } from 'lodash'
import { Label, StyledInput, Red } from './style'
export type IProps = {
labelPosition: string,
labelText: string,
id: string,
hasErrored?: boolean,
onChange: () => void,
dataUxId?: string,
pattern?: string,
sessioncamHide?: boolean,
sessioncamClassList?: string | string[],
value?: string,
validation?: boolean,
}
const InputWithLabelAbove: React.FC<IProps> = ({
labelPosition,
labelText,
id,
hasErrored,
onChange,
dataUxId,
pattern,
sessioncamHide,
sessioncamClassList,
value,
validation,
}) =>
(
<Label hasErrored={hasErrored} labelPosition={labelPosition} htmlFor={id}>
{labelText && (<span>{labelText}{validation && (<Red>*</Red>)}</span>)}
<div>
<StyledInput
type="text"
id={id}
hasErrored={hasErrored}
dataUxId={`InputText_${dataUxId}`}
pattern={pattern}
labelPosition={labelPosition}
value={value}
onInput={onChange}
onChange={onChange}
/>
</div>
</Label>
)
export const MemoInputWithLabelAbove = React.memo(InputWithLabelAbove)
As you can see, it isn't the key I don't think that is causing the re-render, my input component is memoized and the onChange is using a callback, however on using the react profiler, every onChange re-renders all my input components. Could someone elaborate to why this is?
One thing that jumps out is this property on the input component:
onChange={(e) => handleChange(e, 'calculator')}
Even though handleChange is memoized, you're creating a new arrow function every time to call the memoized function. So even if the input is memoized, it's seeing a new onChange every time.
You'd need to pass a stable function to avoid re-rendering, for instance:
const onChange = React.useCallback(
e => handleChange(e, "calculator"),
[handleChange]
);
and then
onChange={onChange}
(It wasn't clear to me where you're defining handleChange; if it's within the component that's rendering these inputs, you can probably combine that definition with the above in a single useCallback or possible useMemo if it's multiple callbacks. Although small pieces are good too.)
Currently, in my form, the input fields get validated as soon as the user types in something. Here's the code for that-
index.js
import React from "react";
import ReactDOM from "react-dom";
import ShowError from "./ShowError";
import "./styles.css";
class App extends React.Component {
state = {
email: "",
name: "",
mobile: "",
errors: {
email: "",
name: "",
mobile: ""
},
nameError: false,
emailError: false,
mobileError: false,
formError: false
};
showMsg = () => {
if (!this.state.formError) {
alert("Error");
}
};
validateFunc = (name, value) => {
let error = this.state.errors;
let nameError = this.state.nameError;
let emailError = this.state.emailError;
let mobileError = this.state.mobileError;
switch (name) {
case "name":
nameError = !/^[a-zA-Z ]+$/.test(value);
error.name = nameError ? " is Invalid" : "";
break;
case "email":
emailError = !/^([\w.%+-]+)#([\w-]+\.)+([\w]{2,})$/i.test(value);
error.email = emailError ? " is Invalid" : "";
break;
case "mobile":
mobileError = !/^[0-9]{10}$/.test(value);
error.mobile = mobileError ? " is Invalid" : "";
break;
}
this.setState({
errors: error,
nameError: nameError,
emailError: emailError,
mobileError: mobileError
});
};
handleInput = event => {
const name = event.target.name;
const value = event.target.value;
this.setState(
{
[name]: value
},
this.validateFunc(name, value)
);
};
handleSubmit = event => {
event.preventDefault();
let formError =
this.state.nameError || this.state.emailError || this.state.mobileError;
this.setState({
formError: formError
});
};
render() {
return (
<div className="App">
<h1>Basic Form Validation</h1>
<form className="FormStyle">
<input
className="FieldStyle"
type="text"
name="name"
placeholder="Name"
onChange={event => this.handleInput(event)}
/>
<input
className="FieldStyle"
type="email"
name="email"
placeholder="Email"
onChange={event => this.handleInput(event)}
/>
<input
className="FieldStyle"
type="number"
name="mobile"
placeholder="Mobile"
onChange={event => this.handleInput(event)}
/>
<button
className="FieldStyle"
type="submit"
onClick={event => this.handleSubmit(event)}
>
SUBMIT
</button>
</form>
<ShowError error={this.state.errors} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
ShowError.js
import React from "react";
const ShowError = props => {
return Object.keys(props.error).map((field, index) => {
if (props.error[field].length > 0) {
return (
<p key={index}>
{field} {props.error[field]}
</p>
);
} else {
return "";
}
});
};
export default ShowError;
Expected Behaviour- What I want is, the fields should get validated as soon as the user focuses on next input field or when 'tab' key is pressed, I don't want to validate while the user is typing but when the user switches the field. How can I achieve the above behaviour? Thanks a lot!
P.S. - Would be better if I can achieve this without using libraries like redux-forms.
Use onBlur so the event is triggered when the user leaves the component
onBlur={event => this.handleInput(event)}
Hi i found an answer to this for a single field form... but what if we have a form with multiple field?
this is fine for disabling it if you have 1 field but it does not work when you want to disable it based on many fields:
getInitialState() {
return {email: ''}
},
handleChange(e) {
this.setState({email: e.target.value})
},
render() {
return <div>
<input name="email" value={this.state.email} onChange={this.handleChange}/>
<button type="button" disabled={!this.state.email}>Button</button>
</div>
}
})
Here is a basic setup for form validation:
getInitialState() {
return {
email: '',
text: '',
emailValid: false, // valid flags for each field
textValid: false,
submitDisabled: true // separate flag for submit
}
},
handleChangeEmail(e) { // separate handler for each field
let emailValid = e.target.value ? true : false; // basic email validation
let submitValid = this.state.textValid && emailvalid // validate total form
this.setState({
email: e.target.value
emailValid: emailValid,
submitDisabled: !submitValid
})
},
handleChangeText(e) { // separate handler for each field
let textValid = e.target.value ? true : false; // basic text validation
let submitValid = this.state.emailValid && textvalid // validate total form
this.setState({
text: '',
textValid: textValid,
submitDisabled: !submitValid
})
},
render() {
return <div>
<input name="email" value={this.state.email} onChange={this.handleChangeEmail}/>
<input name="text" value={this.state.text} onChange={this.handleChangeText}/>
<button type="button" disabled={this.state.submitDisabled}>Button</button>
</div>
}
})
In a more elaborate setup, you may want to put each input field in a separate component. And make the code more DRY (note the duplication in the change handlers).
There are also various solutions for react forms out there, like here.
I would take a little bit different way here...
Instead of setting submitDisabled in every single onChange handler I would hook into lifecycle method to listen to changes.
To be exact into componentWillUpdate(nextProps, nextState). This method is invoked before every change to component - either props change or state change. Here, you can validate your form data and set flag you need - all in one place.
Code example:
componentWillUpdate(nextProps, nextState) {
nextState.invalidData = !(nextState.email && nextState.password);
},
Full working fiddle https://jsfiddle.net/4emdsb28/
This is how I'd do it by only rendering the normal button element if and only if all input fields are filled where all the states for my input elements are true. Else, it will render a disabled button.
Below is an example incorporating the useState hook and creating a component SubmitButton with the if statement.
import React, { useState } from 'react';
export function App() {
const [firstname, setFirstname] = useState('');
const [lastname, setLastname] = useState('');
const [email, setEmail] = useState('');
function SubmitButton(){
if (firstname && lastname && email){
return <button type="button">Button</button>
} else {
return <button type="button" disabled>Button</button>
};
};
return (
<div>
<input value={email} onChange={ e => setEmail(e.target.value)}/>
<input value={firstname} onChange={ e => setFirstname(e.target.value)}/>
<input value={lastname} onChange={ e => setLastname(e.target.value)}/>
<SubmitButton/>
</div>
);
};
This might help. (credits - https://goshakkk.name/form-recipe-disable-submit-button-react/)
import React from "react";
import ReactDOM from "react-dom";
class SignUpForm extends React.Component {
constructor() {
super();
this.state = {
email: "",
password: ""
};
}
handleEmailChange = evt => {
this.setState({ email: evt.target.value });
};
handlePasswordChange = evt => {
this.setState({ password: evt.target.value });
};
handleSubmit = () => {
const { email, password } = this.state;
alert(`Signed up with email: ${email} password: ${password}`);
};
render() {
const { email, password } = this.state;
const isEnabled = email.length > 0 && password.length > 0;
return (
<form onSubmit={this.handleSubmit}>
<input
type="text"
placeholder="Enter email"
value={this.state.email}
onChange={this.handleEmailChange}
/>
<input
type="password"
placeholder="Enter password"
value={this.state.password}
onChange={this.handlePasswordChange}
/>
<button disabled={!isEnabled}>Sign up</button>
</form>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<SignUpForm />, rootElement);
export default function SignUpForm() {
const [firstName, onChangeFirstName] = useState("");
const [lastName, onChangeLastName] = useState("");
const [phoneNumber, onChangePhoneNumber] = useState("");
const areAllFieldsFilled = (firstName != "") && (lastName != "") && (phoneNumber != "")
return (
<Button
title="SUBMIT"
disabled={!areAllFieldsFilled}
onPress={() => {
signIn()
}
}
/>
)
}
Similar approach as Shafie Mukhre's!