Angular 2: How to handle async errors from services in components? - javascript

Let's say we have a service that does an http call to an API to create an user. Based on the result (200 or error) the app should redirect or display the errors (there are some Validations going on client-side, but that is not the topic since validations should always also happen server-side).
The Angular 2 docs state that it is bad practice to let the service return an observable and subscribe to it in the component. The service should be self-contained and the component shouldn't need to know anything about this. It should be enough to call userService.createUser(user_data);
But then again routing should happen in the component. So I'm wondering how to handle this situation? Should my Service return a new Observable? Or just a value? But then how do I deal with the async task?
What is the right way to do this?
Let's take this service to create a user and this component as an example:
// user.service.ts:
import { Http, Headers } from '#angular/http';
import { Inject, Injectable } from '#angular/core';
import { API_CONFIG, Config } from '../config/api.config';
import { User } from '../models/user.model';
#Injectable()
export class UserService {
validation_errors: Array<any> = [];
constructor(
#Inject(API_CONFIG) private api_config: Config,
private http: Http,
#Inject(User) public user: User
) {}
createUser(user: User) {
var body = JSON.stringify({ user });
var myHeader = new Headers();
myHeader.append('Content-Type', 'application/json');
this.http.post(this.api_config.apiEndpoint + '/users/', body, { headers: myHeader })
.map(res => res.json())
.subscribe(
res => {
// User was created successfully
this.user = this.fromJson(res.data);
},
err => {
// Something went wrong, let's collect all errors in a class attribute
let errors = err.json().errors;
for(var i = 0; i < errors.length; i++) {
this.validation_errors.push(errors[i])
}
}
);
}
/**
* #param input_json JSON returned from API, formatted according to jsonapi.org, containing one single user.
* #return UserModel instantiated with the values from input_json
*/
fromJson(input_json: any) {
var user:User = new User();
user = input_json.attributes;
user.id = input_json.id;
return user;
}
}
// user.component.ts:
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup, REACTIVE_FORM_DIRECTIVES, Validators } from '#angular/forms';
import { Router, RouteParams, ROUTER_DIRECTIVES } from '#angular/router-deprecated';
import { UserService } from '../../shared/index';
#Component({
selector: 'fac-user-editor',
templateUrl: 'app/+users/editor/user-editor.component.html',
styleUrls: ['app/+users/editor/user-editor.component.css'],
directives: [REACTIVE_FORM_DIRECTIVES, ROUTER_DIRECTIVES],
providers: [UserService]
})
export class UserEditorComponent implements OnInit {
// Setup Form
private email_regex = '[a-z0-9\\.\\-\\_]+#[a-z0-9\\.\\-\\_]+\\.[a-z0-9\\.\\-\\_]+';
userForm: FormGroup;
action: string;
idCtrl = new FormControl('');
nameCtrl = new FormControl('', [Validators.required]);
emailCtrl = new FormControl('', [Validators.required, Validators.pattern(this.email_regex)]);
usernameCtrl = new FormControl('', [Validators.required, Validators.minLength(5)]);
passwordCtrl = new FormControl('', [Validators.minLength(8)]);
passwordConfirmationCtrl = new FormControl('');
public user_id: string;
constructor(private userService: UserService, private router: Router, private params: RouteParams) {}
/**
* Handle submit of form
*/
onSubmit(form: any) {
// Here should happen some error handling / routing, depending on the result of the call to the API
this.userService.createUser(this.userService.user);
}
ngOnInit(): any {
this.userForm = new FormGroup({
id: this.idCtrl,
name: this.nameCtrl,
email: this.emailCtrl,
username: this.usernameCtrl,
password: this.passwordCtrl,
password_confirmation: this.passwordConfirmationCtrl
});
}
}
I could display the error messages in the template like this:
<div class="form-group"
[hidden]="adminService.validation_errors.length === 0"
class="alert alert-warning" role="alert">
<strong>Some errors occured</strong>
<ul>
<li *ngFor="let validation_error of adminService.validation_errors">
<span class="text-capitalize">{{validation_error.source.field}}:</span> {{validation_error.detail}}
</li>
</ul>
</div>

