Angular 4 - Using Asynchronous custom validators - javascript

I have the following code:
HTML:
<div [class]="new_workflow_row_class" id="new_workflow_row">
<div class="col-sm-6">
<label class="checkmark-container" i18n>New Workflow
<input type="checkbox" id="new-workflow" name="new-workflow" [(ngModel)]="new_checkbox" (click)="uncheckBox($event, 'edit')">
<span class="checkmark" id="new-workflow-checkmark" [class]="checkmark_class"><span id="new-workflow-checkmark-content"></span>{{checkmark_content}}</span>
</label>
<input type="text" *ngIf="new_checkbox" id="new_workflow_name" name="new_workflow_name" (keyup)="clearWorkflowError(true)" [(ngModel)]="new_workflow" placeholder="Enter Workflow Name">
<p [hidden]="!show_workflow_error && !workflowForm.errors" class="workflow-error" i18n>The workflow name already exists. Please use a different name.</p>
</div>
</div>
Component.ts:
duplicateWorkflowValidator(control: FormControl) {
console.log("this validator was called!");
clearTimeout(this.workflowTimeout);
return new Promise((resolve, reject) => {
if (this.new_workflow != '') {
this.workflowTimeout = setTimeout(() => {
this
.requestService
.findWorkflow(control.value)
.subscribe((results: any) => {
let data: any = results.data;
if (data.duplicate) {
resolve({ duplicateWorkflow: { value: control.value}})
}
else if (results.status == "OK") {
resolve(null);
}
})
;
}, 500);
}
else {
resolve(null);
}
})
}
Inside constructor for component.ts:
this.workflowForm = new FormGroup({
name: new FormControl(this.new_workflow, [
Validators.required,
], this.duplicateWorkflowValidator.bind(this))
});
I am trying to bind this asynchronous validator to the reactive form but it's not working. I want to use the duplicateWorkflowValidator inside workflowForm and have it trigger an error message when it finds a duplicate workflow.
How do I a) bind the validator to the reactive form properly, b) access the validator errors? Thanks in advance, hopefully this makes sense.

You are mixing template forms with reactive forms. Chose one approach. In the below example I am using reactive forms.
Try this simplified version. For demonstration purposes below the validator will fail when I type test, but succeed when I type anything else. You will need to change that to your service call.
https://angular-sjqjwh.stackblitz.io
Template:
<form [formGroup]="myForm">
<div>
<div>
<input type="text" formControlName="name">
<div *ngIf="myForm.controls.name.hasError('duplicateWorkflow')">
Workflow already exists!
</div>
{{ myForm.controls.name.hasError('duplicateWorkflow') | json }}
</div>
</div>
</form>
Component
import { Component } from '#angular/core';
import { FormControl, FormGroup, Validators, Form, FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
workflowTimeout: number = 0;
myForm: FormGroup;
constructor(private fb: FormBuilder) {
this.myForm = this.fb.group({
name: new FormControl('',
[Validators.required],
this.duplicateWorkflowValidator.bind(this))
});
}
duplicateWorkflowValidator(control: FormControl) {
console.log("this validator was called!");
return new Promise((resolve, reject) => {
if (control.value === 'test') {
this.workflowTimeout = setTimeout(() => {
resolve({ duplicateWorkflow: { value: control.value } })
}, 500);
}
else {
resolve(null);
}
});
}
}

Related

Angular async validation not printing error message

