Display Loading Icon on Route Resolver / Route Change - javascript

Im trying to show a loading icon while I the route resolver gets the data from the DB.
I've tried the below option:
Root Component:
_router.events.subscribe((routerEvent: RouterEvent) => {
if (routerEvent instanceof NavigationStart) {
console.log("start");
this.loading = true;
} else if (routerEvent instanceof NavigationError || NavigationCancel || NavigationEnd) {
console.log("end");
this.loading = false;
}
});
Root Component HTML:
<h1 *ngIf="loading">Loading</h1>
The loading icon does not show at all.
The following is displayed on console log on every route change:
Update:
Below is the output after applying the following changes:
public loading: boolean = true;
console.log(routerEvent);
console.log("Loading is " + this.loading);
Update 2:
app.component.html:
<div class="uk-offcanvas-content">
<h1>{{loading}}</h1>
<h1 *ngIf="loading">Loading</h1>
<app-root-nav></app-root-nav>
<app-notifications></app-notifications>
<router-outlet></router-outlet>
</div>
app.component.ts:
import {Component, OnInit, AfterViewInit} from '#angular/core';
import {AuthenticationService} from "../../authentication/services/authentication.service";
import {Router, Event, NavigationStart, NavigationEnd, NavigationCancel, NavigationError} from "#angular/router";
import {RouterEvent} from "#angular/router";
import UIkit from 'uikit'
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit {
isLoggedIn: boolean;
public loading: boolean = true;
UIkit: any;
constructor(private _router: Router, private _authService: AuthenticationService) {
_router.events.subscribe((routerEvent: RouterEvent) => {
if (routerEvent instanceof NavigationStart) {
this.loading = true;
console.log(routerEvent);
console.log("Loading is " + this.loading);
} else if (routerEvent instanceof NavigationError || NavigationCancel || NavigationEnd) {
this.loading = false;
}
});
}
ngAfterViewInit() {
}
ngOnInit() {
UIkit.notification({
message: 'my-message!',
status: 'primary',
pos: 'top-right',
timeout: 5000
});
}
}

The problem here is pretty simple but easy to miss. you're improperly checking for the router event type, it should be like :
else if (routerEvent instanceof NavigationError || routerEvent instanceof NavigationCancel || routerEvent instanceof NavigationEnd)
the way you have it is just returning true always because your second clause is basically "or if NavigationCancel is truthy", which it is by definition since it's an existing type. so loading sets to false immediately when the route resolve starts since there are a lot of intermediate router events before NavigationEnd event and it sets to false on all of them due to the way you're checking.
plunk: https://plnkr.co/edit/7UKVqKlRY0EPXNkx0qxH?p=preview

Try this code to show a loading icon while the route resolver gets the data from the DB :
constructor(private router: Router){
router.events.subscribe(e => {
if (e instanceof ChildActivationStart) {
this.loaderservice.show();
} else if (e instanceof ChildActivationEnd) {
this.loaderservice.hide();
}
});
}

I'd similar situation and i resolved in the following way:
public loading = true;
constructor(private router: Router) {
}
public onClick(): void {
this.loading = true;
this.router.navigate(['/test']).then(_ => {
this.loading = false;
});
}
I managed navigation in the programmatically way. I put loading variable to true before start navigation and i switch its value to false when routing is finished.

Related

Property change caused by route change won't update view - *ngIf