This is how I solved the problem. My service now uses .map() to transform the data (JSON) returned from the backend and returns an Observable which will initialise my User Model. The Component can subscribe to the observable. The data is stored in a variable of the component, as well as the validation errors.
I also use FormBuilder form the new Form API. This should work with RC 3.
I hope this helps someone.
// user.service.ts:
import { Http, Headers } from '#angular/http';
import { Inject, Injectable } from '#angular/core';
import { API_CONFIG, Config } from '../config/api.config';
import { User } from '../models/user.model';
#Injectable()
export class UserService {
constructor(
#Inject(API_CONFIG) private api_config: Config,
private http: Http,
#Inject(User) public user: User
) {}
createUser(user: User) {
var body = JSON.stringify({ user });
var myHeader = new Headers();
myHeader.append('Content-Type', 'application/json');
// Use map to transform reply from server (JSON to User Object) and return an Observable
// The component can subscribe to.
return this.http.post(this.api_config.apiEndpoint + '/users/', body, { headers: myHeader })
.map(res => res.json())
.map(res => {
return this.fromJson(res.data);
});
}
/**
* #param input_json JSON returned from API, formatted according to jsonapi.org, containing one single user.
* #return UserModel instantiated with the values from input_json
*/
fromJson(input_json: any) {
var user:User = new User();
user = input_json.attributes;
user.id = input_json.id;
return user;
}
}
// user.component.ts:
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup, FormBuilder, REACTIVE_FORM_DIRECTIVES, Validators } from '#angular/forms';
import { Router, RouteParams, ROUTER_DIRECTIVES } from '#angular/router-deprecated';
// Include the User Model
import { User, UserService } from '../../shared/index';
#Component({
selector: 'fac-user-editor',
templateUrl: 'app/+users/editor/user-editor.component.html',
styleUrls: ['app/+users/editor/user-editor.component.css'],
directives: [REACTIVE_FORM_DIRECTIVES, ROUTER_DIRECTIVES],
providers: [User, UserService]
})
export class UserEditorComponent implements OnInit {
// Setup Form
private email_regex = '[a-z0-9\\.\\-\\_]+#[a-z0-9\\.\\-\\_]+\\.[a-z0-9\\.\\-\\_]+';
userForm: FormGroup;
validation_errors: Array<string> = [];
user_id: string;
constructor(private userService: UserService, private user: User, private router: Router, private params: RouteParams, private formBuilder: FormBuilder) {}
/**
* Handle submit of form
*/
onSubmit(form: any) {
// Subscribe to observable and handle errors
this.userService.createUser(this.userService.user).subscribe(
user => {
// Store created user in Component variable. We could now display it or navigate somewhere
this.user = user;
},
err => {
let errors = err.json().errors;
for(var i = 0; i < errors.length; i++) {
// Handle errors in the component and don't store it in the Service
this.validation_errors.push(errors[i])
}
}
);
}
ngOnInit(): any {
// Use the new FormBuilder
this.userForm = this.formBuilder.group({
id: [''],
name: ['', Validators.required],
email: ['', [Validators.required, Validators.pattern(this.email_regex)]],
username: ['', [Validators.required, Validators.minLength(5)]],
password: ['', [Validators.required, Validators.minLength(8)]],
password_confirmation: ['']
});
}
}
user-editor.component.html
<div class="form-group"
[hidden]="validation_errors.length === 0"
class="alert alert-warning" role="alert">
<strong>Some errors occured</strong>
<ul>
<li *ngFor="let validation_error ofvalidation_errors">
<span class="text-capitalize">{{validation_error.source.field}}:</span> {{validation_error.detail}}
</li>
</ul>
</div>

Related

Why am i having "Cannot read property 'http' of undefined" on this validator?