Below is my Component :
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
import { HttpService } from './http.service';
import { ProjectidService } from './projectid.service';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
projectDetailForm: FormGroup;
public submitted = false;
constructor(private fb: FormBuilder, private projectidvalidator: ProjectidService) { }
ngOnInit() {
this.projectDetailForm = this.fb.group({
projectid: ['', [Validators.required], [this.projectidvalidator.validate.bind(this.projectidvalidator)]],
projectname: ['name', Validators.required]
})
}
get f() { return this.projectDetailForm.controls; }
get validprojectid() { return this.projectDetailForm.get('projectid'); }
onSubmit(form: FormGroup) {
this.submitted = true;
// stop here if form is invalid
if (this.projectDetailForm.invalid) {
return;
}
console.log('Valid?', this.projectDetailForm.valid); // true or false
console.log('ID', this.projectDetailForm.value.projectid);
console.log('Name', this.projectDetailForm.value.projectname);
}
}
My Service :
import { Injectable } from '#angular/core';
import { Observable, of } from 'rxjs';
import { delay, tap, debounceTime } from 'rxjs/operators';
#Injectable()
export class HttpService {
constructor() { }
checkProjectID(id): Observable<any> {
// Here I will have valid HTTP service call to check the data
return of(true)
}
}
My Async validator :
import { HttpService } from './http.service';
import { Injectable } from '#angular/core';
import { AsyncValidator, AbstractControl, ValidationErrors } from '#angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, switchMap } from 'rxjs/operators';
#Injectable()
export class ProjectidService {
constructor(private _httpService:HttpService) { }
validate(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
console.log(control.value);
return control.valueChanges.pipe(
debounceTime(500),
switchMap(_ => this._httpService.checkProjectID(control.value).pipe(
map(isTaken => {
console.log(isTaken);
if (isTaken) {
return { noproject: true }
} else {
return null
}
})
)),
catchError(() => null)
);
}
}
and template :
<form [formGroup]="projectDetailForm" name="projectdetails" (ngSubmit)="onSubmit(projectDetailForm)">
<div class="form-group">
<label for="id">Project ID</label>
<input type="text" class="form-control" id="id" [ngClass]="{ 'is-invalid': f.projectid.invalid && (f.projectid.dirty || f.projectid.touched) }" placeholder="Project ID" name="projectid" formControlName='projectid'>
<button type="button">Validate</button>
<div *ngIf="f.projectid.invalid && (f.projectid.dirty || f.projectid.touched)" class="invalid-feedback">
<div *ngIf="f.projectid.errors.required">Project ID is required</div>
<div *ngIf="f.projectid.errors?.noproject">
Project id is not valid
</div>
</div>
<div *ngIf="f.projectid.errors?.noproject">
Project id is not valid
</div>
{{f.projectid.errors | json}}
</div>
<div class="form-group">
<label for="name">Project Name</label>
<input type="text" class="form-control" id="name" placeholder="Project Name" name="projectname" readonly formControlName='projectname'>
</div>
<div class="form-group d-flex justify-content-end">
<div class="">
<button type="button" class="btn btn-primary">Cancel</button>
<button type="submit" class="btn btn-primary ml-1">Next</button>
</div>
</div>
</form>
Problem is my custom async validation error message is not getting displayed.
Here is stackblitz example
You could do it as follows using rxjs/timer:
import { timer } from "rxjs";
....
return timer(500).pipe(
switchMap(() => {
if (!control.value) {
return of(null);
}
return this._httpService.checkProjectID(control.value).pipe(
map(isTaken => {
console.log(isTaken);
if (isTaken) {
return { noproject: true };
} else {
return null;
}
})
);
})
);
Sample
The real problem is and I have encountered this myself, you subscribe to the value change but you need to wait for the statuschange to return.
It is "PENDING" while it is doing the call.
The debounce/timer/... are just 'hacks' since you never know when the value is returned.
Declare a variable:
this.formValueAndStatusSubscription: Subscription;
In your
this.formValueAndStatusSubscription =
combineLatest([this.form.valueChanges, this.form.statusChanges]).subscribe(
() => this.formStatusBaseOnValueAndStatusChanges = this.form.status
);
Don't forget to desstroy the subscription
The most important point in the async validation is as descriped in Angular Doc
The observable returned must be finite, meaning it must complete at
some point. To convert an infinite observable into a finite one, pipe
the observable through a filtering operator such as first, last, take,
or takeUntil.
so basically you can use for example take(1) , it'll take the first emission then mark the Observable completed
return control.valueChanges.pipe(
debounceTime(500),
take(1),
switchMap(() =>
this._httpService.checkProjectID(control.value).pipe(
map(isTaken =>
isTaken ? { noproject: true } : null
)
))
)
demo

Form control missing randomly for reactive form with filter pipe

