How to add input validation in react? - javascript

I am having a simple form that has firstName and lastName.
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
For this I am trying to add validation.
The validation rules are,
Both fields should accept only text
First name is required and should have at least 4 characters.
If Last name field has value, then it needs to be at least 3 characters.
Things I have tried to achieve this,
components/utils.js
export function isLettersOnly(string) {
return /^[a-zA-Z]+$/.test(string);
}
components/basic_details.js
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
return;
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
On handle input field, I am making the validation to check whether the input has value but I am unable to get the point how to catch the actual validation error and display below respective input box.
Kindly please help me to display the validation message on the respective fields.
Working example:

I suggest adding an errors property to the form data in form_context:
const [formValue, setFormValue] = useState({
basicDetails: {
firstName: '',
lastName: '',
profileSummary: '',
errors: {},
},
...
});
Add the validation to basic_details subform:
const ErrorText = ({ children }) => (
<div style={{ color: 'red' }}>{children}</div>
);
const BasicDetails = () => {
const [value, setValue] = React.useContext(FormContext);
const { basicDetails } = value;
const handleInputChange = (event) => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: 'Can have only letters.',
},
},
}));
return;
}
switch (name) {
case 'firstName': {
const error = value.length < 4 ? 'Length must be at least 4.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
case 'lastName': {
const error = value.length < 3 ? 'Length must be at least 3.' : null;
setValue((value) => ({
...value,
basicDetails: {
...value.basicDetails,
errors: {
...value.basicDetails.errors,
[name]: error,
},
},
}));
break;
}
default:
// ignore
}
setValue((prev) => {
const basicDetails = { ...prev.basicDetails, [name]: value };
return { ...prev, basicDetails };
});
};
return (
<>
<br />
<br />
<div className="form-group col-sm-6">
<label htmlFor="firstName">First Name: </label>
<input
type="text"
className="form-control"
id="firstName"
name="firstName"
value={basicDetails.firstName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.firstName && (
<ErrorText>{basicDetails.errors.firstName}</ErrorText>
)}
<br />
<br />
<div className="form-group col-sm-4">
<label htmlFor="lastName">Last Name: </label>
<input
type="text"
className="form-control"
id="lastName"
name="lastName"
value={basicDetails.lastName}
onChange={(event) => handleInputChange(event)}
/>
</div>
<br />
{basicDetails.errors.lastName && (
<ErrorText>{basicDetails.errors.lastName}</ErrorText>
)}
<br />
</>
);
};
Lastly, check the field values and errors to set the disabled attribute on the next button in index.js. The first !(value.basicDetails.firstName && value.basicDetails.lastName) condition handles the initial/empty values state while the second condition handles the error values.
{currentPage === 1 && (
<>
<BasicDetails />
<button
disabled={
!(
value.basicDetails.firstName && value.basicDetails.lastName
) ||
Object.values(value.basicDetails.errors).filter(Boolean).length
}
onClick={next}
>
Next
</button>
</>
)}
This pattern can be repeated for the following steps.

First you must be getting converting controlled component to uncontrolled component error in your console. For controlled component it is always preferred to use state to set value for the input. And with onChange handler you set the state. I will try to put into a single component so you would get the idea and apply your case
import React, {useState} from 'react';
import {isLettersOnly} from './components/utils'; // not sure where it is in your folder structure
const MyInputComponent = ({value, ...props}) => {
const [inputValue, setInputValue] = useState(value || ''); // input value should be empty string or undefined. null will not be accepted.
const [error, setError] = useState(null);
const handleChange = event => {
const { name, value } = event.target;
if (!isLettersOnly(value)) {
setError('Invalid Input');
}
setInputValue(value);
}
return (
<>
<input
value={inputValue}
onChange={handleChange}
{...props}
/>
{error && (
<span className={"error"}>{error}</span>
)}
</>
)
}
export default MyInputComponent;
This is a very rudimentary component. just to show the concept. You can then import this component as your input field and pass necessary props like name, className etc from parent.
import React from 'react';
import MyInputComponent from 'components/MyInputComponent';
const MyForm = (props) => {
return props.data && props.data.map(data=> (
<MyInputComponent
name="lastName"
className="form-control"
value={data.lastName}
));
}

