Angular2 interdependent form field validation - forms

I have two form fields, where if the first field is filled in, the second field is mandatory. If I try to do this in Angular2, using a custom validator, the validator is only fired on initialization and when the specific field is changed.
Case:
- User fills in field 1
- Field 2 should become required, but isn't till the user actually changes field 2 (firing the custom validation).
private createForm():void {
this.testForm = this._formBuilder.group({
'field1': [],
'field2': ['', this.validateRequired()]
});
}
private validateRequired(){
console.log("something", this);
let component = this;
return (control: Control): { [s: string]: boolean } => {
return component.testModel.field1 && !control.value {"required":true} : null;
}
}
See this plunkr: http://plnkr.co/edit/PEY2QIegkqo8BW1UkQS5?p=preview
Edit:
For now I subscribed to field1's valueChange observable and when changed execute a manual check on field2, like:
this.testForm.controls['field1'].valueChanges.subscribe(
value => {
component.testForm.controls['field2].updateValueAndValidity();
}
)
But I feel like there must be a better way to do this.

You could use a global validator for the group like this:
private createForm():void {
this.testForm = this._formBuilder.group({
'field1': [],
'field2': ['', this.validateRequired()]
}, {
validator: this.someGlobalValidator // <-----
});
}
someGlobalValidator(group: ControlGroup) { // <-----
var valid = false;
for (name in group.controls) {
var val = group.controls[name].value
(...)
}
if (valid) {
return null;
}
return {
someValidationError: true
};
}

I want to expand on Thierry's answer a bit in order to address Arne's comment. In order to handle the validation of multiple fields and possibly multiple validations in your formgroup level validator the solution is to return a function from your validator that then returns an object that indicates the error type. Here is a example of a field matching validator that I added some extra errors to in order to illustrate the point. Note that it returns an object with possibly several properties where each object property is any string and the value is a boolean.
export function FieldMatchingValidator(field1: string, field2 :string) {
return (cg: FormGroup): { [s: string]: boolean } => {
let retVal = null;
let f1 = cg.controls[field1];
let f2 = cg.controls[field2];
retVal = f1.value === f2.value ? null : { fieldMismatch: true };
if(somecondition){
retVal['someerror'] = true;
}
if(someothercondition){
retVal['someothererror'] = true;
}
return retVal;
}
}
When this validator runs, if an error condition is encountered, then the form's errors property will be populated with the returned object with one or more properties indicating different errors. Then all you have to do it put the appropriate angular property setting on the controls that have the validation errors.
<div *ngIf="myForm.hasError('fieldMismatch')">
Field Mismatch
</div>
<div *ngIf="myForm.hasError('someerror')">
Some Error
</div>
<div [class.Errors]="myForm.hasError('someothererror')">
Some Other Error
</div>

Related

Angular form validation: compare two fields

In an Angular 4 application, how can I validate two fields of a form doing a comparison?
For example, let's suppose that my form has a startDate and an endDate date fields and I want to make sure that the endDate must be bigger than the startDate.
When you want to implement validations containing one or more sibling (form)controls, you have to define the validator function on a level up/above that of the sibling controls. For ex:
ngOnInit() {
this.form = this.formbuilder.group({
'startDate': ['', [<control-specific - validations >]],
'endDate': ['', [<control-specific - validations >]]
}, { validator: checkIfEndDateAfterStartDate });
}
Then outside the component class's definition (in the same file), define the function checkIfEndDateAfterStartDate as well.
export function checkIfEndDateAfterStartDate (c: AbstractControl) {
//safety check
if (!c.get('startDate').value || !c.get('endDate').value) { return null }
// carry out the actual date checks here for is-endDate-after-startDate
// if valid, return null,
// if invalid, return an error object (any arbitrary name), like, return { invalidEndDate: true }
// make sure it always returns a 'null' for valid or non-relevant cases, and a 'non-null' object for when an error should be raised on the formGroup
}
This validation will make the FormGroup invalid by adding the error-flag (here invalidEndDate) to true to the errors object for that FormGroup. If you want to have specific errors to be set on any of the sibling controls instead, then you can manually set the error flags on that formControl by using something like, c.get('endDate').setErrors({ invalidEndDate: true }). If you do this, then make sure you clear them for a valid case by setting the errors to null like this, c.get('endDate').setErrors(null).
A live demo of a similar validation can be seen here.
try this
export class validationComponent implements OnInit {
private testForm:FormGroup;
constructor(private fb: FormBuilder) {
}
ngOnInit() {
this.testForm = this.fb.group({
'startDate': ['', [Validators.required]],
'endDate': ['', [Validators.required]]
});
this.subscribeDateChanges();
}
subscribeDateChanges() {
const startDateChanges = (<any>this.testForm).controls.startDate.valueChanges;
const endDateChanges = (<any>this.testForm).controls.endDate.valueChanges;
startDateChanges.subscribe(start => {
this.testForm.controls['endDate'].
setValidators(
[Validators.required,
CustomValidators.minDate(this.toYYYYMMDD(start))]);
this.validateDates();
});
endDateChanges.subscribe(end => {
this.validateDates();
});
}
dateError: boolean = false;
validateDates(): void{
let startDate = this.testForm.controls['startDate'].value;
let endDate = this.testForm.controls['endDate'].value;
if(endDate && startDate){
this.dateError = endDate <= startDate;
}
}
toYYYYMMDD(d:Date): string {
d = new Date(d)
var yyyy = d.getFullYear().toString();
var mm = (d.getMonth() + 101).toString().slice(-2);
var dd = (d.getDate() + 100).toString().slice(-2);
return yyyy + '-' + mm + '-' + dd;
}
based on the dateError boolean value you show error msg

ReactJS state is modified with delay?

I'm programatically validating an email and password inputs for simple login, here is the function that call other function that validate the email.
handleLogin(event) {
this.validateEmail();
this.validatePassword();
if (this.state.emailValid === 'error' || this.state.passwordValid === 'error') {
alert('invalid form');
return;
};
const email = ReactDOM.findDOMNode(this.refs.email).value;
const password = ReactDOM.findDOMNode(this.refs.password).value;
const creds = { email: email, password: password }
this.props.onLoginClick(creds)
}
Notice that first than all I'm calling the validateEmail() function which modifies the store that indicates if the input is correct, here's the validateEmail() source code:
validateEmail() {
const email = ReactDOM.findDOMNode(this.refs.email).value;
let validEmail = /^.+([.%+-_]\w+)*#\w+([.-]\w+)*\.\w+([-.]\w+)*$/.test(email);
if (!validEmail) {
this.setState({
emailValid: 'error'
});
return;
}
this.setState({
emailValid: 'success'
});
}
But in the if statement the state.emailValid has not been yet updated, this is a delay in the state modifying, so the alert() is not displayed. How to get the updated state correctly?
Thanks
The thing to note here is that setState is asynchronous. It will not update the state until everything else that is synchronous in your handleLogin method has completed.
With React I like to use state as a single source of truth as often as I can. In the example above you have the html element as a source of truth and state. By changing your components to be controlled by the react state, you can validate your forms on each keystroke.
Forms and Controlled Components
Start by keeping the state of your input in state
class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
emailValid: true,
};
// we bind the function in case we want to
// control text in child component
this.emailChange = this.handleEmailChange.bind(this);
}
emailChange(event) {
this.setState({email: event.target.value});
}
render() {
<textarea value={this.state.email} onChange={this.emailChange} />
}
}
Now whenever you type the state of your html input is handled in react. This will enable you to more easily check its validity. We can do this by adding another method to our class:
class LoginForm extends React.Component {
// ...all the stuff from above
validateEmail() {
let validEmail = /^.+([.%+-_]\w+)*#\w+([.-]\w+)*\.\w+([-.]\w+)*$/.test(email);
if (!validEmail) {
// Object.assign just ensures immutability
this.setState(Object.assign({}, this.state, {
emailValid: false
}))
} else {
// If using babel, this is ensure immutable also
this.setState({
...state,
emailValid: true
})
}
}
// or....
validateEmail() {
let validEmail = /^.+([.%+-_]\w+)*#\w+([.-]\w+)*\.\w+([-.]\w+)*$/.test(email);
this.setState({...state, emailValid: validEmail})
}
// ...render method
}
The validation will now occur on each keystroke. When you need to submit your form all you need to do is check the state if the data is valid and dont need to reference the dom. You can send the data from state.

