I am using formsy-react to handle validation of my input fields. Problem is that I can't update states. I followed example on formsy github page and validation is working but problem is that states are incorrect. They are always one step (or few) behind and I am not sure why...
I used callback on setState function to implement some custom logic on validation and that part doesn't work properly.
I have a situation where user enters email. After user enters email I check if email is already registrated. If user is already in system, I create new input component (password type) and if not I create new "input type email" component.
Since all forms elements are required I added one more validation check that checks if new password or email component is added and if there is any data.
To update states I used Forms form API call onChange() and this part is not working for unknown reason.
Does someone knows where is the problem?
This is code I am using:
Component Input (shorted version)
changeValue(event) {
this.setValue(event.currentTarget.value);
},
render() {
// Set a specific className based on the validation
// state of this component. showRequired() is true
// when the value is empty and the required prop is
// passed to the input. showError() is true when the
// value typed is invalid
const className = (this.props.className || "col-md-4" );
const classValidationName =this.isValid() ? 'valid' : this.showError() ? ' invalid' : null;
// An error message is returned ONLY if the component is invalid
// or the server has returned an error message
const errorMessage = this.getErrorMessage();
return (
<div className= {className}>
<div className="md-form">
<span className="prefix"><i className={this.props.icon}></i></span>
<input
className={classValidationName}
name={this.props.name}
id={this.props.id}
type={this.props.inputType}
value={this.getValue() || ""}
onChange={this.changeValue}
onBlur={this.props.controlFuncOnBlur}
placeholder={this.props.placeholder}
required={this.props.required}
pattern={this.props.pattern}
/>
<label id={this.props.name + 'Label'} htmlFor={this.props.name} data-error={errorMessage}
data-success={this.props.successMessage}>{this.props.title}
</label>
</div>
</div>
);
}
Container (shorted version)
handleEmailBlur(event) {
const self = this;
if (this.refs.email.isValid) {
axios.get('/api/checkIsUserRegistrated', {
params: {
email: this.state.email
}
})
.then(function (response) {
if (self.state.userExist !== response.data[0].userExist) {
self.setState({
userExist: response.data[0].userExist,
confirmEmail: "",
password: ""
});
self.forceUpdate();
}
})
.catch(function (error) {
console.log(error);
});
}
}
enableButton = () => {
this.setState({
formValid: true
});
}
disableButton = () => {
this.setState({
formValid: false
});
}
saveCurrentValuesToStates = (getCurrentValues, isChanged) => {
console.log(this);
this.setState(getCurrentValues, ()=> {
if (this.state.formValid && (this.state.password || this.state.confirmEmail)){
this.setState({
canSubmitForm: true
});
}
else{
this.setState({
canSubmitForm: false
});
}
});
}
<Formsy.Form className="booker-form" ref="form"
onChange={this.saveCurrentValuesToStates} onValid={this.enableButton} onInvalid={this.disableButton}>
<SingleInput
inputType={'email'}
icon={'icon-Email'}
id={'email'}
name={'email'}
title={'E-mail'}
ref="email"
controlFuncOnBlur={this.handleEmailBlur}
content={this.state.email}
errorMessage={'Incorect E-Mail address'}
required
validations="isEmail"
validationError="This is not a valid email"
/>
{(this.state.userExist === '0') ?
<SingleInput
inputType={'email'}
icon={'icon-Email'}
id={'confirmEmail'}
name={'confirmEmail'}
title={'Confirm your E-mail'}
content={this.state.confirmEmail}
required
validations="equalsField:email"
validationError="Emails don't match"
/>
: null}
{(this.state.userExist === '1') ?
<SingleInput
inputType={'password'}
icon={'icon-Padlock'}
id={'password'}
name={'password'}
title={'Enter your password'}
content={this.state.password}
required
/>
: null}
Related
I have created a react app to take user inputs.
class VehiReg extends Component {
constructor(props){
super(props);
this.state={
vehicle:"",
plateNo:"",
owner:"",
manufacturer:"",
manufacturedYear:"",
color:""
}
}
takInput = (e) => {
const {name,value}= e.target;
this.setState({
...this.state,
[name]:value
})
}
register = (e) =>{
e.preventDefault();
const {plateNo,owner,manufacturer,manufacturedYear,color,vehicle} = this.state;
const data ={
plateNo:plateNo,
owner:owner,
manufacturer:manufacturer,
manufacturedYear:manufacturedYear,
color:color,
vehicle:vehicle
}
console.log(data);
axios.post("http://localhost:8080/registrations/new",data).then((res)=>{
if(res.data.success){
this.setState({
vehicle:"",
plateNo:"",
owner:"",
manufacturer:"",
manufacturedYear:"",
color:""
})
}
})
}
<div className="box">
<input placeholder="Enter the licence plate number "
className="input2" name="plateNo" value={this.state.plateNo} onChange={this.takInput} />
</div>
then I want to validate the input which the user enters.
The vehicle license plate can be in many forms:
· Vintage: 13 ශ්රී 9999
· Old: 250-9999, 19-9999
· Modern: WP GA-9999, CAR-9999
then according to the type which the user enters I want to write a function in the backend to validate and categorize the user inputs.
Then again I want to return the vehicle type to the frontend
the main thing that I want to know is how can I validate the user input in the backend.
here is the code I have written for entering the data into the database, before entering the data into the database that validation function should be executed.
//new registration
router.post("/registrations/new",(req, res)=>{
let newRegistration = new Registrations(req.body);
newRegistration.save((err)=>{
if(err){
return res.status(400).json({
error:err
})
}
return res.status(200).json({
success:"registration Ok"
});
});
});
There are different types of doing validations and I will share one which I have used hope this can help you.
In Frontend:
add properties to state:
this.state = {
plateNo: '',
plateNoError: ''},
plateNoValid: false,
}
takInput = (e) => {
const {plateNo,value}= e.target;
const plateNoRegEx =
`^[A-Z]{2}[ -][0-9]{1,2}(?: [A-Z])?(?: [A-Z]*)? [0-9]{4}$`
if(!value){
this.setState({
...this.state,
[plateNoError]:"Plate number cannot be empty"
})
}else if(!plateNo.match(plateNoRegEx)){
this.setState({
...this.state,
[plateNoError]:"Invalid Plate Number"
})
}else{
this.setState({
...this.state,
[plateNo]: value,
[plateNoError]:''
})
}
}
<div className="box">
<input placeholder="Enter the licence plate number "
className="input2" name="plateNo" value=
{this.state.plateNo} onChange={this.takInput} />
{this.state.plateNoError &&
<p>{this.state.plateNoError}</p>
}
</div>
For backend
you can use
1)Express validator - npm install --save express-validator
2)Joi - https://joi.dev/ - npm i joi
both are good for validating requests and you can follow the (route - controller -service) format, where you can write validations in route.
eg: router
.route('/registrations/new')
.post(validate(publicValidation.getUserByEmail),
publicController.getUserByEmail)
const getUserByEmail = {
body: Joi.object().keys({
email: Joi.string().required(),
.....
})
};
So I'm using react-bootstrap-typeahead and it's working completely fine when I'm typing something in the search box. It gives me the relevant options when I type something in the search box like this:
However, when I select one of options and then try to re-change the text it throws an error. This is how it looks like when I select 1 option.
And this is the error it throws: TypeError: 'Cannot read property 'match' of undefined'
Here is the state of the Search component which has the Typeahead:
class Search extends Component {
state = {
hcpName: [],
hcps: [],
searchName: '',
isLoading: false,
hcp_id: 101,
searchSelectedOption: ''
}
And here is the Typeahead I'm using:
<div className='col-md-3'>
<div class="form-group">
<Typeahead
id="basic-example"
options={this.state.hcpName}
placeholder="Search by Name..."
emptyLabel={this.state.isLoading ?
<>
<span>Searching...
<Loader
style={{ paddingLeft: '5px', display: 'inline' }}
type="Circles"
color="#0caf8d"
height={15}
width={20}
radius={30}
/>
</span>
</>
: ''}
isLoading={this.state.isLoading}
onInputChange={(searchName) => this.setState({ searchName }, () => {
{
let nameValue = this.state.searchName;
this.setState({ isLoading: true })
axios.post('/get-hcp-data', {
nameValue: nameValue
})
.then((res) => {
const hcps = res.data;
this.setState({ hcps: hcps, hcpName: Object.values(hcps.hcp_details_concat) })
this.setState({ isLoading: false })
}, (error) => {
console.log(error);
});
}
})}
onChange={(selectedOption) => {
console.log('selected option: ', selectedOption[0]);
console.log('npi id selected', selectedOption[0].replace(/(^.*\[|\].*$)/g, ''));
console.log('parsed npi id selected', parseInt(selectedOption[0].replace(/(^.*\[|\].*$)/g, '')[0]));
this.setState({hcp_id: parseInt(selectedOption[0].match(/[^[\]]+(?=])/g)[0])});
}}
/>
</div>
</div>
Inside 'onInputChange' inside Typeahead, I'm basically making an api call after every keystroke that a user enters. So that's why you can see an axios request over there. And inside 'onChange', I extract the number inside the square brackets of the user selection.
As I mentioned, I face an error when I try to change the text of the already selected option. For example, suppose I clicked on [101]Anna, I see that text in the search bar. And when I try to modify it again, I immediately see an error. What's the possible reason for this?
Here is the console log for onInput change:
I solved the problem by identifying that the match/replace function can't be used inside the onChange of Typeahead so I instead directly used it while fetching the api data. For that, I first set the state according to what the user has selected like this:
onChange={(selectedOption) => this.setState({ searchName: selectedOption }, () => {
console.log('selected option: ', selectedOption);
})}
And then while fetching the data, I made use of the searchName state to run the replace function.
dataFetch = () => {
this.setState({ receivedData: [], loading: true });
let page_id = this.state.page_id;
let hcp_id = parseInt(this.state.searchName[0].replace(/(^.*\[|\].*$)/g, ''));
axios.post('/test-json', {
page_id: page_id,
hcp_id: hcp_id
})
.then((res) => {
this.setState({ receivedData: res.data, loading: false });
console.log('State after loading data: ', this.state);
}, (error) => {
this.setState({ error: true });
console.log(error);
});
}
I have a simple setup for validating a few form entries. It only pushes validation for the first entry. So in state I have:
this.state = {
username: '',
email: '',
zip: '',
errors:[]
}
The submit button:
<div>
<button onClick={this.handleSubmit}
>submit
</button>
</div>
The submit button and the form both trigger handleSubmit:
handleSubmit(e) {
e.preventDefault();
this.setState({errors:[]});
const {username, email, zip} = this.state;
const errors = validate(username, email, zip);
if (errors.length>0) {
this.setState({errors});
}
}
which hits validate:
function validate(username, email, zip) {
let validEmail = RegExp(/[^# \t\r\n]+#[^# \t\r\n]+\.[^# \t\r\n]+/);
let validZip = RegExp(/^\d{5}$/);
const errors = [];
if (username.length < 3 || null) {
errors.push('Name must be at least 3 characters long.');
}
if (validEmail.test(email)) {
errors.push('Email address must be valid email address.');
}
if (validZip.test(zip)) {
errors.push('Zip code must be 5 digits.')
}
console.log(errors);
return errors;
}
and then, from the updated state, errors are supposed to print as line items in:
<ul className="errorList">
{this.state.errors.map(
(error =>
<li key={error}>
{error}
</li>
)
)}
</ul>
but only the first validation is working (for username), in a console log and in the unordered list.
Looking for advice ^~^
validEmail.test(email) return false, try !validEmail.test(email) will return true if email failed
When an input field requires more info, the browser shows a message in a bubble about why the input is invalid. I would like to prevent the default for this in vue but I'm not sure how. Below is how I would do it in JavaScript but in Vue, there may be a way to do #invalid like how I know you can do #submit on a form as an eventListener. I'm also wondering if extra prevention is needed to prevent this in ios or android.
HTML
<form>
<input type="text" required>
<input type="submit">
</form>
JS
document.querySelector( "input" ).addEventListener( "invalid",
function( event ) {
event.preventDefault();
});
https://codepen.io/albert-anthony4962/pen/BajORVZ
If you want to completely disable validation, you can add novalidate="true" to your form element.
I suspect that you might only want to do that on the initial page load. If so, could you update your section and hopefully and add an example? I can update my answer after that 😀
A pattern I have (idea from Vuetify) is pretty easy:
new Vue({
el: "#app",
data: {
isFormValid: null,
form: {
input_1: {
text: null,
rules: ['required', 'min3'],
validateText: null
},
input_2: {
text: null,
rules: ['required'],
validateText: null
}
},
rules: {
required: v => !!v && !![...v].length || 'This field is required.',
min3: v => !!v && !!([...v].length > 2) || 'This field must be at least 3 characters long.'
}
},
methods: {
validateForm() {
const validated = []
for (let key in this.form) {
const v = this.form[key].rules.map(e => {
return this.rules[e](this.form[key].text)
})
if (v.some(e => e !== true)) {
this.form[key].validateText = v.filter(e => e !== true)[0]
validated.push(false)
} else {
this.form[key].validateText = "This field is valid."
validated.push(true)
}
}
return validated.every(e => e === true)
},
submitForm() {
if (this.validateForm()) {
// submit logic
this.isFormValid = "Yes, it's valid."
} else {
// not valid logic:
this.isFormValid = "No, it's not valid."
}
},
resetValidation() {
const form = JSON.parse(JSON.stringify(this.form))
for (let key in form) {
form[key].validateText = null
}
this.isFormValid = null
this.form = form
},
resetForm() {
const form = JSON.parse(JSON.stringify(this.form))
for (let key in form) {
form[key].validateText = null
form[key].text = null
}
this.isFormValid = null
this.form = form
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form ref="formRef">
<label for="input_1">
Input 1:
<input
id="input_1"
type="text"
v-model="form.input_1.text"
/>
</label>
<div>This field will validate if NOT EMPTY AND HAS AT LEAST 3 CHARS</div>
<div>{{ form.input_1.validateText || ' ' }}</div>
<br />
<label for="input_2">
Input 2:
<input
id="input_2"
type="text"
v-model="form.input_2.text"
/>
</label>
<div>This field will validate if NOT EMPTY</div>
<div>{{ form.input_2.validateText || ' ' }}</div>
<br />
<button type="submit" #click.prevent="submitForm">
SUBMIT
</button>
<div>Is the form valid? {{ isFormValid }}</div>
</form>
<button #click="resetValidation">RESET VALIDATION</button><br />
<button #click="resetForm">RESET FORM</button>
</div>
This way you don't have to put up with the HTML5 "bubbles", but can still validate your form - in any way you need. You can compose any validation scheme you want by using functions that go over your input text. You could even come up with regexp validation, pattern validation (like phone numbers), etc. It's not the greatest solution, but quite "pluggable".
This is also supposed to be cross-platform (if you use Vue).
I have created a settings page where users can update their email addresses. Everything worked fine but suddenly the validation is not updating anymore. Only the first change of the input field triggers validateState().
Any further changes will not trigger this function so the status of that field stays as it is.
I have compared the code with other components that use the same code and they still work fine.
I am using bootstrap-vue components for the form.
<template>
<div class="row">
<div class="col-md-12">
<b-form #submit="onSubmit">
<b-form-group :label="$t('general.email')"
label-for="settingsEmail"
:invalid-feedback="errors.first('email')">
<b-form-input id="settingsEmail"
type="text"
v-model="form.email"
:disabled="saving"
name="email"
:state="validateState('email')"
v-validate="{required: true, email: true}">
</b-form-input>
</b-form-group>
<b-button type="submit" variant="primary" :disabled="saving || !hasChanged() || errors.any()"><i class="fa fa-refresh fa-spin fa-fw" v-if="saving"></i> {{$t('general.save')}}</b-button>
</b-form>
</div>
</div>
</template>
<script>
import {UPDATE_USER} from '../config/actions'
export default {
name: 'settingsAccount',
data() {
return {
form: {},
saving: false
}
},
computed: {
user: function() {
return this.$store.getters.getUser;
}
},
created() {
this.init();
},
methods: {
init() {
this.form.email = this.user.email;
},
hasChanged() {
if(this.form.email !== this.user.email) {
return true;
}
return false;
},
onSubmit(event) {
event.preventDefault();
this.saving = true;
this.$validator.validateAll().then((result) => {
if (result) {
let data = {};
if(this.form.email !== this.user.email) {
data.email = this.form.email;
}
this.$store.dispatch(UPDATE_USER, data).then(() => {
this.saving = false;
this.$validator.reset();
}).catch(() => {
this.saving = false;
});
} else {
this.saving = false;
}
});
},
validateState(ref) {
if (this.veeFields[ref] && (this.veeFields[ref].dirty || this.veeFields[ref].validated)) {
return !this.errors.has(ref)
}
return null
},
}
}
</script>
The problem you're having is that the form data element is an empty object, so it will only trigger reactivity when the whole object changes. Either you need to change your data to be this:
data() {
return {
form: {email:''},
saving: false
}
},
Or in your init function, explicitly add the email property as reactive:
methods: {
init() {
this.$set(form,'email',this.user.email)
},
//...
If you're not clear on why, you can read the details here: https://v2.vuejs.org/v2/guide/reactivity.html
A working example (minus vuex) here: https://codesandbox.io/s/x4kp93w3o
PS, when writing questions about vue, it's very helpful to boil it down to a simpler example. Get rid of vuex, remove your translation stuff. Sometimes the answer will jump out at you once you have it as simple as possible.