Showing the loading spinner icon on all components - javascript

I have written the code to show the loading spinner on all components when any event is triggered. It works fine on a single component but the issue with it, I have to show the same loading spinner on the around multiple components when certain event is triggered. See below code:
tasks() {
this.handler.activateLoader();
this.tasksService.get(this.page, this.pageSize).subscribe(results => {
this.handler.hideLoader();
if (this.handler.handle(results)) {
return;
}
this.tasksRes = results['data'];
for (let i = 0; i < this.tasksRes.length; i++) {
if (this.tasksRes[i].status == 'In_progress' && this.tasksRes[i].eventType == 'Sync' &&
this.tasksRes[i].entityId == this.id) {
this.progressFlag = true;
break;
} else {
this.progressFlag = false;
}
}
this.length = results['totalElements'];
}, error => {
this.handler.hideLoader();
this.handler.error(error);
});
}
connect() {
let source = new EventSource('/api/v1/events/register');
source.addEventListener('message', message => {
this.tasks();
});
}
And on ngOnInit(), I have called these 2 methods as below then its working fine.
ngOnInit() {
this.tasks();
this.connect();
}
The actual requirement is when I run a particular event the button is going to be disabled and at the same time the spinner loading will come. I have achieved this one. But how to show the same spinner on multiple components so that the user can know that the task is running.
This is how I am showing the loading spinner. See below:
<span class="text-warning pull-right" *ngIf="progressFlag">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</span>
In my code, I have many components at around 17-18 where I need to show the loading spinner. If I want to show the spinner globally means I can show it on either header and footer component which is common to my entire template. Can any one provide any ideas on it.
Thanks.

Please search keyword HttpInterceptor learn details. One simple example below:
// siteHttpInterceptor.ts
import { Injectable } from '#angular/core';
import { HttpRequest, HttpInterceptor, HttpHandler, HttpEvent, HttpResponse } from '#angular/common/http';
import { throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { LoadingService } from './loading.service';
#Injectable()
export class SiteHttpInterceptor implements HttpInterceptor {
constructor(private loadingService: LoadingService){}
intercept(request: HttpRequest<any>, httpHandler: HttpHandler): Observable<any> {
/* Start loading here */
this.loadingService.startLoading();
return httpHandler.handle(request).pipe(
tap((event: HttpEvent<any>) => {
/* End loading */
this.loadingService.endLoading();
},
(err: any) => {
/* End loading */
this.loadingService.endLoading();
}),
catchError(err => {
return throwError(err);
})
);
}
}
//loading.service.ts LoadingService base on Ionic framework, you can instead it
import { Injectable } from '#angular/core';
import { LoadingController } from '#ionic/angular';
#Injectable({
providedIn: 'root'
})
export class LoadingService {
private loaders = [];
//sometimes, the request so quickly then close event earlier than open loading bar
private badLoaders = 0;
constructor(
private loadingController: LoadingController
) {
}
async startLoading() {
if (this.badLoaders > 0) {
this.badLoaders --;
} else {
await this.loadingController.create({
message: 'Loading ...',
}).then(loader => {
this.loaders.push(loader);
loader.present().then(() => {
//if it is bad loader, close
if (this.badLoaders > 0) {
this.badLoaders --;
this.endLoading();
}
});
});
}
}
endLoading() {
let loader = this.loaders.pop();
if (loader) {
loader.dismiss();
} else {
// it is mean close event earlier
this.badLoaders ++;
}
}
}
Use it then you not need manage loader handle each request method.

Put your spinner at the main component.. in most cases its the AppComponent
Then put a these on your shared service
private LoadingStatus = new Subject<boolean>();
// Observable string streams
IsLoading$ = this.LoadingStatus.asObservable();
// Service message commands
triggerLoading(status: boolean) {
this.LoadingStatus.next(mission);
}
Then at your sender component call triggerLoading(true) or triggerLoading(false) from the service and subscribe at your main component (AppComponent):
this.shareService.IsLoading$.subscribe( data => progressFlag = data )
Or Add your logic as this:
this.shareService.IsLoading$.subscribe(
data => {
if(data) {
// start loading logic here
} else {
// end loading logic here
}
}
)
Source: Angular - Component Interaction