Related

How to get inputs data from multiple text box in for loop in React js and pass it to Api

I am trying to build a quiz application where I want to generate no of Question input fields based on admin inputs.
So suppose the admin enters 10 questions for the quiz.
Then I am rendering the form inside for loop for 10 Questions and their answers respectively.
The problem I am facing is I am not able to get all values from input fields.
Below is my demo code:
import { useState } from "react";
const MyComponent = () => {
const [inputs, setInputs] = useState({});
const handleChange = (e) =>
setInputs((prevState) => ({
...prevState,
[e.target.name]: e.target.value
}));
const finalData = (e) => {
e.preventDefault();
console.log("data", inputs);
};
function buildRows() {
const arr = [];
for (let i = 1; i <= 3; i++) {
arr.push(
<div key={i} id={i}>
<input name="Question" onChange={handleChange} />
<input name="option1" onChange={handleChange} />
<input name="option2" onChange={handleChange} />
<input name="option3" onChange={handleChange} />
<input name="option4" onChange={handleChange} />
</div>
);
}
return arr;
}
return (
<>
{buildRows()}
<button
onClick={(e) => finalData(e)}
variant="contained"
className="button-left"
sx={{ marginRight: 3.5 }}
>
Submit Quiz Questions
</button>
</>
);
};
export default MyComponent;
You could use the id (or any other unique property, a unique name would probably be preferred) you're giving your div and build your object with that as an array index like so:
const handleChange = (e) => {
const parent = e.currentTarget.parentNode;
const id = parent.id;
setInputs((prevState) => ({
...prevState,
[id]: {
...prevState[id],
[e.target.name]: e.target.value
}
}));
};
This produces an object like this:
{
"1":{
"Question":"1",
"option1":"2",
"option2":"3",
"option3":"4",
"option4":"5"
},
"2":{
"Question":"6",
"option1":"7",
"option2":"8",
"option3":"9",
"option4":"11"
},
"3":{
"Question":"22",
"option1":"33",
"option2":"44",
"option3":"55",
"option4":"66"
}
}

React wipes the value of an input field

