I am working on a scenario where I have to do a multi-step form which I have already done, as well as the validation part. I am using react-hook-form for validation.
I have multi-step form:
in the first form I have several fields and one radio button
by default radio button is ticked on for auto generated pass so in this case I have nothing to do
the second one is let me create a password so in this case one input field will be show and the user will create the password
Issue
In my final form I am doing the validation like below:
{
fields: ["uname", "email", "password"], //to support multiple fields form
component: (register, errors, defaultValues) => (
<Form1
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
So to validate uname, email and password I am passing the values like above.
But when the radio button is ticked for auto generated password it is still handling the validation, I click on next and it is not going to next for because of password field.
And if I check the radio button as let me create the password it goes to next form and when I came back by clicking back it is going to auto generated password again and it is not holding the previous state. For other input fields it is handling the previous values but not in case of radio button scenario.
My full working code sandbox
Answer 1 The reason is you fields: ["uname", "email", "password"] is fixed, password is always to be taken validation.
Solution Need to store state of Form1 in App so you can check if the state of auto generated password is on remove password from the list
App.js
... other code
// need to move state and function form Form to app
const [show_input, setshow_input] = useState(false);
const createInput = () => {
setshow_input(true);
};
const auto_text = () => {
setshow_input(false);
};
const forms = [
{
// validate based on show_input state
fields: show_input ? ["uname", "email", "password"] : ["uname", "email"], //to support multiple fields form
component: (register, errors, defaultValues) => (
<Form1
register={register}
errors={errors}
defaultValues={defaultValues}
auto_text={auto_text}
createInput={createInput}
show_input={show_input}
/>
)
},
{
fields: ["lname"],
component: (register, errors, defaultValues) => (
<Form2
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
},
{
component: (register, errors, defaultValues) => (
<Form3
register={register}
errors={errors}
defaultValues={defaultValues}
/>
)
}
];
... other code
Answer 2 When you go next the Form1 is unmounted so its state is destroyed. When you store Form1's state in App.js you will fix this issue too
Bonus: It's prefered to use camalCase (E.g: showInput) rather than underscore (show_input)
The main problem is that you render the forms conditionally so all the previous form values are removed. The solution for this is to keep all forms mounted and just use display: none or display: block depending on which form is selected. This way all values will be persisted whenever you go to next or prev form or submit the form.
The second problem that you didn't remove the password field when it's unmounted so when moveToNext is called the valid argument in triggerValidation callback is always false. I fixed that by setting the fields for Form1 conditionally depending on if the password input is visible or not.
The third problem you are using defaultValues for the wrong purpose. You can get the current form values using getValues() which will return all the current values of the form.
I set the default value for uname field just as an example to show you how defaultValues should be used.
you can check the full solution here: https://codesandbox.io/s/fragrant-forest-75pzs?file=/src/App.js
here are all the changed files:
App.js
import React, { useState } from "react";
import Form1 from "./components/Form1";
import Form2 from "./components/Form2";
import Form3 from "./components/Form3";
import { useForm } from "react-hook-form";
function MainComponent() {
const {
register,
triggerValidation,
defaultValues,
errors,
getValues
} = useForm({
// You can set default values here
defaultValues: {
uname: "Lol"
}
});
console.log("Errors: ", errors);
const [currentForm, setCurrentForm] = useState(0);
// control password input visibility and Form1 fields
const [passwordVisible, setPasswordVisible] = useState(false);
const showPassword = () => {
setPasswordVisible(true);
};
const hidePassword = () => {
setPasswordVisible(false);
};
const forms = [
{
fields: passwordVisible
? ["uname", "email", "password"]
: ["uname", "email"],
component: (register, errors) => (
<Form1
// a key is needed to render a list
key={0}
// this will be used to set the css display property to block or none on each form
shouldDisplay={currentForm === 0}
register={register}
errors={errors}
showPassword={showPassword}
hidePassword={hidePassword}
passwordVisible={passwordVisible}
/>
)
},
{
fields: ["lname"],
component: (register, errors) => (
<Form2
key={1}
shouldDisplay={currentForm === 1}
register={register}
errors={errors}
/>
)
},
{
component: (register, errors) => (
<Form3
key={2}
shouldDisplay={currentForm === 2}
register={register}
errors={errors}
values={getValues()}
/>
)
}
];
const moveToPrevious = () => {
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm - 1);
});
};
const moveToNext = () => {
triggerValidation(forms[currentForm].fields).then(valid => {
if (valid) setCurrentForm(currentForm + 1);
});
};
const prevButton = currentForm !== 0;
const nextButton = currentForm !== forms.length - 1;
const handleSubmit = e => {
console.log("whole form data - ", getValues());
};
return (
<div>
<div className="progress">
<div>{currentForm}</div>
</div>
{forms.map(form => form.component(register, errors))}
{prevButton && (
<button
className="btn btn-primary"
type="button"
onClick={moveToPrevious}
>
back
</button>
)}
{nextButton && (
<button className="btn btn-primary" type="button" onClick={moveToNext}>
next
</button>
)}
{currentForm === 2 && (
<button
onClick={handleSubmit}
className="btn btn-primary"
type="submit"
>
Submit
</button>
)}
</div>
);
}
export default MainComponent;
Form1
import React from "react";
function Form1({
register,
errors,
shouldDisplay,
passwordVisible,
showPassword,
hidePassword
}) {
return (
<div style={{ display: shouldDisplay ? "block" : "none" }}>
<form autoComplete="on">
<br />
<div className="form-group">
<label>User name</label>
<input type="text" name="uname" ref={register({ required: true })} />
{errors.uname && <span>required</span>}
<label>Email</label>
<input type="email" name="email" ref={register({ required: true })} />
{errors.email && <span>required</span>}
</div>
<div>
<div className="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12">
<label className="form_label">Password</label>
<div className="form-check">
<label>
<input
type="radio"
name="auto_pass"
id="Radios1"
value="auto_pass"
className="form-check-input"
defaultChecked={true}
onChange={hidePassword}
/>
Auto generated password
</label>
</div>
<div className="form-check">
<label>
<input
type="radio"
name="auto_pass"
id="Radios2"
value="let_me"
className="form-check-input"
onChange={showPassword}
/>
Let me create the password
</label>
</div>
</div>
{passwordVisible && (
<div className="col-12 col-sm-12 col-md-12 col-lg-12 col-xl-12 mb-3">
<label className="form_label">Password</label>
<input
type="password"
name="password"
className="form-control"
ref={register({ required: true })}
/>
{errors.password && (
<span className="text-danger">Password is reguired</span>
)}
</div>
)}
</div>
</form>
</div>
);
}
export default Form1;
Form2
import React from "react";
function Form2({ register, errors, shouldDisplay }) {
return (
<div style={{ display: shouldDisplay ? "block" : "none" }}>
<form autoComplete="on">
<br />
<div className="form-group">
<label>User last name</label>
<input type="text" name="lname" ref={register({ required: true })} />
{errors.lname && <span>required</span>}
</div>
</form>
</div>
);
}
export default Form2;
Form3
import React from "react";
function Form3({ values, shouldDisplay }) {
return (
<div style={{ display: shouldDisplay ? "block" : "none" }}>
<h3>Want to display all values here like below</h3>
{Object.entries(values).map(([key, value]) => (
<p key={key}>
{key}: {value}
</p>
))}
<br />
<p>So that use can check for any Wrong info</p>
</div>
);
}
export default Form3;
Related
I'm back once more with something that has been breaking my head today.
so I'm making a contact form is almost done except for an animation I want to include that comes in three steps.
1- prompting the user to contact
2-making the waiting for the user-friendlier with a small loader
3-showing either everything went good and the form was sent or something went wrong
so my idea to accomplish this was to use three different icons/loaders and position all of them on top of each other and make them visible or hide them as necessary using UseState.
for now, I can hide the first icon(from step one) as soon as the submit button is clicked, but I haven't been able to make appear the loader or as the API completes the response the last icon
wondering if I should access it any other way?
import styled from "styled-components";
import { RiMailSendFill,FcApproval } from 'react-icons/all';
import '../../Style/styleComponents/Style.css';
import {sendMessage} from '../../Actions/msgAction';
import { useDispatch, useSelector } from 'react-redux';
import { useForm } from "../../Hook/useForm";
const ContactForm = () => {
const dispatch = useDispatch();
const initFormValues =
{
nombre : "nico ",
email : "sasjaja#asdsa ",
telefono : "asda ",
empresa : "dasd",
mensaje : "dasdas",
date: "s"
}
const[formValues, handleInputChange, reset] = useForm(initFormValues);
const{nombre, email, telefono, empresa, mensaje} = formValues
//loader submit buttons
const[mail, setMail]= useState(true);
const[loading, setLoading]= useState(false);
const[approved, setApproved]= useState(false);
const handleSendMsg = ( event ) =>
{
event.preventDefault();
dispatch( sendMessage( formValues ) )
.then( ( result ) =>
{
if( result )
{
console.log(initFormValues);
//closeModal();
console.log('dat')
};
setLoading(true);
});
reset();
}
const showE =()=> {
if (mail) {
setMail(false)
setLoading(true)
console.log("pressed submit");
}
}
const showL=()=>{
if (loading) {
setLoading(false)
console.log("sending email ");
}
}
return (
<Container>
<Left>
<h1>Tráenos tus desafíos</h1>
</Left>
<Right>
<form onSubmit={ handleSendMsg }>
<Label>
<Linput>
<Name>
<label htmlFor="name">Nombre:</label>
<input type="text" id="name" required name="nombre" value={ nombre } onChange={ handleInputChange } />
</Name>
<Tel>
<label htmlFor="name">Telefono:</label>
<input type="text" id="phone" required name="telefono" value={ telefono } onChange={ handleInputChange } />
</Tel>
<Company>
<label htmlFor="company">Empresa:</label>
<input type="text" id="company" name="empresa" value={ empresa} onChange={ handleInputChange }/>
</Company>
</Linput>
<Rinput>
<Email>
<label htmlFor="email">E-mail:</label>
<input type="email" id="email" required name="email" value={ email } onChange={ handleInputChange }/>
</Email>
<Msg>
<label htmlFor="message">Mensaje:</label>
<textarea id="message" required name="mensaje" rows="8" cols="50" value={ mensaje } className="bigger" onChange={ handleInputChange }/>
</Msg>
</Rinput>
</Label>
<Button>
<Send>
<button type="Submit" id ="submit" onClick={showE, showL}>Enviar</button>
</Send>
<Sent>
<RiMailSendFill id="mail" className={ mail ? 'svg': "svg active"}/>
<Loader className={ loading ? 'spin': "spin active"}>
<div class="spin"></div>
</Loader>
{/* <FcApproval id= "approve" className={ approved ? 'approve': "approve active"}/> */}
</Sent>
</Button>
</form>
</Right>
</Container>
);
};
export default ContactForm;
thanks, yall always saving me!
There are ways to make states work with each other easiest way is like this.
1-useStates for each of the
elements you want to be able to switch states to.
const [mail, setMail] = useState(true);
const [approved, setApproved] = useState(false);
2 Main function with smaller functions,
for each one to change accordingly.
function showL( ){
setMail(false);
if(!mail){
return setApproved(true);
}
}
//hide mailIcon / show approve
function showA( ){
setApproved(true);
if(approved){
return setMail(false);
}
}
add an event listener to the specific
the element you will work with to trigger the changes,
here you pass the two functions like this.
<Button>
<Send>
<button type="Submit" id="submit" onClick={() => {showL(); showA();}}>
Enviar
</button>
</Send>
TERNARY EXPRESION if true, render element and false null
<Sent>
<EMail>
{mail ? <RiMailSendFill/> : null}
</EMail>
<Loader>
{approved ? <FcApproval/>: null}
</Loader>
</Sent>
</Button>```
I'm trying to do validations for my form in react. I chose "react-hook-form" library. But I'm constantly getting error "Path.split is not a function. Even after using the default example given in their website, I'm getting the same error.
This is the default code given in the official site.
import React from "react";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit, watch, errors } = useForm();
const onSubmit = data => console.log(data);
console.log(watch("example")); // watch input value by passing the name of it
return (
{/* "handleSubmit" will validate your inputs before invoking "onSubmit" */}
<form onSubmit={handleSubmit(onSubmit)}>
{/* register your input into the hook by invoking the "register" function */}
<input name="example" defaultValue="test" ref={register} />
{/* include validation with required or other standard HTML validation rules */}
<input name="exampleRequired" ref={register({ required: true })} />
{/* errors will return when field validation fails */}
{errors.exampleRequired && <span>This field is required</span>}
<input type="submit" />
</form>
);
}
react-hook-form updated to 7.0.0 from 6.X.X and has breaking changes:
You have to replace all ref={register} with {...register('value_name')}
Example:
Version 6.X.X:
<input ref={register({ required: true })} name="test" />
Version 7.0.X:
<input {...register('test', { required: true })} />
Simple input with required and errors.message features, necessary changes in update:
From version 6.x.x:
function MyComponent(props) {
const { register, handleSubmit, errors } = useForm();
const onSubmit = (values) => {...};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<input
name="message"
autoComplete="off"
ref={register({
required: "Required",
})}
/>
{errors.message && errors.message.message}
<input type="submit" />
</form>
</div>
);
}
To version 7.x.x:
function MyComponent(props) {
const { register, handleSubmit, formState: { errors }} = useForm();
const onSubmit = (values) => {...};
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<input
name="message"
autoComplete="off"
{...register("message", {
required: "Required",
})}
/>
{errors.message && errors.message.message}
<input type="submit" />
</form>
</div>
);
}
In addition to register fix, if you use errors from useForm(), now errors feature is exported from formState.
Worth mentioning that if you are using material ui or something similar, where ref={ref} throws an error (because it expects a different prop name instead of ref), you might want to
import { TextField } from '#material-ui/core';
return (
<TextField {...register('name')} />
)
there is a migration guide for this here, but still worth to mention
As noted above, there are changes to how register is to be used in v7
If you are still getting errors, ensure that id is actually a string and not any other type such as a number.
<input {...register(id)} />
import { useForm } from "react-hook-form";
export default function App() {
const { register, formState: { errors }, handleSubmit } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { required: true })} />
{errors.firstName?.type === 'required' && "First name is required"}
<input {...register("lastName", { required: true })} />
{errors.lastName && "Last name is required"}
<input type="submit" />
</form>
);
}
I already built the form in React and it shows the input fields in red borders that'll change to regular borders once someone types it in. I used this example from this React form article link So everything is working except I wanted to add the error message under the input field that displays "Please fill in the blank field" that will disappear once someone starts typing in the field. How do I do this?
Here's my code in Form.js:
import React, { Component } from 'react';
import FormField from './FormFieldBox';
function validate(name, isin) {
// true means invalid, so our conditions got reversed
return {
name: name.length === 0,
isin: isin.length === 0
};
}
export default class PopupForm extends Component {
constructor(props) {
super(props)
this.state = {
name: '',
isin: '',
country: '',
errormessage: ''
}
}
updateInput = (e) =>{
this.setState({[e.target.name]: e.target.value})
}
closePopupSubmit = (e) => {
if (!this.canBeSubmitted()) {
e.preventDefault();
}
let security = { //1.gather security data from form submit
name: this.state.name,
isin: this.state.isin,
country: this.state.country
}
this.props.submitPopup(security); //2.closePopup function, add security data
}
canBeSubmitted() {
const errors = validate(this.state.name, this.state.isin);
const isDisabled = Object.keys(errors).some(x => errors[x]);
return !isDisabled;
}
cancelPopupSubmit = (e) => {
e.preventDefault()
this.props.cancelPopup();
}
render() {
const errors = validate(this.state.name, this.state.isin);
const isDisabled = Object.keys(errors).some(x => errors[x]);
return (
<div className='popup'>
<div className='popup-inner'>
<form onSubmit={this.closePopupSubmit}>
<FormField onChange={this.updateInput} className={errors.name ? "input error" : "input"} label="Name" type="text" name="name" value={this.state.name} />
<FormField onChange={this.updateInput} className={errors.isin ? "input error" : "input"} label="ISIN" type="text" name="isin" value={this.state.isin} />
<FormField onChange={this.updateInput} label="Country" type="text" name="country" value={this.state.country} />
<button type="button" onClick={this.cancelPopupSubmit} className="button">Cancel</button>
<button type="submit" className="button" disabled={isDisabled}>Submit</button>
</form>
</div>
</div>
)
}
}
And my component FormField.js
import React from "react";
const FormBox = props => {
return (
<div className="field">
<label className="label">{props.label}</label>
<div className="control">
<input onChange={props.onChange}
className={props.className}
type={props.type}
name={props.name}
value={props.value}
placeholder={props.placeholder} />
{/* {props.errormessage} */}
</div>
</div>
)
}
export default FormBox;
const FormBox = props => {
return (
<div className="field">
<label className="label">{props.label}</label>
<div className="control">
<input onChange={props.onChange}
className={props.className}
type={props.type}
name={props.name}
value={props.value}
placeholder={props.placeholder} />
</div>
{Boolean(props.value.length) || (
<div className="err-msg">
Please fill in the blank field
</div>
)}
</div>
)
}
There are two ways you can achieve this
First : oninvalid attribute in HTML5 and calling a custom function on that.
Second : along with each element name object in state have a length attribute. In validation function you can check for the length and throw a custom error that you want to display.
I am using redux-form 7.0.4, it its working fine other than the following issue.
New user can enter details and existing user can edit details on the same form. I am having issue when existing user edit details and remove data from form field. I want to add check onBlur. If user empty form field and there is blur event I need to fill the initial data inside field.
Restore the initial/server value on blur if field is empty.
Form Field:
<Field
component={InputField}
type='text'
name='registeredName'
className='form-control'
required
placeholder='Registered Business Name*'
/>
InputField Component:
const InputField = ({
input,
type,
placeholder,
meta: { touched, error, initial }
}) => {
const onChange = (e) => {
const val = e.target.value;
if (type === 'phoneNumber') {
if (!/^\d+$/.test(val) || String(val).length > 10) {
return;
}
}
input.onChange(e);
};
return (
<div className='form-group'>
<input
{...input}
onChange={onChange}
type={type}
className='form-control'
placeholder={placeholder}
value={input.value }
onBlur={e => {input.value = (!e.target.value) ? initial : e.target.value;}}
/>
{touched &&
((error &&
<div className='error text-danger'>
{error}
</div>
))}
</div>
);
};
/* eslint-enable */
export default InputField;
You need to set onChange and onBlur in the Field component like this:
<Field
component={InputField}
type='text'
name='registeredName'
className='form-control'
required
placeholder='Registered Business Name*'
onChange={(event) => {
...your onChange function here...
}}
onBlur={(event) => {
...Your onBlur function here...
}}
/>
I'd recommend having a good read through the docs for the <Field /> component.
Hope that's helpful
My application has dynamic routes (dynamic route param), in which it contains the redux form.In order to distinguish the form data, I need to post redux form data along with the react route param.
I have passed the react route param as props from the parent component to the child component having the redux form i.e, initial values in props have the param value.I want to initialize the route param to the input field with hidden type.
import React from 'react';
import { Field, reduxForm,propTypes } from 'redux-form';
import submit from '../actions/commentActions'
import connect from 'react-redux';
const validate = values => {
const errors = {}
if (!values.email) {
errors.email = 'Required'
} else if (!/^[A-Z0-9._%+-]+#[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
if (!values.message) {
errors.message = 'Required !!'
}else if (values.message.length > 15) {
errors.message = 'Must be 15 characters or less'
}
return errors
}
const renderField = ({
input,
label,
type,
meta: { touched, error, warning }
}) => (
<div>
<div>
<input {...input} placeholder={label} type={type} className="form-control" />
{touched &&
((error && <span className="text-danger">{error}</span>) )}
</div>
</div>
)
const renderTextAreaField = ({
input,
label,
type,
meta: { touched, error, warning }
}) => (
<div>
<div>
<textarea {...input} rows="3" placeholder={label}
className="form-control shareThought mt-1"></textarea>
{touched &&
((error && <span className="text-danger">{error}</span>) )}
</div>
</div>
)
const AddComment = props => {
const { error,handleSubmit, pristine, reset, submitting,initialValues } = props;
// console.log(initialValues); prints route param i.e honda
// console.log(props);
return (
<div className="leaveComment pb-2">
<form onSubmit={handleSubmit(submit)}>
<Field
name="email"
component={renderField}
type="email"
label="Email Id"
placeholder="Enter Email"
/>
<Field name="message"
component={renderTextAreaField}
label="Share Your thought"
type="text"
/>
<Field name="modelname"
type="text"
component="input"
value={initialValues}
hidden
/>
<span className="text-danger">{error && <span>{error}</span>}</span>
<div className="row mx-0" >
<button type="submit" className="btn btn-sm btn-info btn-block mt-2" disabled={pristine || submitting}>Leave a comment</button>
</div>
</form>
</div>
);
};
export default reduxForm({
form: 'addcommentmsg',
validate
})(AddComment);
I solved this issue by passing initialValues with key-value
let initialValues = {
initialValues: {
modelname: this.props.pageId
}
};
Therefore you dont have to define the initialValues either in input field or props.
I also could make it work only this way:
const mapStateToProps = state => {
return {
initialValues: {
status: state.profile.userStatus
}
}
}
And input has such value:
value={props.initialValues}