How to perform async validation using reactive/model-driven forms in Angular 2 - forms

I have an email input and I want to create a validator to check, through an API, if the entered email it's already in the database.
So, I have:
A validator directive
import { Directive, forwardRef } from '#angular/core';
import { Http } from '#angular/http';
import { NG_ASYNC_VALIDATORS, FormControl } from '#angular/forms';
export function validateExistentEmailFactory(http: Http) {
return (c: FormControl) => {
return new Promise((resolve, reject) => {
let observable: any = http.get('/api/check?email=' + c.value).map((response) => {
return response.json().account_exists;
});
observable.subscribe(exist => {
if (exist) {
resolve({ existentEmail: true });
} else {
resolve(null);
}
});
});
};
}
#Directive({
selector: '[validateExistentEmail][ngModel],[validateExistentEmail][formControl]',
providers: [
Http,
{ provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => ExistentEmailValidator), multi: true },
],
})
export class ExistentEmailValidator {
private validator: Function;
constructor(
private http: Http
) {
this.validator = validateExistentEmailFactory(http);
}
public validate(c: FormControl) {
return this.validator(c);
}
}
A component
import { Component } from '#angular/core';
import { FormGroup, FormBuilder, Validators } from '#angular/forms';
import { ExistentEmailValidator } from '../../directives/existent-email-validator';
#Component({
selector: 'user-account',
template: require<string>('./user-account.component.html'),
})
export class UserAccountComponent {
private registrationForm: FormGroup;
private registrationFormBuilder: FormBuilder;
private existentEmailValidator: ExistentEmailValidator;
constructor(
registrationFormBuilder: FormBuilder,
existentEmailValidator: ExistentEmailValidator
) {
this.registrationFormBuilder = registrationFormBuilder;
this.existentEmailValidator = existentEmailValidator;
this.initRegistrationForm();
}
private initRegistrationForm() {
this.registrationForm = this.registrationFormBuilder.group({
email: ['', [this.existentEmailValidator]],
});
}
}
And a template
<form novalidate [formGroup]="registrationForm">
<input type="text" [formControl]="registrationForm.controls.email" name="registration_email" />
</form>
A've made other validator this way (without the async part) and works well. I think te problem it's related with the promise. I'm pretty sure the code inside observable.subscribe it's running fine.
What am I missing?
I'm using angular v2.1

Pretty sure your problem is this line:
...
email: ['', [this.existentEmailValidator]],
...
You're passing your async validator to the synchronous validators array, I think the way it should be is this:
...
email: ['', [], [this.existentEmailValidator]],
...
It would probably be more obvious if you'd use the new FormGroup(...) syntax instead of FormBuilder.

Related

Ionic 4. Alternative to NavParams