Below is a simple reactive form with the filter of an array of checkboxes.
As soon as page render getting error
Cannot find control with path: 'accountsArray -> 555'
However, the filter is working perfectly, but while removing any character from filter throws an error
Cannot find control with path: 'accountsArray -> 123'
Form control not found based on search.
Below is length code, but that will help you to understand clearly.
Component:
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormArray, FormGroup, FormControl } from '#angular/forms';
import { SubAccount } from './account-model';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
searchForm: FormGroup;
searchTerm = '';
formUpdated = false;
accounts = [
new SubAccount('123'),
new SubAccount('555'),
new SubAccount('123555')
];
subAccount = [];
constructor(private fb: FormBuilder) { }
get accountsArray(): FormArray {
return this.searchForm.get('accountsArray') as FormArray;
}
addAccount(theAccount: SubAccount) {
this.accountsArray.push(this.fb.group({
account: theAccount
}));
}
ngOnInit() {
this.formUpdated = false;
this.searchForm = this.fb.group({
accountSearch: '',
accountsArray: this.fb.array([new FormControl('')])
});
this.accounts.forEach((field: any) => {
this.subAccount.push({ key: field.key, value: field.key });
});
const fieldFGs = this.subAccount.map((field) => {
const obj = {};
if (field.value) {
obj[field.value] = true;
} else {
obj[field] = true;
}
return this.fb.group(obj);
});
const fa = this.fb.array(fieldFGs);
this.searchForm.setControl('accountsArray', fa);
this.formUpdated = true;
}
getAccountNumber(account: SubAccount) {
return Object.keys(account)[0];
}
}
View:
<div [formGroup]="searchForm" *ngIf="formUpdated">
<label for="search">Find an account...</label>
<input id="search" formControlName="accountSearch" [(ngModel)]="searchTerm" />
<div formArrayName="accountsArray" *ngIf="formUpdated">
<div *ngFor="let account of accountsArray.controls | filter: 'key' :searchTerm; let ind=index">
<input type="checkbox" id="checkbox_claim_debtor_{{ind}}" formControlName="{{getAccountNumber(account.controls)}}"/>
<span> {{getAccountNumber(account.controls)}} </span>
</div>
</div>
</div>
Pipe:
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'filter'
})
export class FilterPipe implements PipeTransform {
transform(items: any[], field: string, value: string): any[] {
if (!value && !items) {
return items;
}
return items.filter((item) => {
const val = Object.keys(item.controls)[0];
if (val && val.toLowerCase().indexOf(value.toLowerCase()) >= 0) {
return true
} else {
return false;
}
});
}
}
Appreciate your help.
Stackblitz link:
https://stackblitz.com/edit/angular-9ouyqr
please check the [formGroupName]="ind" , it is not written while iterating the form array ,formGroupname should be addeed with the form index
<div [formGroup]="searchForm" *ngIf="formUpdated">
<label for="search">Find an account...</label>
<input id="search" formControlName="accountSearch" [(ngModel)]="searchTerm" />
<div formArrayName="accountsArray" *ngIf="formUpdated">
<div [formGroupName]="ind" *ngFor="let account of accountsArray.controls | filter: 'key' :searchTerm; let ind=index"
>
<input type="checkbox" id="checkbox_claim_debtor_{{ind}}" formControlName="{{getAccountNumber(account.controls)}}"/>
<span> {{getAccountNumber(account.controls)}} </span>
</div>
</div>
</div>

Angular 6: setTimeout function is not working in my custom validator