So I have a kind of custom select bar with products-header__select expanding the list on click. To do so I created the property expanded which is supposed to describe its current state. With *ngIf I either display it or not.
It works fine clicking the products-header__select. But a click on one of the expanded list's items changes the route, the path and some other element changes, but the products-header__select remains visible.
All good, but I want to collapse the list on route change - my approach was to listen to router events and then run expanded = false when the navigation has ended. - But somehow the view won't update and the list remains expanded, even though running console.log(this.expanded) inside of the router event returns false. Why won't it update then?
View:
<div class="products-header__select" (click)="expanded = !expanded">
<ul>
<li class="basic-text__small custom-select">{{mobileCategories ? (mobileCategories[0].name | transformAllProducts) : ''}}</li>
<div class="select-options" *ngIf="expanded">
<li class="basic-text__small" *ngFor="let category of mobileCategories.slice(1, mobileCategories.length); let i = index" routerLink="/products/{{category.name.toLowerCase()}}">
{{category?.name | transformAllProducts}}
</li>
</div>
</ul>
</div>
import {Component, Input, OnInit} from '#angular/core';
import {NavigationEnd, Router, RouterEvent} from '#angular/router';
#Component({
selector: 'app-products-header',
templateUrl: './products-header.component.html',
styleUrls: ['./products-header.component.scss']
})
export class ProductsHeaderComponent implements OnInit {
expanded = false;
url: string;
$categories;
#Input() set categories(value) {
if (value) {
this.$categories = value;
this.createArrayForMobile();
this.getActiveRoute();
}
}
mobileCategories: any[];
constructor(private router: Router) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
}
});
}
ngOnInit(): void {
}
getActiveRoute() {
if (!this.mobileCategories) { return; }
const decodedUrl = decodeURI(this.url);
const index = this.mobileCategories.findIndex(item => decodedUrl.includes(item.name.toLowerCase()));
const obj = this.mobileCategories[index];
this.mobileCategories.splice(index, 1);
this.mobileCategories.unshift(obj);
}
createArrayForMobile() {
this.mobileCategories = [...this.$categories, {name: 'all'}];
}
}
That's how I use it:
<app-products-header [categories]="categories"></app-products-header>
Don't know the answer but things I would try would be:
Try subscribing to the router events in ngOnInit() {} rather than the constructor.
Try specifically calling change detection.
constructor(private router: Router, private cdr: ChangeDetectorRef) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
this.cdr.detectChanges();
}
});}
Had to wrap the router event's code with a timeout:
constructor(private router: Router) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
setTimeout(() => {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
});
}
});
}

Method some() doesn't work as expected in Angular 8

I have an Angular app and I want to add follow/unfollow functionality for users. I'm trying to add isFollowed flag, so I will be able to know if user is followed or no, and depending on that I will show 2 different buttons: Follow and Unfollow. I'm using some() method for this purposes but it doesn't work. It shows me that isFollowed flag is undefined although it should show true or false. I don't understand where the problem is, here is my HTML relevant part:
<button *ngIf="!isFollowing; else unfollowBtn" class="btn" id="btn-follow" (click)="follow(id)">Follow </button>
<ng-template #unfollowBtn><button class="btn" id="btn-follow" (click)="unFollow(id)">Unfollow</button></ng-template>
TS component relevant part:
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute } from "#angular/router";
import { AuthenticationService } from '#services/auth.service';
import { FollowersService } from '#services/followers.service';
#Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
user;
id;
followers;
isFollowing: boolean;
constructor(
private authenticationService: AuthenticationService,
private followersService: FollowersService,
private router: Router,
private route: ActivatedRoute,
) { }
ngOnInit() {
this.id = this.route.snapshot.paramMap.get("id");
this.authenticationService.getSpecUser(this.id).subscribe(
(info => {
this.user = info;
})
);
this.followersService.getFollowing().subscribe(
data => {
this.followers = data;
this.isFollowing = this.followers.some(d => d.id == this.user.id);
}
);
}
follow(id) {
console.log('follow btn');
this.followersService.follow(id).subscribe(
(data => console.log(data))
)
this.isFollowing = true;
}
unFollow(id) {
console.log('unFollow btn');
this.followersService.unFollow(id).subscribe(
(data => console.log(data))
)
this.isFollowing = false;
}
}
Any help would be appreciated.
If you want it called everytime and to make sure this.user is populated. Then you could use a forkJoin
forkJoin(
this.authenticationService.getSpecUser(this.id),
this.followersService.getFollowing()
).pipe(
map(([info, data]) => {
// forkJoin returns an array of values, here we map those values to the objects
this.user = info;
this.followers = data;
this.isFollowing = this.followers.some(d => d.id == this.user.id);
})
);
Not tested this because I didn't have time. If you make a StackBlitz we could see it in action and try from there.
Hope this helps.

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 2 Tour of Heroes doesn't work