Full example on CodeSandbox
(Css is a bit borked)
Writing anything into the input field or the textarea and then clicking on the select wipes the input field & the textarea, I am not sure why -
It seems that is because I am passing jsx elements to the HoverWrapper element.
When I just inlined the WrapInHover element it behaved as expected. Am I passing Elements in a bad way ?
Adding a key to the passed elements didn't seem to solve the issue ...
const Form = () => {
const selectInit = {
open: false,
initial: true,
selected: 'please select',
};
const selectReducer = (state, action) => {
switch (action.type) {
case 'toggle': {
return { ...state, open: !state.open };
}
case 'select': {
return { ...state, selected: action.selected, open: !state.open, initial: false };
}
}
};
const [selectState, selectDispatch] = useReducer(selectReducer, selectInit);
const selectHelp = selected => selectDispatch({ type: 'select', selected });
const OptionComp = ({ txt, value, onClick }) => (
<Option onClick={onClick} state={selectState} value={value}>
{selectState.open && selectState.selected === value ? null : <HoverBorder />}
{txt}
</Option>
);
const WrapInHover = ({ elements }) => {
const [hover, setHover] = useState(false);
return (
<div
css={css`
position: relative;
`}
onMouseEnter={() => {
setHover(true);
}}
onMouseLeave={() => {
setHover(false);
}}>
{elements}
<HoverBorder hover={hover} />
</div>
);
};
return (
<FormEl>
<WrapInHover elements={<Input key='ContactEmailInput' type='email' required />} />
<Label htmlFor='subject' onClick={() => selectDispatch({ type: 'toggle' })}>
Subject
</Label>
<Select>
<OptionComp
onClick={() => selectHelp('art')}
txt='I want you to paint something !'
value='art'
/>
{selectState.initial && !selectState.open ? (
<OptionComp
txt='Please Select An Option'
value='please select'
onClick={() => selectDispatch({ type: 'toggle' })}
/>
) : null}
</Select>
</FormEl>
);
};
Store value of input and message inside state. Also input will lose focus if your WrapInHover is inside main function
export default function App() {
const Form = () => {
const [formState, setFormState] = useState({ email: "", message: "" });
...
const handleFormDataChange = (e, type) => {
const {target: { value }} = e;
setFormState((prevState) => ({ ...prevState, [type]: value }));
};
return (
<FormEl>
<FormTitle>Contact me</FormTitle>
<Label htmlFor="email">Email</Label>
<WrapInHover
elements={
<Input
key="ContactEmailInput"
type="email"
value={formState.email}
onChange={(e) => handleFormDataChange(e, "email")}
required
/>
}
/>
...
<Label htmlFor="message">Message</Label>
<WrapInHover
elements={
<TextArea
key="ContactMessageTextArea"
name="message"
value={formState.message}
onChange={(e) => handleFormDataChange(e, "message")}
/>
}
/>
CSB Example - I will delete after 24 hours.

React Hooks: handle multiple inputs

on react docs forms section there is the following example using class components:
class Reservation extends React.Component {
constructor(props) {
super(props);
this.state = {
isGoing: true,
numberOfGuests: 2
};
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;
this.setState({
[name]: value
});
}
render() {
return (
<form>
<label>
Is going:
<input
name="isGoing"
type="checkbox"
checked={this.state.isGoing}
onChange={this.handleInputChange} />
</label>
<br />
<label>
Number of guests:
<input
name="numberOfGuests"
type="number"
value={this.state.numberOfGuests}
onChange={this.handleInputChange} />
</label>
</form>
);
}
}
Considering Hooks can only be called either in a React function component or a custom React Hook function is there a way of doing it using hooks instead?
you can clean up #adam 's final solution a bit by not using the useCallback hook, and instead simply using the useState hook as a controlled component.
const MyComponent = () => {
const [inputs, setInputs] = useState({});
const handleChange = e => setInputs(prevState => ({ ...prevState, [e.target.name]: e.target.value }));
return (
<>
<input name="field1" value={inputs.field1 || ''} onChange={handleChange} />
<input name="field2" value={inputs.field2 || ''} onChange={handleChange} />
</>
)
}
example
const MyComponent = () => {
const [inputs,setInputs] = useState({});
return (
<>
<input key="field1" name="field1" onChange={({target}) => setInputs(state => ({...state,field1:target.value}))} value={inputs.field1}/>
<input key="field2" name="field2" onChange={({target}) => setInputs(state => ({...state,field2:target.value}))} value={inputs.field2}/>
</>
)
}
you can pass in initial values like this:
const MyComponent = (initialValues = {}) => {
const [inputs,setInputs] = useState(initialValues);
...
}
EDIT: A nice short onChange according to #hamidreza's comment
const MyComponent = (initialValues = {}) => {
const [inputs,setInputs] = useState(initialValues);
const onChangeHandler = useCallback(
({target:{name,value}}) => setInputs(state => ({ ...state, [name]:value }), [])
);
return (
<>
<input key="field1" name="field1" onChange={onChangeHandler} value={inputs.field1}/>
<input key="field2" name="field2" onChange={onChangeHandler} value={inputs.field2}/>
</>
)
}
etc, etc, etc
Maybe, on the last example onChangeForField('...') will be triggered on each render, so maybe you have to write onChange={()=>onChangeForField('...')} or if you want the event to get passed onChange={(e)=>onChangeForField('...', e)}
I was looking for the same answer,but i was finding difficulty to understand the previous solutions,so i tried in my own way ,and i found a solution.
const [inputs,setInputs] = useState({
'field1':'',
'field2':'',
});
const handleChange = (e) => {
const name = e.target.name; //it is the name of that input
const value = e.target.value; //value of that input
setInputs((prev) => {
prev[name] = value;//changing the updated value to the previous state
return prev;
});
};
return (
<>
<input key="field1" name="field1" onChange={handleChange} value={inputs.field1}/>
<input key="field2" name="field2" onChange={handleChange} value={inputs.field2}/>
</>
adding to Adam's answer and for those who are looking towards typescript solution,
interface MyIType {
field1: string;
...
}
//Partial from typescript to make properties optional
interface MyFormType extends Partial<MyIType> {}
const [inputs,setInputs] = useState<MyFormType>(initialValues);
const onChangeForField = useCallback(({target}) =>
setInputs(_state => {
return {
..._state,
[target.name]: target.value,
};
}),
[]
);
If you were like me, having multiple inputs on multiple pages using the same input id/name/key, try value={data.xxx || ''} .
Full code:
const [data, setData] = useState<any>({});
const handleValueChanges = e => {
setData({
...data,
[e.target.name]: e.target.value,
});
};
<InputText (using prime react)
id="firstName"
name="firstName"
value={data.firstName || ''}
onChange={handleUpdate}
/>
As of v6 you can use .forEach(), Please refer to the migrate guide
[{name: "firstName", value: "Safwat" }, {name: "lastName", value: "Fathi", }].forEach(({name, value}) => setValue(name, value));

Redux-form's Field component is not triggering normalize function if Field's type is number

I'm passing antd's component (FormFieldInput) to redux-form's Field component. Everything works well with "text" and "telephone" input types. Normalize number function stops working as soon as I change Field's type to "number".
I can see that FormFieldInput component is triggered only when I input numbers. When I'm typing alphabetic characters into the input FormFieldInput console log at the top of the function is not returning new values.
Normalizer:
const normalizeNumber = (value /* , previousValue */) => {
console.log('---input', value);
if (!value) {
return value;
}
const onlyNums = value.replace(/[^\d]/g, '');
console.log('---output', onlyNums);
return onlyNums;
};
Usage:
<Field
name="size"
type="number"
component={FormFieldInput}
label="Size"
placeholder="Size"
required
validate={[required, maxLength255]}
normalize={normalizeNumber}
/>
FormFieldInput:
const FormFieldInput = ({
input,
label,
type,
placeholder,
required,
meta: { touched, error, warning }
}) => {
const [hasError, setHasError] = useState(false);
const [hasWarning, setHasWarning] = useState(false);
console.log('---input', input);
useEffect(() => {
setHasError(!!error);
}, [error]);
useEffect(() => {
setHasWarning(!!warning);
}, [warning]);
const ref = createRef();
return (
<div className="form-item">
<div className="form-item__label">
{`${label}:`}
{required && <span style={{ color: 'red' }}> *</span>}
</div>
<div className={`form-item__input`}>
<AntInput
{...input}
ref={ref}
placeholder={placeholder}
type={type}
/>
</div>
</div>
);
};
export const AntInput = React.forwardRef((props, ref) => {
const { placeholder, type, suffix } = props;
console.log('---type', type);
return <Input {...props} ref={ref} placeholder={placeholder} type={type} suffix={suffix} />;
});
I expect all input data to go through normalize function, but somehow alphabetic characters are going through it.

Redux-form validations not getting caught

im fairly new to redux.
What im trying to validate is, two key fields cannot hold the same value and all fields are required. For the required part, i am using Field-Level Validations and that seems to work fine. For deciding whether an element already exists, i am using Sync Validation.
When the sync validation works, i checked it with console log. It catching the error and adding it to the errors object. But my form is not showing that.. is it not binded? What am i missing here?
I have added 'onFocus' and 'onBlur' mouse events to the text-fields, to make then readonly onBlur. They seem to be working fine. But the moment i added that, my {touched && error && <span>{error}</span>} error stops getting displayed. What am i doing wrong here?
my form
const required = value => (value ? "" : "required")
class CreateObject extends React.Component {
enableTextField = (e) => {
document.getElementById(e.target.id).removeAttribute("readonly");
}
disableTextField = (e) => {
document.getElementById(e.target.id).setAttribute("readonly", true);
}
renderField = ({ input, label, type, id, meta: { touched, error } }) => (
<React.Fragment>
{touched && error && <span>{error}</span>}
<FormControl {...input} type={type} placeholder={label} id={id}
className={`align-inline object-field-length ${error ? 'error' : ''}`}
onFocus={this.enableTextField.bind(this)}
onBlur={this.disableTextField.bind(this)}
/>
</React.Fragment>
);
renderObjects = ({ fields, meta: { touched, error, submitFailed, errors } }) => {
return (
<ul>
<li>
<center>
<Button bsStyle="success" onClick={() => fields.push({})}>Add New Object</Button>
</center>
</li>
{fields.map((object, index) => (
<li key={index}>
<br />
<center>
<Field
name={`${object}.key`}
type='text'
component={this.renderField}
validate={required}
label="Key"
id={`${object}.key`}
/>
<div className="divider" />
<Field
name={`${object}.method`}
type='text'
component={this.renderField}
label="Method"
validate={required}
id={`${object}.key` + `${object}.method`}
/>
<div className="divider" />
<Field
name={`${object}.value`}
type='text'
component={this.renderField}
label="Value"
validate={required}
id={`${object}.key` + `${object}.value`}
/>
<div className="divider" />
<span
className="align-inline"
onClick={() => fields.remove(index)}
className="allIcons mdi mdi-delete-forever"
/>
</center>
</li>
)
)}
</ul>
);
}
submit() {
//this
}
render() {
const { handleSubmit, pristine, reset, submitting, invalid } = this.props;
console.log(this.props);
return (
<form onSubmit={handleSubmit(this.submit.bind(this))}>
<FieldArray name='objects' component={this.renderObjects} />
<center>
<Button className="align-inline" type="submit" disabled={pristine || submitting || invalid}>Submit</Button>
<div className="divider" />
<Button className="align-inline" disabled={pristine || submitting} onClick={reset}> Clear All Values </Button>
</center>
</form>
);
}
}
export default reduxForm({
form: 'ObjectRepo',
validate
})(CreateObject);
validate.js
const validate = values => {
const error = {}
if (!values.objects || !values.objects.length) {
error.objects = { _error: 'At least one object must be entered' }
} else {
const objectArrayErrors = []
values.objects.forEach((object, objectIndex) => {
const objectErrors = { _error: 'Object Key should be unique' }
if (values.objects.filter(item => item.key == object.key).length == 2) {
objectArrayErrors[objectIndex] = objectErrors
}
})
if (objectArrayErrors.length) {
error.objects = objectArrayErrors
}
}
console.log(error)
return error
}
export default validate
Thanks a lot in advance!
You might want to look at the code below which works for me. This is your container (or smart component if you wish)
export const validateProps = {
name: [required],
value: [required, intOrFloat, maxPercent],
someId: [required],
}
export const transformer = new TypesModel({
name: String,
value: Number,
someId: Number,
})
export default createFormContainer(
formName,
'your_form',
transformer,
validateProps,
mapStateToProps,
mapDispatchToProps,
null,
false,
null,
onSuccessSubmit)(YourFormContainer)
And the code for types model is
class TypesModel {
constructor(schema) {
this.schema = schema
}
transform(data) {
const keys = Object.keys(this.schema)
const result = {}
for (let index = 0, len = keys.length; index < len; index += 1) {
const keyName = keys[index]
try {
result[keyName] = this.schema[keyName](data[keyName])
} catch (e) {
throw new Error(`Type conversion for field "${keyName}" failed`)
}
}
return result
}
}
export default TypesModel
You want validators to look like:
export const required = value => isEmpty(value) &&
'Required field'
export const intOrFloat = value => (!isInt(`${value}`) && !isFloat(`${value}`)) &&
'Must be an integer of float'

Categories

Resources