Im trying to make an asynchronous validator for a reactive form control, that checks if a given username already exists. Heres's the async validator code:
userdata.service.ts
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { FormControl } from '#angular/forms';
import { Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class UserdataService {
private apiUrl = 'http://apiurl.com/api'; // its not the real url, im just not posting it for privacy reasons
constructor(private http: HttpClient) {}
checkUsername(control: FormControl): Promise<any> | Observable<any> {
let isUsernameValid;
return new Promise<any>(
(resolve, reject) => {
this.http.get(this.apiUrl + '/users?name='+control.value).subscribe(
response => {
isUsernameValid = response;
});
if (isUsernameValid === 'false') {
resolve({'usernameIsInvalid': true})
} else {
resolve(null);
}
}
);
}
}
When I try this validator i get this error:
"core.js:4197 ERROR TypeError: Cannot read property 'http' of undefined"
Now, i think the errr has something to do with using 'this' but I can't understand why isn't working...
It was driving me insane so I tryed to just place a
console.log(this.apiUrl)
inside the function, outside the promise, just for the sake of trying, and i had the same error: "core.js:4197 ERROR TypeError: Cannot read property 'apiUrl' of undefined"...
Please if someone can explain to me what am I doing wrong, and how to solve it i would be thankful.
EDIT:
I'm calling my service from the reactive form ts file, as shown below:
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup, Validators } from '#angular/forms';
import { ActivatedRoute, Router } from '#angular/router';
import { CustomValidatorsService } from '../services/custom-validators.service';
import { LocationService } from '../services/location.service';
import { UserdataService } from '../services/userdata.service';
#Component({
selector: 'app-userdata-form',
templateUrl: './userdata-form.component.html',
styleUrls: ['./userdata-form.component.scss']
})
export class UserdataFormComponent implements OnInit {
userdataForm: FormGroup;
provinces: any = null;
provincesLoading = false;
cities: any = null;
citiesLoading = false;
constructor(
private locationService: LocationService,
private userdataService: UserdataService,
private customValidators: CustomValidatorsService,
private router: Router,
private route: ActivatedRoute
) { }
ngOnInit(): void {
this.formInit();
this.loadProvinces();
}
formInit() {
let dni: number = null;
let firstname: string = null;
let lastname: string = null;
let email: string = null;
let mobile: number = null;
let phone: number = null;
let birthdate: Date = null;
let username: string = null;
let password: string = null;
this.userdataForm = new FormGroup({
// ... a lot of controls before ...
'username': new FormControl(username, [
Validators.required,
Validators.minLength(3),
Validators.maxLength(30),
Validators.pattern(/^[a-zA-ZÀ-ÿ\u00f1\u00d1]+(\s*[a-zA-ZÀ-ÿ\u00f1\u00d1]*)*[a-zA-ZÀ-ÿ\u00f1\u00d1]+$/)
], this.userdataService.checkUsername), // <-- here's the async validator
// ... form continues...
}
loadProvinces() {
this.provincesLoading = true;
this.locationService.getProvinces().subscribe(response => {
this.provinces = response;
this.provincesLoading = false;
});
}
As I've mentioned in the comments, your custom validator should implement the AsyncValidator interface:
import { AbstractControl, AsyncValidator, ValidationErrors } from '#angular/forms';
import { catchError, map } from 'rxjs/operators';
// ...
#Injectable({
providedIn: 'root'
})
export class UserDataValidator implements AsyncValidator {
private apiUrl = 'http://apiurl.com/api'; // its not the real url, im just not posting it for privacy reasons
constructor(private http: HttpClient) {}
// This method is defined as an arrow function such that it can be used
// properly - see https://github.com/angular/angular/issues/24981
validate = (control: AbstractControl) => {
return this.http.get(`${this.apiUrl}/users?name=${control.value}`).pipe(
map(isUsernameValid => (isUsernameValid === 'false' ? { usernameIsInvalid: true } : null),
catchError(() => of(null))
);
}
}
This can then be added to your FormGroup as follows:
constructor (private userDataValidator: UserDataValidator) {
this.userdataForm = new FormGroup({
username: new FormControl(username, [
// ... validators
// Validator classes don't currently work - see
// https://github.com/angular/angular/issues/24981
this.userDataValidator.validate
]),
// ... other form controls
}
// ...
Notes
I've also cleaned up your code that does the validation logic to use RxJS' pipeable operators as well as to use template literals)
Disclaimer: I've not tested that the code above actually works
Resources
Angular - Validating form input > Creating asynchronous validators
Allow passing validator classes to the FormControl constructor · Issue #24981 · angular/angular
I think you should create validator function which get service instance in argument, just like in this article:
https://medium.com/#tomaszsochacki/how-to-do-asynchronous-validator-in-angular-7-6e80243a874a
when you pass method like this
'username': new FormControl(username, [
Validators.required,
Validators.minLength(3),
Validators.maxLength(30),
Validators.pattern(/^[a-zA-ZÀ-ÿ\u00f1\u00d1]+(\s*[a-zA-ZÀ-ÿ\u00f1\u00d1]*)*[a-zA-ZÀ-ÿ\u00f1\u00d1]+$/)
], this.userdataService.checkUsername),
this context in checkUsername method is missed
or you can do this:
'username': new FormControl(username, [
Validators.required,
Validators.minLength(3),
Validators.maxLength(30),
Validators.pattern(/^[a-zA-ZÀ-ÿ\u00f1\u00d1]+(\s*[a-zA-ZÀ-ÿ\u00f1\u00d1]*)*[a-zA-ZÀ-ÿ\u00f1\u00d1]+$/)
], this.userdataService.checkUsername.bind(this.userdataService)),
bind in first argument pass 'this' context to method when it is called

Angular: how to make localStorage works async

I trying to get data when I log in, by sending ID from localStorage. Everything I tried didn't work, and the only thing comes to my mind is that getting ID from local storage works synchronously. I hope someone can help me make it async. Unfortunately, I don't have permission to show API here. The code:
auth.service.ts
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams } from '#angular/common/http';
import { throwError, Observable } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
import { Restaurant } from '../models/Restaurant';
import { LocalStorage } from '#ngx-pwa/local-storage';
#Injectable({
providedIn: 'root'
})
export class AuthService {
loginUrl = 'xxxxxxxxxx';
errorData: {};
constructor(private http: HttpClient) { }
redirectUrl: string;
login(email: string, password: string) {
var postData = {email: email, password: password};
return this.http.post<Restaurant>(this.loginUrl, postData)
.pipe(map(restaurant => {
if (restaurant) {
localStorage.setItem('currentRestaurant', JSON.stringify(restaurant));
return restaurant;
}
}),
catchError(this.handleError)
);
}
isLoggedIn() {
if (localStorage.getItem('currentRestaurant')) {
return true;
}
return false;
}
getAuthorizationToken() {
const currentRestaurant = JSON.parse(localStorage.getItem('currentRestaurant'));
return currentRestaurant.token;
}
logout() {
localStorage.removeItem('currentRestaurant');
}
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
console.error(`Backend returned code ${error.status}, ` + `body was: ${error.error}`);
}
// return an observable with a user-facing error message
this.errorData = {
errorTitle: 'Oops! Request for document failed',
errorDesc: 'Something bad happened. Please try again later.'
};
return throwError(this.errorData);
}
currRestaurant: Restaurant = JSON.parse(localStorage.getItem('currentRestaurant'));
currID = this. currRestaurant.id;
}
login.component.ts
import { Component, OnInit } from '#angular/core';
import { FormBuilder, Validators, FormGroup } from '#angular/forms';
import { Router } from '#angular/router';
import { AuthService } from '../services/auth.service';
#Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
loginForm: FormGroup;
submitted = false;
returnUrl: string;
error: {};
loginError: string;
constructor(
private fb: FormBuilder,
private router: Router,
private authService: AuthService
) { }
ngOnInit() {
this.loginForm = this.fb.group({
email: ['', Validators.required],
password: ['', Validators.required]
});
this.authService.logout();
}
get email() { return this.loginForm.get('email'); }
get password() { return this.loginForm.get('password'); }
onSubmit() {
this.submitted = true;
this.authService.login( this.email.value, this.password.value).subscribe((data) => {
if (this.authService.isLoggedIn) {
const redirect = this.authService.redirectUrl ? this.authService.redirectUrl : '/';
this.router.navigate([redirect]);
} else {
this.loginError = 'email or password is incorrect.';
}
},
error => this.error = error
);
}
}
Thanks everyone for their time
There are some mistakes:
Are you aware that you use Native localStorage, not the one you import - import { LocalStorage } from '#ngx-pwa/local-storage'; (and also it should be injected in the constructor if you want to use it, and used in asynchronous way)
if (this.authService.isLoggedIn) { will always be true, because this.authService.isLoggedIn is a function and it is not a falsy value. You probably want to execute it - if (this.authService.isLoggedIn()) {
redirectUrl is always undefined because your provided snippets does not assign it any value.

Angular 4: reactive form control is stuck in pending state with a custom async validator

I am building an Angular 4 app that requires the BriteVerify email validation on form fields in several components. I am trying to implement this validation as a custom async validator that I can use with reactive forms. Currently, I can get the API response, but the control status is stuck in pending state. I get no errors so I am a bit confused. Please tell me what I am doing wrong. Here is my code.
Component
import { Component,
OnInit } from '#angular/core';
import { FormBuilder,
FormGroup,
FormControl,
Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { EmailValidationService } from '../services/email-validation.service';
import { CustomValidators } from '../utilities/custom-validators/custom-validators';
#Component({
templateUrl: './email-form.component.html',
styleUrls: ['./email-form.component.sass']
})
export class EmailFormComponent implements OnInit {
public emailForm: FormGroup;
public formSubmitted: Boolean;
public emailSent: Boolean;
constructor(
private router: Router,
private builder: FormBuilder,
private service: EmailValidationService
) { }
ngOnInit() {
this.formSubmitted = false;
this.emailForm = this.builder.group({
email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ]
});
}
get email() {
return this.emailForm.get('email');
}
// rest of logic
}
Validator class
import { AbstractControl } from '#angular/forms';
import { EmailValidationService } from '../../services/email-validation.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
export class CustomValidators {
static briteVerifyValidator(service: EmailValidationService) {
return (control: AbstractControl) => {
if (!control.valueChanges) {
return Observable.of(null);
} else {
return control.valueChanges
.debounceTime(1000)
.distinctUntilChanged()
.switchMap(value => service.validateEmail(value))
.map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
});
}
}
}
}
Service
import { Injectable } from '#angular/core';
import { HttpClient,
HttpParams } from '#angular/common/http';
interface EmailValidationResponse {
address: string,
account: string,
domain: string,
status: string,
connected: string,
disposable: boolean,
role_address: boolean,
error_code?: string,
error?: string,
duration: number
}
#Injectable()
export class EmailValidationService {
public emailValidationUrl = 'https://briteverifyendpoint.com';
constructor(
private http: HttpClient
) { }
validateEmail(value) {
let params = new HttpParams();
params = params.append('address', value);
return this.http.get<EmailValidationResponse>(this.emailValidationUrl, {
params: params
});
}
}
Template (just form)
<form class="email-form" [formGroup]="emailForm" (ngSubmit)="sendEmail()">
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<fieldset class="form-group required" [ngClass]="{ 'has-error': email.invalid && formSubmitted }">
<div>{{ email.status }}</div>
<label class="control-label" for="email">Email</label>
<input class="form-control input-lg" name="email" id="email" formControlName="email">
<ng-container *ngIf="email.invalid && formSubmitted">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Please enter valid email address.
</ng-container>
</fieldset>
<button type="submit" class="btn btn-primary btn-lg btn-block">Send</button>
</div>
</div>
</form>
There's a gotcha!
That is, your observable never completes...
This is happening because the observable never completes, so Angular does not know when to change the form status. So remember your observable must to complete.
You can accomplish this in many ways, for example, you can call the first() method, or if you are creating your own observable, you can call the complete method on the observer.
So you can use first()
UPDATE TO RXJS 6:
briteVerifyValidator(service: Service) {
return (control: AbstractControl) => {
if (!control.valueChanges) {
return of(null);
} else {
return control.valueChanges.pipe(
debounceTime(1000),
distinctUntilChanged(),
switchMap(value => service.getData(value)),
map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
})
).pipe(first())
}
}
}
A slightly modified validator, i.e always returns error: STACKBLITZ
OLD:
.map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
})
.first();
A slightly modified validator, i.e always returns error: STACKBLITZ
So what I did was to throw a 404 when the username was not taken and use the subscribe error path to resolve for null, and when I did get a response I resolved with an error. Another way would be to return a data property either filled width the username or empty
through the response object and use that insead of the 404
Ex.
In this example I bind (this) to be able to use my service inside the validator function
An extract of my component class ngOnInit()
//signup.component.ts
constructor(
private authService: AuthServic //this will be included with bind(this)
) {
ngOnInit() {
this.user = new FormGroup(
{
email: new FormControl("", Validators.required),
username: new FormControl(
"",
Validators.required,
CustomUserValidators.usernameUniqueValidator.bind(this) //the whole class
),
password: new FormControl("", Validators.required),
},
{ updateOn: "blur" });
}
An extract from my validator class
//user.validator.ts
...
static async usernameUniqueValidator(
control: FormControl
): Promise<ValidationErrors | null> {
let controlBind = this as any;
let authService = controlBind.authService as AuthService;
//I just added types to be able to get my functions as I type
return new Promise(resolve => {
if (control.value == "") {
resolve(null);
} else {
authService.checkUsername(control.value).subscribe(
() => {
resolve({
usernameExists: {
valid: false
}
});
},
() => {
resolve(null);
}
);
}
});
...
I've been doing it slightly differently and faced the same issue.
Here is my code and the fix in case if someone would need it:
forbiddenNames(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
setTimeout(() => {
if (control.value.toUpperCase() === 'TEST') {
resolve({'nameIsForbidden': true});
} else {
return null;//HERE YOU SHOULD RETURN resolve(null) instead of just null
}
}, 1);
});
return promise;
}
I tries using the .first(). technique described by #AT82 but I didn't find it solved the problem.
What I eventually discovered was that the form status was changing but it because I'm using onPush, the status change wasn't triggering change detection so nothing was updating in the page.
The solution I ended up going with was:
export class EmailFormComponent implements OnInit {
...
constructor(
...
private changeDetector: ChangeDetectorRef,
) {
...
// Subscribe to status changes on the form
// and use the statusChange to trigger changeDetection
this.myForm.statusChanges.pipe(
distinctUntilChanged()
).subscribe(() => this.changeDetector.markForCheck())
}
}
import { Component,
OnInit } from '#angular/core';
import { FormBuilder,
FormGroup,
FormControl,
Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { EmailValidationService } from '../services/email-validation.service';
import { CustomValidators } from '../utilities/custom-validators/custom-validators';
#Component({
templateUrl: './email-form.component.html',
styleUrls: ['./email-form.component.sass']
})
export class EmailFormComponent implements OnInit {
public emailForm: FormGroup;
public formSubmitted: Boolean;
public emailSent: Boolean;
constructor(
private router: Router,
private builder: FormBuilder,
private service: EmailValidationService
) { }
ngOnInit() {
this.formSubmitted = false;
this.emailForm = this.builder.group({
email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ]
});
}
get email() {
return this.emailForm.get('email');
}
// rest of logic
}