Angular2: how to add validator and exclude required

I created a custom validator to check email addresses filled by users. Anyway the email field is not required, but if I add my validator it includes required as well.
Here is my form in the constructor of my component class:
this.myForm = fb.group({
'name': [''],
'surname': [''],
'username': ['', Validators.required],
'email': ['', validateEmail]
});
Email field is not required, but if users doesn't fill it, the form doesn't get validated. At the same time if users fill it, I want that the email validation is applied.
Here is my email validator:
export function validateEmail(c: FormControl) {
var EMAIL_REGEXP = /^(|(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6})*$/i;
return EMAIL_REGEXP.test(c.value) ? null : {
validateEmail: {
valid: false
}
};
}
I could edit my custom validator to accept empty strings, but I think this is not the right way to solve my problem.
Do you have a better idea?
I create a custom validator that take a parameter. So it can be use to check valid email and even to check required valid email.
It is still reusable and it can be even not required, according if you call it with true or false.
Here is the code:
import {FormControl} from "#angular/forms";
export const validateEmail = (canBeEmpty:boolean) => {
return (control:FormControl) => {
var EMAIL_REGEXP = /^(|(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6})*$/i;
if (canBeEmpty) {
EMAIL_REGEXP = /^$|^(|(([A-Za-z0-9]+_+)|([A-Za-z0-9]+\-+)|([A-Za-z0-9]+\.+)|([A-Za-z0-9]+\++))*[A-Za-z0-9]+#((\w+\-+)|(\w+\.))*\w{1,63}\.[a-zA-Z]{2,6})*$/i;
}
return EMAIL_REGEXP.test(control.value) ? null : {
validateEmail: {
valid: false
}
};
};
}
Allow an empty string for control value, as follows:
static emailValidator(control: any) {
// RFC 2822 compliant regex
var EMAIL_REGXP = /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*#(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/;
if ( control.value === '' || control.value.match(EMAIL_REGXP) ) {
return null;
} else {
return { 'invalidEmailAddress': true };
}
};
I don't think this could be seen as a side-effect as the purpose of the function is to find invalid strings that should conform to an email format.

Angular 2. Set value of ControlGroup in data driven form

Let's say I have this model:
export class MyModel {
constructor(
public id: number,
public name: string
) {}
}
and this ControlGroup:
export class MyComponent {
form: ControlGroup;
model: MyModel;
constructor(builder: FormBuilder) {
this.form = this.builder({
'id' : [''],
'name' : ['']
})
}
}
To get form's data I can simply do that (if field names match):
this.model = this.form.value;
But how can I set form's value in the same manner?
something like: this.form.value = model;
Getting the following error:
Cannot set property value of #<AbstractControl> which has only a getter
Thank you!
UPD: Based on Günter Zöchbauer's suggestion below I ended up with that helper method:
setFormValues(form: ControlGroup, model: any) {
for(var key in model) {
var ctrl = (<Control>form.controls[key]);
if ( ctrl != undefined )
ctrl.updateValue(model[key]);
}
}
The ControlGroup returned from this.builder.group(...) doesn't support to set the value. To set the value you have to set it on each control individually like:
setValue() {
let value = {id: 'xxx', name: 'yyy'};
Object.keys(value).forEach((k) => {
this.form.controls[k].updateValue(value[k]);
});
}
Plunker example

Assertion Failed: ArrayProxy expects an Array or Ember.ArrayProxy, but you passed object

This is my code
/******************************************************/
import Ember from "ember";
var TodosController = Ember.ArrayController.extend({
actions: {
createTodo: function(){
// Get the todo title by the "New Todo" input
var title = this.get('newTitle');
if(!title.trim()){ return; }
// Create the new Todo model
var todo = this.store.createRecord('todo', {
title: title,
isCompleted: false
});
// Clear the 'New Todo' input field
this.set('newTitle', '');
// Save the new model
todo.save();
},
clearCompleted: function(){
var completed = this.filterBy('isCompleted', true);
completed.invoke('deleteRecord');
completed.invoke('save');
}
},
remaining: function() {
return this.filterBy('isCompleted', false).get('length');
}.property('#each.isCompleted'),
inflection: function() {
var remaining = this.get('remaining');
return remaining === 1 ? 'todo' : 'todos';
}.property('remaining'),
hasCompleted: function(){
return this.get('completed') > 0;
}.property('completed'),
completed: function(){
return this.filterBy('isCompleted', true).get('length');
}.property('#each.isCompleted'),
allAreDone: function(key, value) {
if(value === undefined){
return !!this.get('length') && this.everyProperty('isCompleted', true);
} else {
this.setEach('isCompleted', value);
this.invoke('save');
return value;
}
}.property('#each.isCompleted')
});
export default TodosController;
/*******************************************************/
In terminal not showing any error when i run this command
$ ember server
but in browser not showing any thing and console showing this error
Uncaught Error: Assertion Failed: ArrayProxy expects an Array or
Ember.ArrayProxy, but you passed object
Please suggest me what i m doing wrong, the code is also on github : https://github.com/narayand4/emberjs
thanks in advance.
The most likely reason for this is that you have a controller which extends from Ember.ArrayController while you only return a plain object in the corresponding model.
I had the same issue and changed my controller to extend Ember.Controller instead.
In the related route for this controller, your model method doesn't return an array, as you've indicated by extending an arrayController.