I am using ionic 4. It does not accept to receive data using navparams.
Here is my sender page method:
//private route:Router
gotoFinalView(intent) {
this.route.navigateByUrl(`${intent}`, this.destination);
}
Receiver page line;
//private navParams:NavParams
this.destination = navParams.data;
What is the right approach to doing this in ionic 4. I am also uncertain whether gotoFinalView method is valid.
This is how I solved my problem:
I created a Service with a setter and getter methods as;
import { Injectable } from "#angular/core";
#Injectable({
providedIn: "root"
})
export class MasterDetailService {
private destn: any;
constructor() {}
public setDestn(destn) {
this.destn = destn;
}
getDestn() {
return this.destn;
}
}
Injected the Service and NavController in the first page and used it as;
gotoFinalView(destn) {
this.masterDetailService.setDestn(destn);
this.navCtrl.navigateForward("destn-page");
}
Extracted the data at the final page by;
constructor(
private masterDetailService: MasterDetailService
) {
this.destination = this.masterDetailService.getDestn();
}
This is the efficient way to solve your problem
user Angular Routers concepts in your application.
just declare your router like the following
Your app routing module like the following
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import {ViewComponent} from "./crud/view/view.component";
import {CreateComponent} from "./crud/create/create.component";
import {UpdateComponent} from "./crud/update/update.component";
import {ReadComponent} from "./crud/read/read.component";
const routes: Routes = [
{path: '', component: ViewComponent},
{path: 'create', component: CreateComponent},
{path: 'update/:id', component: UpdateComponent},
{path: 'view/:id', component: ReadComponent}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
:id is the parameter what i want to send to that page.
this.router.navigate([link + '/' + id]);
share your parameter like this in your first page.
In your second page inject the activated route using DI(Dependency Injection)
constructor(private actRoute: ActivatedRoute)
Then Get your parameters using the following code
this.productID = this.actRoute.snapshot.params['id'];
This is the simple way. You can send multiple parameter at a time.
{path: 'update/:id/:name/:price', component: UpdateComponent}
and get those parameters like the following
this.productID = this.actRoute.snapshot.params['id'];
this.productName = this.actRoute.snapshot.params['name'];
this.productPrice = this.actRoute.snapshot.params['price'];
While Routing you can write like this:
this.router.navigate(["/payment-details",{
prev_vehicle_type: this.vehicle_type,
prev_amt: this.amt,
prev_journey:this.whichj
}]);
To get this parameters on the next page you can write:
constructor(
public router: Router,
public activateroute: ActivatedRoute){
this.activateroute.params.subscribe((data: any) => {
console.log(data);
this.vehicle_type = data.prev_vehicle_type;
this.amt = data.prev_amt;
this.whichj = data.prev_journey;
});
}
ionic 4 navigation with params
sender page
1. import the following
import {NavController} from '#ionic/angular';
import { NavigationExtras } from '#angular/router';
constructor(private navCtrl:NavController)
sender page
gotonextPage()
gotonextPage()
{
let navigationExtras: NavigationExtras = {
state: {
user: 'name',
parms:Params
}
};
this.navCtrl.navigateForward('pageurl',navigationExtras);
}
4.Receiver Page
import { ActivatedRoute, Router } from '#angular/router';
constructor( private route: ActivatedRoute, private router: Router)
{
this.route.queryParams.subscribe(params => {
if (this.router.getCurrentNavigation().extras.state) {
this.navParams = this.router.getCurrentNavigation().extras.state.parms;
}});
}
Send data with Router service and extract with global variable, history
//sender component
// private router: Router
nextPage() {
this.router.navigate(['history'],
{ state: [{ name: "covid-19", origin: "china" },{ name: "ebola", origin: "us" }] }
)
}
//receiver page
ngOnInit() {
let data = history.state;
console.log("data-->",data);
// ** data**
//0:{name: "covid-19", origin: "china"} 1: {name: "ebola", origin: "us"} navigationId: 2
}
The item, icon and title variables you want to send should be written in the state in this way.
this.nav.navigateForward('/myUrl', {
state: {
'items': this.substances,
'icon': ICONS.substance,
'title': 'Etken Maddeler'
}
});
We take incoming variables this way.
//receive
constructor(
protected route: ActivatedRoute,
protected router: Router,
) {
this.selectedItem = null;
this.route.paramMap.subscribe(params => {
let st = this.router.getCurrentNavigation().extras.state;
if (st) {
this.selectedItem = st.items;
}
});
}
Very very simply, you can do something like this:
In the "calling screen" :
this.router.navigate(['url', {model: JSON.stringify(myCustomObject)}])
In the "called screen" :
this.myvar = JSON.parse(this.activatedRoute.snapshot.paramMap.get('model')
Et voilà !
//in home.ts
import{ Router,ActivatedRoute, NavigationExtras } from '#angular/router';
getProductStatics(productObject : any) {
var object1 = {
id: productObject,
}
const navigationExtras: NavigationExtras = {state : {object:
JSON.stringify(object1)}};
this.router.navigate(["/product-statics"], navigationExtras);
}
//in product-statics.ts
import{ Router,ActivatedRoute,NavigationExtras } from '#angular/router';
if(self.router.getCurrentNavigation().extras.state) {
var object1
= this.router.getCurrentNavigation().extras.state.object;
var object = JSON.parse(object1);
var newObjectData = object.id;
}

Use property as form validator in Angular2 (data-driven)

I'm having a hard time trying to set a max value using the data driven approach in Angular2.
I want to set the max value of the input to a property called userLimit, which I get from firebase. This is my code:
component.ts
import { Component, OnInit, AfterViewInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators, FormControl } from "#angular/forms";
import { FiredbService } from '../services/firedb.service';
import { AuthService } from '../services/auth.service';
import { AngularFireDatabase, FirebaseListObservable, FirebaseObjectObservable } from 'angularfire2/database';
#Component({
selector: 'my-dashboard',
styleUrls: ['./recibirpago.component.scss'],
templateUrl: './recibirpago.component.html'
})
export class RecibirpagoComponent implements OnInit, AfterViewInit {
myForm2: FormGroup;
uid: string;
userLimit: any;
constructor(private fb: FormBuilder,
private dbfr: FiredbService,
private db: AngularFireDatabase,
private authService: AuthService) {
this.myForm2 = this.fb.group({
email: ['', Validators.email],
clpmount: ['', [Validators.required, Validators.max(this.userLimit)]]
});
}
ngOnInit() {
this.uid = this.authService.getUserUid();
}
ngAfterViewInit() {
this.dbfr.getUserLimit(this.uid).subscribe(snapshot => {
this.userLimit = snapshot.val().accountLimit;
console.log(this.userLimit);
})
}
If I write, for example, Validators.max(5000) it works, but if I try to get the data from Firebase it doesn't work.
Thanks for your help!
The problem is that the constructor is executing before the ngAfterViewInit so you don't have the value of the userLimit at that point.
Instead use the setVAlidators method within the subscribe where you get the data.
Something like this:
constructor
this.myForm2 = this.fb.group({
email: ['', Validators.email],
clpmount: ['', Validators.required] // <-- not here
});
ngAfterViewInit
ngAfterViewInit() {
this.dbfr.getUserLimit(this.uid).subscribe(snapshot => {
this.userLimit = snapshot.val().accountLimit;
console.log(this.userLimit);
const clpControl = this.myForm2.get(`clpmount');
clpControl.setValidators(Validators.max(this.userLimit)); // <-- HERE
clpControl.updateValueAndValidity();
})
}
NOTE: Syntax was not checked.

Angular 2 Custom Validator (Template Form) Validation

I am trying to implement a custom validator that checks if the keyed username exists. However, I am running into a problem where the control is always invalid. I have confirmed the webApi is returning the correct values and the validator function is stepping into the proper return statements.
My custom validator is as follows:
import { Directive, forwardRef } from '#angular/core';
import { AbstractControl, ValidatorFn, NG_VALIDATORS, Validator, FormControl } from '#angular/forms';
import { UsersService } from '../_services/index';
import { Users } from '../admin/models/users';
function validateUserNameAvailableFactory(userService: UsersService): ValidatorFn {
return (async (c: AbstractControl) => {
var user: Users;
userService.getUserByName(c.value)
.do(u => user = u)
.subscribe(
data => {
console.log("User " + user)
if (user) {
console.log("Username was found");
return {
usernameAvailable: {
valid: false
}
}
}
else {
console.log("Username was not found");
return null;
}
},
error => {
console.log("Username was not found");
return null;
})
})
}
#Directive({
selector: '[usernameAvailable][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => UserNameAvailableValidator), multi: true }
]
})
export class UserNameAvailableValidator implements Validator {
validator: ValidatorFn;
constructor(private usersService: UsersService) {
}
ngOnInit() {
this.validator = validateUserNameAvailableFactory(this.usersService);
}
validate(c: FormControl) {
console.log(this.validator(c));
return this.validator(c);
}
}
And the form looks like:
<form #userForm="ngForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form form-group col-md-12">
<label for="UserName">User Name</label>
<input type="text" class="form-control" id="UserName"
required usernameAvailable maxlength="50"
name="UserName" [(ngModel)]="user.userName"
#UserName="ngModel" />
<div [hidden]="UserName.valid || UserName.pristine" class="alert alert-danger">
<p *ngIf="UserName.errors.required">Username is required</p>
<p *ngIf="UserName.errors.usernameAvailable">Username is not available</p>
<p *ngIf="UserName.errors.maxlength">Username is too long</p>
</div>
<!--<div [hidden]="UserName.valid || UserName.pristine" class="alert alert-danger">Username is Required</div>-->
</div>
</form>
I have followed several tutorials proving this structure works, so I am assuming that I am possibly messing up the return from within the validator function.
Also, is there a way I can present the list of errors (I have tried using li with ngFor, but I get nothing from that)?
So, there was something wrong with using .subscribe the way I was. I found a solution by using the following 2 links:
https://netbasal.com/angular-2-forms-create-async-validator-directive-dd3fd026cb45
http://cuppalabs.github.io/tutorials/how-to-implement-angular2-form-validations/
Now, my validator looks like this:
import { Directive, forwardRef } from '#angular/core';
import { AbstractControl, AsyncValidatorFn, ValidatorFn, NG_VALIDATORS, NG_ASYNC_VALIDATORS, Validator, FormControl } from '#angular/forms';
import { Observable } from "rxjs/Rx";
import { UsersService } from '../_services/index';
import { Users } from '../admin/models/users';
#Directive({
selector: "[usernameAvailable][ngModel], [usernameAvailable][formControlName]",
providers: [
{
provide: NG_ASYNC_VALIDATORS,
useExisting: forwardRef(() => UserNameAvailableValidator), multi: true
}
]
})
export class UserNameAvailableValidator implements Validator {
constructor(private usersService: UsersService) {
}
validate(c: AbstractControl): Promise<{ [key: string]: any }> | Observable<{ [key: string]: any }> {
return this.validateUserNameAvailableFactory(c.value);
}
validateUserNameAvailableFactory(username: string) {
return new Promise(resolve => {
this.usersService.getUserByName(username)
.subscribe(
data => {
resolve({
usernameAvailable: true
})
},
error => {
resolve(null);
})
})
}
}
This is now working. However, I now have an issue where the the control temporarily invalidates itself while the async validator is running.
Return for true should be:
{usernameAvailable: true}
or null for false.

Angular2 - access the AbstractControl instance in validation directive

I have to trigger validation from the inside of a validator directive.
Here is the directive I have. It works as expected. However I want it to trigger the validation process when the validator function changes. I.e. when its input variable maxDate changes.
How could I do this ?
If I could access the AbstractControl instance in the constructor I could easily do this. I can't think of a way to do it, however.
import { AbstractControl, FormGroup, ValidatorFn, Validator, NG_VALIDATORS, Validators } from '#angular/forms';
import { Directive, Input, OnChanges, SimpleChanges, EventEmitter } from '#angular/core';
function parseDate(date: string):any {
var pattern = /(\d{2})-(\d{2})-(\d{4})/;
if (date) {
var replaced = date.search(pattern) >= 0;
return replaced ? new Date(date.replace(pattern,'$3-$1-$2')) : null;
}
return date;
}
export function maxDateValidator(maxDateObj): ValidatorFn {
return (control:AbstractControl): {[key: string]: any} => {
const val = control.value;
let date = parseDate(val);
let maxDate = parseDate(maxDateObj.max);
if (date && maxDate && date > maxDate) {
return {
maxDateExceeded: true
};
}
return null;
};
}
...
#Directive({
selector: '[maxDate]',
providers: [{provide: NG_VALIDATORS, useExisting: maxDateDirective, multi: true}]
})
export class maxDateDirective implements Validator, OnChanges {
#Input() maxDate: string;
private valFn = Validators.nullValidator;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['maxDate'];
if (change) {
const val: string = change.currentValue;
this.valFn = maxDateValidator(val);
}
else {
this.valFn = Validators.nullValidator;
}
//This is where I want to trigger the validation again.
}
validate(control): {[key: string]: any} {
return this.valFn(control);
}
}
Usage:
<input type="text" [(ngModel)]="deathDateVal">
<input class="form-control"
type="text"
tabindex="1"
[maxDate]="deathDateVal"
name="will_date"
[textMask]="{pipe: datePipe, mask: dateMask, keepCharPositions: true}"
ngModel
#willDate="ngModel">
Here is what I've just come up with:
#Directive({
selector: '[maxDate]',
providers: [{provide: NG_VALIDATORS, useExisting: maxDateDirective, multi: true}]
})
export class maxDateDirective implements Validator, OnChanges {
#Input() maxDate: string;
private valFn = Validators.nullValidator;
private control:AbstractControl;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['maxDate'];
if (change) {
const val: string = change.currentValue;
this.valFn = maxDateValidator(val);
}
else {
this.valFn = Validators.nullValidator;
}
if (this.control) {
this.control.updateValueAndValidity(this.control);
}
}
validate(_control:AbstractControl): {[key: string]: any} {
this.control = _control;
return this.valFn(_control);
}
}
It works. Validate is called on initialization so I just store its parameter.
It is fuckin' ugly but it works.
To get your hands on the abstractControl of the input you can do something like this:
#Directive({
// tslint:disable-next-line:directive-selector
selector: 'input[type=date][maxDate]'
})
export class InputFullWithDirective implements Validator, OnChanges {
constructor(#Self() private control: NgControl) {}
/** the rest is mostly unchanged from the question */
}

AngularJS 2 Typescript interface

I have a service for handling users operations and an interface for the user object.
user.service.ts
import {Injectable} from 'angular2/core';
export interface User {
name: string;
email?: string;
picture?: string;
}
#Injectable()
export class UserService {
me: User;
constructor() {
}
setUser(user: User) {
this.me = user;
}
}
In my login component I try to set the user with the profile returned from the login service but I get this error:
Property 'firstName' does not exist on type '{}'.
login.component.ts
import {Component} from 'angular2/core';
import {User, UserService} from './services/user.service';
import {LinkedinService} from './services/linkedin.service';
declare const IN: any;
console.log('`Login` component loaded asynchronously');
#Component({
selector: 'Login',
providers: [
UserService,
LinkedinService
],
template: require('./login.html')
})
export class LoginComponent {
me: User;
constructor(public linkedinService: LinkedinService, public userService: UserService) {
this.me = userService.me;
}
ngOnInit() {
console.log('hello `Login` component');
}
login() {
this.linkedinService.login()
.then(() => this.linkedinService.getMe()
.then(profile => this.userService.setUser({ name: profile.firstName })));
}
}
linkedin.service.ts
import {Injectable} from 'angular2/core';
declare const IN: any;
#Injectable()
export class LinkedinService {
constructor() {
IN.init({
api_key: 'xxxxxxxxxxx',
authorize: true
});
}
login() {
return new Promise((resolve, reject) => {
IN.User.authorize(() => resolve());
});
}
getMe() {
return new Promise((resolve, reject) => {
IN.API.Profile('me').result((profile) => resolve(profile.values[0]));
});
}
}
I'm trying to import the User interface from UserService and use inside the LoginComponent but I don't know what I'm doing wrong. Any idea? I am not sure if I have to use the User interface inside the LoginComponent, is that right?
Narrow in on the code :
.then(() => this.linkedinService.getMe())
.then(profile => this.userService.setUser({ name: profile.firstName })));
The type of profile is driven by the response of this.linkedinService.getMe(). Seems like it is something like Promise<{}>. It does not have the member firstName. Hence the error:
Property 'firstName' does not exist on type '{}'.
Fix
Check to the code / signatures of linkedinService. This has nothing to do with the user.service.ts file that the question contains 🌹
Update
Focus in on the code:
getMe() {
return new Promise((resolve, reject) => {
IN.API.Profile('me').result((profile) => resolve(profile.values[0]));
});
}
The value returned is driven by what is being passed to resolve. So make sure profile.values[0] has the right type. Alternatively provide the hint to the compiler:
getMe() {
return new Promise<{firstName:string}>((resolve, reject) => {
IN.API.Profile('me').result((profile) => resolve(profile.values[0]));
});
}