I tried to write application based on tour of heroes.
I have Spring application which shares resources and client app which should get this data. I know that resources get to client app, but I can't print it.
import { HeroesService } from './shared/HeroesService';
import { Observable } from 'rxjs/Observable';
import { Hero } from './shared/Hero';
import { OnInit } from '#angular/core';
import { Component } from '#angular/core';
#Component({
selector: 'app',
template: require('app/app.component.html!text')
})
export class AppComponent implements OnInit {
errorMessage: string;
items: Hero[];
mode: string = 'Observable';
firstItem: Hero;
constructor(private heroesService: HeroesService) { }
ngOnInit(): void {
this.getHeroes();
console.log(this.items);
//this.firstItem = this.items[0];
}
getHeroes() {
this.heroesService.getHeroes()
.subscribe(
heroes => this.items = heroes,
error => this.errorMessage = <any>error
);
}
}
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { Hero } from './Hero';
#Injectable()
export class HeroesService {
private heroesUrl = 'http://localhost:8091/heroes';
constructor(private http: Http) { }
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
console.log(body);
return body || { };
}
private handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
In method extract data when I printed by console.log(body.data) I get undefined, but when I printed console.log(body) I get list of objects, therefore I return body instead body.data.
And when I print objects in extractData I get list of objects, but in AppComponent when I print console.log(this.items) I get undefined.
What's going on?
this.getHeroes() returns an Observable which means that you can't get data out of it unless you subscribe to it. Think about it like a magazine subscription, by calling this.getHeroes(), you have registered for the magazine but you don't actually get the magazine until it gets delivered.
In order to get a console.log of the data that comes back in AppComponent, replace the .subscribe block with the following:
.subscribe(
(heroes) =>{
console.log(heroes);
this.items = heroes;
},
error => this.errorMessage = <any>error
);
To further the magazine analogy, inside the subscribe block, you have received the magazine and here we are console logging its contents.
Hope this helps

Angular 2 binding not updating after async operation

In my Angular 2 app, I want to start by loading a number of SVGs, before starting the app proper. To do this, I first load a list of svg locations from the server, then fetch each one in turn.
I have a 'loading' property on my AppComponent thats controlling a couple of ngIfs to show/hide some Loading text. Problem is, once the svgs are all loaded, Angular doesn't update the binding in AppComponent.
Why is this? I thought zones took care of this?
The SvgLoader
import {Injectable, Output, EventEmitter, NgZone} from 'angular2/core';
import {Http, Response} from 'angular2/http';
import 'rxjs/Rx';
import {Observable} from 'rxjs/Observable';
const SVG_LIST:string = 'svg/list.json';
#Injectable()
export class SvgLoader {
#Output() state: EventEmitter<string> = new EventEmitter();
private svgs:{[file:string]:string};
constructor(private http:Http){
this.svgs = {};
}
getSvg(path:string):string {
return this.svgs[path];
}
load():void {
this.http.get(SVG_LIST)
.map(res => {
return <Array<string>> res.json().files;
})
.flatMap((files) => Observable.forkJoin(files.map(file => {
return this.http.get('svg/' + file);
})))
.catch(this.handleError)
.mergeAll()
.subscribe(
res => {
let index = res.url.indexOf('svg');
let path = res.url.substring(index);
this.svgs[path] = res.text();
},
error => console.error(error),
() => {
this.state.emit('loaded');
}
);
}
private handleError(error:Response) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}
AppComponent
export class AppComponent {
// On start, list the game sessions
private state:string;
public loading:boolean;
constructor(private svgLoader:SvgLoader){
this.loading = true;
this.state = 'joining';
}
ngOnInit():void {
this.svgLoader.state.subscribe(this.loaded);
this.svgLoader.load();
}
loaded():void {
console.log('loaded');
this.loading = false;
}
}
The template
<div>
<h1 *ngIf="loading">Loading...</h1>
<div *ngIf="!loading">
Loaded
</div>
</div>

Categories

Resources