Angular - When observable is complete, do something

I have a 2 part process that I am working with.
Component 1 allows me to import a list of usernames and submit it to a service. That service then returns the user profile data which I use in Component 2.
My issue is that I am trying to do something when I receive the data back from the observable I am subscribed to but it doesn't appear to be firing.
Component 1:
import { Component, EventEmitter, NgModule, OnInit, Output } from '#angular/core';
import { FormControl, FormGroup, FormBuilder, Validators } from '#angular/forms';
import { MassEmpService } from "app/mass-change/shared/mass.service";
import { Observable } from 'rxjs/Observable';
#Component({
selector: 'app-import-list',
templateUrl: './import-list.component.html',
styleUrls: ['./import-list.component.css'],
})
export class ImportListComponent implements OnInit {
// Define the data types used for the import list
importForm: FormGroup;
message: {};
error: string;
constructor(
private fb: FormBuilder,
private _massEmpService: MassEmpService
) {
}
ngOnInit() {
this.createForm();
}
// Generate the form
createForm() {
this.importForm = this.fb.group({
dataType: ['-1', Validators.required],
data: ['', Validators.required]
});
}
// Process the data for the form
onProccess(data) {
// Define our vars
let i: number;
const dat: string = data.data.split('\n');
const dataType: string = data.dataType;
const validArray = [];
const invalidArray = [];
// Loop over each line
for (i = 0; i < dat.length; i++) {
// Validate our data point
if (this.validateData(dataType, dat[i])) {
validArray.push(dat[i]);
} else {
invalidArray.push(dat[i]);
}
}
// Do we have any invalid data?
if (invalidArray.length) {
this.renderMessage('danger', 'fa-warning', 'Oops! Please check for invalid data.', false);
} else {
// Receive the data based on the imported criteria.
this._massEmpService.processImport(dataType, validArray)
.subscribe(
data => { this._massEmpService.fetchImportedResults(data); },
error => { this.error = error.statusText; }
);
}
}
... Other Code Here ...
}
Component 2:
export class EmployeeSelectionComponent implements OnInit {
// Define our search results
public searchResults: ImportResults[] = [];
constructor(
private _massEmpService: MassEmpService
) {
}
ngOnInit() {
this.fetchData();
}
fetchData() {
// Push our results to the array if they don't already exist
this._massEmpService.importedResults
.subscribe(
data => {
this.searchResults.push(...data);
console.log('I made it here');
},
() => {
console.log('.. but not here');
}
);
}
}
Service:
import { Injectable } from '#angular/core';
import { Http, Response, Headers } from '#angular/http';
import { RouterLink } from '#angular/router';
import { FrameworkService } from '#aps/framework';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
#Injectable()
export class MassEmpService {
// API URL
baseUrl = 'https://internal/api';
// Define the token headers for the service calls
headers: Headers = new Headers({
"Authorization": this._frameworkService.getSessionInfo().token
});
// Create a subject to observe the results and changes over time
public importedResults = new Subject<any>();
public transitionFields = new Subject<any>();
constructor(
private _http: Http,
private _frameworkService: FrameworkService
) { }
// Given a dataset, return the users based on data points submitted
processImport(dataType, dataSet): Observable<any> {
return this._http.post(this.baseUrl + '/fetchEmployeesFromImport', { "dataType": dataType, "data": dataSet }, { "headers": this.headers })
.map((result: Response) => result.json())
.share()
.catch(this.handleError);
};
// Pass the data received from the import process through our subject to observe
fetchImportedResults(data){
this.importedResults.next(data);
}
}
The Question:
In component 2, I am trying to check when I get data back so I can do something else in that component. I don't seem to reach the completed part of the observable though.
Any thoughts as to what I am doing wrong?
The first part of the problem lies in this snippet:
this._massEmpService.importedResults
.subscribe(
data => {
this.searchResults.push(...data);
console.log('I made it here');
},
() => {
console.log('.. but not here');
}
);
The second callback you are passing is for error notifications - not completion notifications. You will need to pass an additional callback to handle completion notifications.
The second part of the problem is that importedResults is a Subject and as such won't complete until its complete method is called. And there is no indication in the snippets that you are calling that method.