Related

How to display a navbar component after logging in without reloading the page in angular 12

My landing page does not show a navbar, but I want to display a navbar after logging in successfully. Currently, I'm able to show navbar if I do a full page reload after logging in successfully. I'm very sure that there is a better way than this approach.
app.component.html
<app-navbar></app-navbar>
<router-outlet></router-outlet>
login.component.ts
login(){
this.credentials = this.myForm.value;
if(this.credentials){
this.loginService.authenticate(this.credentials)
.subscribe(data => {
this.storageService.setLocalStorageItem('auth', JSON.stringify(data));
this.dataService.global.showNav = true;
this.sharedService.getProjectMetadata()
.subscribe(metadata => {
this.storageService.setLocalStorageItem('projectMetaData', JSON.stringify(metadata));
this.router.navigate(['/home']);
})
}, err => console.log(err));
} else {
console.log('Please enter your username and password');
}
}
data.service.ts
import { Injectable } from '#angular/core';
import { Subject, Subscription } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { IGlobal, IMessage } from '../../Shared/interfaces';
import { MessageCallback } from '../../Shared/types';
#Injectable({
providedIn: 'root'
})
export class DataService {
constructor() { }
date: string = (new Date()).toString();
global: IGlobal = {
showNav: false,
sessionTimedOut: false,
timezone: this.date.substring(this.date.indexOf('GMT')),
projectMetaData: {
name: ''
},
isAdmin: false,
auth: {
roles: {
admin: false,
developer: false
}
}
}
private handler: Subject<IMessage> = new Subject<IMessage>();
broadcast(type: string, payload: any){
this.handler.next({type, payload});
}
subscribe(type: string, callback: MessageCallback): Subscription {
return this.handler.pipe(filter(message => message.type === type), map(message => message.payload))
.subscribe(callback);
}
}
navbar.component.html
<mat-toolbar fxLayout="row" color="primary" *ngIf='showNavbar'></mat-toolbar>
navbar.component.ts
export class NavbarComponent implements OnInit {
user: IAuth = {};
showNavbar: boolean;
progressbar: number = 0;
constructor(
private storageService: StorageService,
private dataService: DataService
) {
this.showNavbar = this.dataService.global.showNav;
}
ngOnInit(): void {
this.user = JSON.parse(this.storageService.getLocalStorageItem('auth'));
if(this.user){
this.showNavbar = true;
}
}
}
Please help me out. Your help is highly appreciated. Thank you.
The problem lies here,
once authentication is completed successfully in login() function, it is not communicated to navbar.component.ts
showNavbar in navbar.component.ts is used to display/hide navbar template.
Though dataService.global.showNav is set to true, it will not trigger change detection in navbar components. Since it is copied to `showNavbar' only in constructor.
So before login, navbar is already loaded, probably with showNavbar evaluated as false, and never recomputed until page reload.
During pagereload value is read from localStorage which provides latest value to showNavbar
I have two suggestions{S1,S2} to fix this.
S1.
1.broadcast via subject from login component about successful login status
2.And subscribe for that status in navbar component and upon subscription , control rendering of navbar template
3. Looks like as per your business logic,broadcast and subscribe functions in dataservice does that for you in IMessage type subject.
4. Consider example code below and update according to your application.
For eg:
login.component.ts
this.dataService.broadcast('authSuccess',{auth:'successful'})
in navbar.component.ts
OnInit() {
this.dataService.subscribe('authSuccess',setShowNavbar);
}
setShowNavbar() {
this.showNavbar=true;
}
S2:
This is not a clean approach and difficult for tracking, but it works for quick and dirty solutions.
navbar.component.html
<mat-toolbar fxLayout="row" color="primary" *ngIf="dataService.global.showNav"></mat-toolbar>
This case will run change detection whenever value in dataService.global.showNav is updated and evaluate ngIf accordingly.
Note: Better to add a small working proto in stackblitz/jsfiddle/codesandbox etc when posting questions in public forum. So it will be easier for everyone to identify exact problem and arrive on specific solutions quickly.

Angular setTimeout running too many times

I have a log viewer component implemented in two or more other components. This log viewer uses a setTimeout to create an interval loop to fetch data from a file. My problem is since this component is imported in other components the timer runs for every component separately thus making multiple file reads per second.
Can this be avoided and run the timer only once regardless of the number of components using this component?
Here's the code for the log viewer component where the setTimeout interval is created:
import { Component, ElementRef, OnInit, ViewChild } from '#angular/core';
import { LogsService } from '../../services/logs.service';
import { HelperService } from '../../services/helper.service';
#Component({
selector: 'app-logger',
templateUrl: './logger.component.html',
styleUrls: ['./logger.component.scss']
})
export class LoggerComponent implements OnInit {
#ViewChild('scroller', {static: false}) scroller: ElementRef;
logClassName: string = 'logs shadow close';
logs: string = '';
logTS: number = 0;
logTimer;
scrollTop: number = 0;
constructor(
private logsService: LogsService,
private h: HelperService
){}
ngOnInit(): void {
this.getLogs();
}
ngOnDestroy(): void {
if (this.logTimer) window.clearTimeout(this.logTimer);
}
toggle(type){
switch (type)
{
case 'open': this.logClassName = 'logs shadow open'; break;
case 'close': this.logClassName = 'logs shadow close'; break;
case 'full': this.logClassName = 'logs shadow full'; break;
}
}
getLogs(){
this.logsService.fetch(this.logTS).subscribe(
response => {
this.logs += response.data.join('');
this.logTS = response.ts;
window.setTimeout(() => {
this.scrollTop = this.scroller.nativeElement.scrollHeight;
}, 100);
this.setLogTimer();
},
error => {
this.h.lg('unable to fetch logs', 'error');
this.logs = '<p>Unable to fetch logs</p>';
this.setLogTimer();
}
);
}
setLogTimer(){
if (this.logTimer) window.clearTimeout(this.logTimer);
this.logTimer = window.setTimeout(() => {
this.getLogs();
}, 1000);
}
}
The problem is this.logTimer is associated with component instance and it will be different for each instance and never get cancelled solution would be to use some shared service to perform your file read like
#Injectable(provideIn:'root')
export class SomeService{
setLogTimer(){
if (this.logTimer) window.clearTimeout(this.logTimer);
this.logTimer = window.setTimeout(() => {
this.getLogs();
}, 1000);
}
}
Then use this service in your component like
this.someService.setLogTimer()
To solve this problem Angular has Singleton services. You can move the setLogTimer() and logTimer in your LogsService.

Angular 6 - Back button press trigger more than once

I have the following code to detect the back button press using angular 6.
import { Location } from '#angular/common';
export class ProductsComponent implements OnInit {
constructor( private location: Location){
this.handleBackButtonPress();
}
handleBackButtonPress() {
this.subscribed = true;
this.location.subscribe(redirect => {
if (redirect.pop === true) {
alert('this is a backbutton click');
}
});
}
}
This is working and we got alert on back button press. The problem is If we visit the same page more than once it will trigger the alert with the number of time we visited the route with the same component.
Note:
I have checked for a solution like this.location.unsubscribe(), But failed to find a function like that for location.
You just need to unsubscribe when the component is destroyed by the ngOnDestroy lifecycle hook.
import { Location } from '#angular/common';
import { SubscriptionLike } from 'rxjs';
export class ProductsComponent implements OnInit, OnDestroy {
public subscription: SubscriptionLike;
constructor(private location: Location){
this.handleBackButtonPress();
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
handleBackButtonPress() {
this.subscription = this.location.subscribe(redirect => {
if (redirect.pop === true) {
alert('this is a backbutton click');
}
});
}
}
As mentioned by briosheje in the comments the lifecycle hook does not run on browser refreshes. For that you'll need to handle the unsubscription on the document's onbeforereload event.
The problem, I analyzed here is, every time whenever constructor will run. It will call your function for sure. So you have to check whether this function has been run previously or not.
Simplest answer is
constructor( private location: Location){
const PopCalled = localStorage.getItem('PopCalled')
if(!PopCalled)
this.handleBackButtonPress();
}
handleBackButtonPress() {
this.subscribed = true;
this.location.subscribe(redirect => {
if (redirect.pop === true) {
localStorage.setItem('PopCalled', true);
alert('this is a backbutton click');
}
});
}
Basically, you have to manage the state of PopCalled its up to you which way you want to choose, as per my knowledge this is the simplest way.

Right approach to display success, error messages via NGRX

I know two solutions for this problem, first one is to keep message in your state which doesn't seem good, second one is to subscribe to an ActionSubject which I currently use to display messages.
Is there any other solution for this ? Also how to set a CSS class in template, not in component ?
Here is my example:
this.actionSubject.subscribe(action => {
if (action.type === fromActions.LOGIN_SUCCESS) {
this.message$ = action.payload.message;
this.messageClass = 'alert alert-success';
}
if (action.type === fromActions.LOGIN_FAILURE) {
this.message$ = action.payload.error.message;
this.messageClass = 'alert alert-danger';
this.LoginForm.reset();
}
})
It seems too long, not DRY, I should do this in every component where I expect to have a message.
Example from original docks https://github.com/ngrx/effects/blob/master/docs/intro.md
Create an AuthEffects service that describes a source of login actions:
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Actions, Effect } from '#ngrx/effects';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class AuthEffects {
constructor(
private http: Http,
private actions$: Actions
) { }
#Effect() login$ = this.actions$
// Listen for the 'LOGIN' action
.ofType('LOGIN')
// Map the payload into JSON to use as the request body
.map(action => JSON.stringify(action.payload))
.switchMap(payload => this.http.post('/auth', payload)
// If successful, dispatch success action with result
.map(res => ({ type: 'LOGIN_SUCCESS', payload: res.json() }))
// If request fails, dispatch failed action
.catch(() => Observable.of({ type: 'LOGIN_FAILED' }))
);
}
Provide your service via EffectsModule.run to automatically start your effect:
import { EffectsModule } from '#ngrx/effects';
import { AuthEffects } from './effects/auth';
#NgModule({
imports: [
EffectsModule.run(AuthEffects)
]
})
export class AppModule { }
Note: For effects that depend on the application to be bootstrapped (i.e. effects that depend on the Router) use EffectsModule.runAfterBootstrap. Be aware that runAfterBootstrap will only work in the root module.
Else you can look about using guard with effect here: https://toddmotto.com/preloading-ngrx-store-route-guards
There are many approaches that you could take to move the logic to the template.
Here is one approach:
// component
public isSuccess = merge(
this.actions.pipe(filter(x => x.type === 'SUCCESS'),mapTo(true)),
this.actions.pipe(filter(x => x.type === 'FAILURE'),mapTo(false))
);
public message = merge(
this.actions.pipe(filter(x => x.type === 'SUCCESS'),map(x => x.payload.message)),
this.actions.pipe(filter(x => x.type === 'FAILURE'),map(x => x.payload.error.message))
);
// template
<div class="alert"
[class.alert-success]="isSuccess | async"
[class.alert-danger]="!(isSuccess | async)">
{{ message | async}}
</div>
Here is another:
<div class="alert alert-success"
*ngIf="(action | async).type === 'SUCCESS'">
{{ (action | async).payload.message }}
</div>
<div class="alert alert-danger"
*ngIf="(action | async).type === 'FAILURE'">
{{ (action | async).payload.error.message }}
</div>
As far as the form reset I guess you would still need a subscription. If you are using effects then you could do actions.ofType(...) instead of the filter operator. I don't know what version of rxjs you are using so I am using the pipeable syntax.
If you will be doing this same thing in multiple places then I would suggest setting up a component that encapsulated this logic.
i'll merge #Kliment Ru and #bygrace answer's and give an example of something I built to encapsulate the logic of global messaging with snackbars (Material) as a dispatchable action.
message.action.ts
import { Action } from '#ngrx/store';
export const MESSAGE = '[Messages] Show Message';
export class Message implements Action {
readonly type = MESSAGE;
constructor(
public payload: {
message: string;
action?: string;
duration?: number;
callback?: Function;
}
) { }
}
pretty straightforward, encapsulated the snackbar properties into a ngrx action.
message.effect.ts
import { Injectable } from '#angular/core';
import { Effect, Actions } from '#ngrx/effects';
import * as MessageActions from '../actions/message.action';
import { tap, map } from 'rxjs/operators';
import { MatSnackBar } from '#angular/material';
import { first } from 'rxjs/operators/first';
#Injectable()
export class MessageEffects {
constructor(
private actions$: Actions,
private snackBar: MatSnackBar
) { }
#Effect({ dispatch: false })
navigate$ = this.actions$
.ofType(MessageActions.MESSAGE)
.pipe(
map((action: MessageActions.Message) => action.payload),
tap(({ message, action, duration, callback }) => {
duration = duration ? duration : 3000;
// incase of an action assigned, subscribe to the snackbar, else just show the message
if (callback) {
this.snackBar.open(message, action, { duration: duration })
.onAction()
.pipe(
first()
)
.subscribe(() => {
callback();
});
} else {
this.snackBar.open(message, action, { duration: duration });
}
}));
}
the effect that listens to the action and shows the snackbar.
then when you want to use it, just do the following,
this.store.dispatch(new fromRoot.Message({ message: 'Something went wrong, please try again later' }));
a simple one-liner that encapsulate the entire logic and UI of the messages in your application, the good thing behind it is that I can change my snackbar to anything I want using any library and ill have to change the code only in one place.

RxJS Observable :'Skip is not a function' (or any other function)

I am getting a weird error with RxJS, getting a 'Method is not a function'.
I do have import the Observable library.
I already got Observable array with no errors raised.
But when I do a skip, or take,I get this error.
If I read correctly :
Returns an Observable that skips the first count items emitted by the
source Observable.
As my observable contains Article, it should skip x Article on the Observable right?
Here is the code
import { Component, OnInit } from '#angular/core';
import { Article} from '../../model/article';
import { ArticleService} from '../../model/article.service';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
#Component({
selector: 'app-liste-article',
templateUrl: './liste-article.component.html',
styleUrls: ['./liste-article.component.css']
})
export class ListeArticleComponent implements OnInit {
articles: Observable<Article[]>;
articlesPage: Observable<Article[]>;
selectedArticle: Article;
newArticle: Article;
page = 1;
itemPerPage = 2;
totalItems = 120;
constructor(private router: Router, private articleService: ArticleService) {
}
ngOnInit() {
this.articles = this.articleService.getArticles();
this.articlesPage = this.articles.skip((this.page - 1) * this.itemPerPage ).take(this.itemPerPage);
//this.articlesPage = this.articles.slice( (this.page - 1) * this.itemPerPage , (this.page) * this.itemPerPage );
}
onSelect(article: Article) {
this.selectedArticle = article;
this.router.navigate(['../articles', this.selectedArticle.id ]);
}
onPager(event: number): void {
console.log('Page event is' , event);
this.page = event;
this.articlesPage = this.articles.skip((this.page - 1) * this.itemPerPage ).take(this.itemPerPage);
console.log('tab' , this.articlesPage.count());
//this.articleService.getArticles().then(articles => this.articles = articles);
}
getArticles(): void {
this.articles = this.articleService.getArticles();
}
createArticle(article: Article): void {
//this.articleService.createArticle(article)
//.then(articles => {
// this.articles.push(articles);
// this.selectedArticle = null;
//});
}
deleteArticle(article: Article): void {
//this.articleService
// .deleteArticle(article)
// .then(() => {
// this.articles = this.articles.filter(b => b !== article);
// if (this.selectedArticle === article) { this.selectedArticle = null; }
//});
}
}
I tried to use it directly with .skip(1), but getting the same error.
I checked official documentations RxJS Observable and it looks OK to me. It seems the case is OK, as my call to the function. Both arryas are Observable so I should be able to use those function on one, and put the result in the other one right?
I don't see what is wrong here, probably a small detail, but can't see it myself (as I am starting with this, I see less details).
You'll want to import skip and take the same way as you have done with map and catch.
import 'rxjs/add/operator/skip';
import 'rxjs/add/operator/take';
For rxjs#6.5.5 I had to make the following changes:
import skip
import { skip } from "rxjs/operators";
change
return this.udpstream.asObservable().skip(1);
to
return this.udpstream.asObservable().pipe(skip(1));

Categories

Resources