I am following a tutorial in order to perform Asynchronous validation in Angular.
What I am trying to achieve is my custom validator which is shouldBeUnique should be call after delay of 2 seconds. I am using setTimeout function in it but it is not working. even Error message not shows in div.
Here is my custom validation error file.
import { AbstractControl, ValidationErrors } from '#angular/forms';
export class UsernameValidator {
static cannotContainSpace(control: AbstractControl): ValidationErrors | null {
if ((control.value as string).indexOf(' ') >= 0 ) {
return { cannotContainSpace: true };
}
return null;
}
static shouldBeUnique(control: AbstractControl): Promise<ValidationErrors | null> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (control.value === 'bilal') {
resolve({shouldBeUnique: true});
} else {
resolve(null);
}
}, 2000);
});
}
}
HTML file.
<form [formGroup] = "form">
<div class="form-group">
<label for="username">Username</label>
<input
formControlName = "username"
id="username"
type="text"
class="form-control">
<div *ngIf="username.touched && username.invalid" class="alert alert-danger">
<div *ngIf="username.errors.required">username is required</div>
<div *ngIf="username.errors.minlength">
minlength {{username.errors.minlength.requiredLength}} is required
</div>
<div *ngIf="username.errors.cannotContainSpace">
cannot contain space
</div>
<div *ngIf="username.errors.shouldBeUnique">
username should b unique
</div>
</div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
formControlName = "password"
id="password"
type="text"
class="form-control">
</div>
<button class="btn btn-primary" type="submit">Sign Up</button>
</form>
Type script file
import { Component } from '#angular/core';
import {FormGroup, FormControl, Validators} from '#angular/forms';
import { UsernameValidator } from './username.validator';
#Component({
// tslint:disable-next-line:component-selector
selector: 'signup-form',
templateUrl: './signup-form.component.html',
styleUrls: ['./signup-form.component.css']
})
export class SignupFormComponent {
form = new FormGroup({
username: new FormControl('', [
Validators.required,
Validators.minLength(3),
UsernameValidator.cannotContainSpace,
UsernameValidator.shouldBeUnique
]),
password: new FormControl('' , Validators.required)
});
get username() {
return this.form.get('username');
}
}
Async validators should be the third argument to FormControl, so you should initialize yours like this:
form = new FormGroup({
username: new FormControl('',
[
// regular validators
Validators.required,
Validators.minLength(3),
UsernameValidator.cannotContainSpace
],
[
// async validators
UsernameValidator.shouldBeUnique
]),
password: new FormControl('' , Validators.required)
});
Async validators should be placed after validators,
export declare class FormControl extends AbstractControl {
constructor(
formState?: any,
validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null,
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null
);
new FormGroup({
username: new FormControl('',
[
Validators.required,
Validators.minLength(3),
UsernameValidator.shouldBeUnique
],
[
UsernameValidator.cannotContainSpace,
])
});

Angular form disable control on valueChanges

I am trying to disable a form control when one of my form group value changes but the problem is that the method disable() and enable() also updates the form status, so I have an infinite loop.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input type="text" formControlName="other"/>
</form>
`,
})
export class App {
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if(v.question === 'test') {
this.questionForm.get('other').disable();
} else {
...enable()
}
})
}
}
How can I solve this problem?
Well, according to the official docs you could use an directive for custom validation, this could of course in your case could be applied with logic of checking what the user has input and then disable and enable the other field. There's quite a lot of code there, sooooo...
...You could also do a smaller hack if you do not want all that bunch of code. Let's call a function that checks what user has typed. We also need to bind this, so we can refer to the questionForm:
this.questionForm = this.fb.group({
question: ['', [this.checkString.bind(this)]],
other: ['']
});
Then the function:
checkString(control: FormControl) {
if(control.value == 'test') {
this.questionForm.get('other').disable()
}
// we need to check that questionForm is not undefined (it will be on first check when component is initialized)
else if (this.questionForm) {
this.questionForm.get('other').enable()
}
}
This seems to serve it's purpose.
Demo
Hope this helps! :)
I would first add a property isDisabled to your component, then I would use the [disabled] directive. This would solve your looping issue.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input type="text" formControlName="other" [disabled]="isDisabled" />
</form>
`,
})
export class App {
isDisabled = false;
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if (v.question === 'test') {
this.isDisabled = true;
} else {
this.isDisabled = false;
}
})
}
}
With reactive forms, you do it something like this:
question: {value: '', disabled: true }
Adding another answer to work around the reactive forms issue using two controls and an *ngIf directive. See https://github.com/angular/angular/issues/11324.
This solution may not be ideal as it requires that you add a second form control that will appear when the conditions for changing the isDisabled property are met. This means there are two controls to manage when submitting the form.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="questionForm">
<input type="text" formControlName="question" />
<input *ngIf="isDisabled" type="text" formControlName="other" disabled />
<input *ngIf="!isDisabled" type="text" formControlName="other2" />
</form>
`
providers: [FormBuilder]
})
export class App {
isDisabled = true;
constructor(private fb: FormBuilder) {
this.questionForm = this.fb.group({
question: [''],
other: [''],
other2: ['']
});
}
ngOnInit() {
this.questionForm.valueChanges.subscribe(v => {
if(v.question === 'test') {
this.isDisabled = false;
} else {
this.isDisabled = true;
}
});
}
}
Check out or play with the plunker if you wish:
https://plnkr.co/edit/0NMDBCifFjI2SDX9rwcA

Angular 2 Cannot read property 'x' of null

I am learning promises and I am trying to get a simple example to work but I get the error. The promises role is just to check whether a certain name has been entered and produce a boolean error on call back
Cannot read property 'shouldBeUnique' of null
Here is my component
import {Component, Inject} from '#angular/core';
import {FormGroup, Validators, FormBuilder} from '#angular/forms';
import {UsernameValidators} from './usernameValidators'
#Component({
selector: 'signup-form',
templateUrl: 'app/signup-form.component.html'
})
export class SignUpFormComponent {
form: FormGroup;
constructor(#Inject(FormBuilder) fb: FormBuilder) {
this.form = fb.group({
username: ['', Validators.compose([
Validators.required,
UsernameValidators.cannotContainSpace
]), UsernameValidators.shouldBeUnique],
password: ['', Validators.required]
})
}
get username(): any {return this.form.get('username');}
get password(): any {return this.form.get('password');}
}
Here is my component.html
<form [formGroup]="form" (ngSubmit)="signup()">
<div class="form-group">
<label for="username">Username</label>
<input
id="username"
type="text"
class="form-control"
formControlName="username" placeholder="Username"
>
<div *ngIf="username.touched && username.errors">
<div *ngIf="username.errors.shouldBeUnique" class="alert alert-danger">This username is already taken</div>
</div>
</form>
Here is my validator class where the promise is being made
import {FormControl} from '#angular/forms';
export class UsernameValidators {
static shouldBeUnique(control: FormControl) {
return new Promise((resolve, reject) => {
setTimeout(function(){
if (control.value == "mosh")
resolve({ shouldBeUnique: true});
else
resolve(null);
}, 1000)
});
}
}
Thanks
Try using the safe navigation operator (?.) to guard against null and undefined values in property paths.
<div *ngIf="username.touched && username.errors">
<div *ngIf="username.errors?.shouldBeUnique" class="alert alert-danger">This username is already taken</div>
</div>
This should resolve the error you are currently running into. Read more in the Angular 2 docs here:
https://angular.io/guide/template-syntax#safe-navigation-operator

Categories