Angular 2.3.1 async custom validator promise doesn't resolve

I've got a sign up form and I want to check that if the username is already taken or not. To achieve this I'm using promises now.
My sign up component looks like this:
import { Component, OnInit } from '#angular/core';
import { FormControl, FormBuilder, FormGroup, Validators } from '#angular/forms';
import { UserService } from '../shared/user.service'
#Component({
selector: 'app-auth',
providers: [],
templateUrl: './auth.component.html',
styleUrls: ['./auth.component.css']
})
export class AuthComponent implements OnInit {
// Tabs for log in or sign up
tab = 'signup';
// Sign up form
signUpForm: FormGroup;
// Log in form
logInForm: FormGroup;
constructor(private formBuilder: FormBuilder, private ussr: UserService) {
this.signUpForm = this.formBuilder.group({
'username': [null, [
Validators.required, Validators.minLength(4), Validators.maxLength(12), this.ussr.getUserNameFromServer
]],
'email': '',
'password': '' });
this.logInForm = this.formBuilder.group({ 'username': '', 'password': '' });
}
ngOnInit() {
}
activeTab(tab: string) {
this.tab = tab;
}
signUpSubmit(value: any) {
console.log(value);
}
}
And the UserService:
import { Injectable } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { FormControl } from '#angular/forms';
import 'rxjs/add/operator/map';
#Injectable()
export class UserService {
constructor (private http: Http) {}
private extractData (res: Response) {
let body = res.json();
return body || { };
}
getUserNameFromServer = (c: FormControl) => {
return new Promise (
(resolve, reject) => {
this.http.get('https://jsonplaceholder.typicode.com/users/1')
.map(this.extractData)
.subscribe(
(res: any) => {
if (c.value == res.username) {
console.log('taken')
resolve({'usernameTaken': true})
} else {
console.log('is not taken')
resolve(null)
}
},
err => { console.log(err) }
)
}
);
}
}
I already read some blog posts about this topic, and I also checked two SO questions (1, 2), but I can't get it to work.
The service successfully got the server's answer, but when I call it inside the component's validator, the form is going to be invalid every time.
In the examples above, they just call it in the validator section, and I guess the ng2 do the rest of the work in the background, or am I wrong? How is the validator got the promise's value?
The problem was that I insert the async custom validator into the sync validator section. Which means the 2nd param is for the sync validators, the 3rd one for the async ones.
This works now:
'username': [null,
[ Validators.required, Validators.minLength(4), Validators.maxLength(12) ],
[ this.ussr.getUserNameFromServer ]
],

Categories

Resources