Right approach to display success, error messages via NGRX - javascript

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.

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.

Is it possible and if ok, then how to transform data inside ngrx effect?

//app.effect.ts
import { Injectable } from '#angular/core';
import { Actions, createEffect, ofType } from '#ngrx/effects';
import {addCityToIndex, returnCityToIndex} from './reducers/form.reducer';
import { map } from 'rxjs/operators';
import { HttpService } from './services/http.service';
#Injectable()
export class AppEffects {
effect$ = createEffect(() =>
this.actions$.pipe(
ofType(addCityToIndex),
map(data => returnCityToIndex(data))
), {});
constructor(private actions$: Actions, private http: HttpService) {}
}
I need to update data, which I have in addCityToIndex (array) via http service - get property of array element, make request to service, update element and pass new array to returnCityToIndex.
Is it possible and if ok, then how to transform data inside ngrx effect?
Based on documentation
You can use a action to trigger a request and return another action witch will update your state, and your component will recive the new data in selector when the data be updated.
Ex:
login$ = createEffect(() =>
this.actions$.pipe(
ofType(LoginPageActions.login),
exhaustMap(action =>
this.authService.login(action.credentials).pipe(
map(user => AuthApiActions.loginSuccess({ user })),
catchError(error => of(AuthApiActions.loginFailure({ error })))
)
)
)
);

Observable rxjs filter

In my project, I wanted to create some sort of "Recommended Products" in each product page,
but having trouble with making my function filtering an observable.
I have tried using .pipe(filter()) in different ways, but to no use.
Basically the fucntion should filter products with the same type and id, and show them in the proper product page, but pretty much got stuck after subscribing all of my products(which is marked down below).
Much Appreciated!
import { Component, OnInit } from '#angular/core';
import { ProductService } from '../services/product.service';
import { ActivatedRoute, ParamMap, Router } from '#angular/router';
import Product from '../interfaces/product';
import { map, filter} from 'rxjs/operators';
import { Observable } from 'rxjs';
#Component({
selector: 'app-product',
templateUrl: './product.component.html',
styleUrls: ['./product.component.css']
})
export class ProductComponent implements OnInit {
recommandedProducts: Product[];
allProducts:Product[];
// allProducts:Observable< Product> = new Observable< Product>();
product: Product;
constructor(private productService: ProductService, private route: Router, private actRoute: ActivatedRoute) { }
ngOnInit() {
this.findRecommendedProducts(this.product)
};
//From ProductService:
// getProducts(){
// return this.http.get(`${this.uri}`);
// }
findRecommendedProducts(currectProduct: Product){
this.productService.getProducts().subscribe((data: Product[]) => {
this.allProducts = data;
console.log(this.allProducts)
this.recommandedProducts = this.allProducts.filter(otherProduct =>
otherProduct.type == currectProduct.type && otherProduct.id == currectProduct.id)
console.log(this.recommandedProducts);
});
};
}
A filter in rxjs is not the same as an Array.filter. In rxjs, a filter is used to emit values that pass the provided condition. So if you use a filter, based on the condition, the observable will either emit your data or return nothing.
Instead, what you need is pipe(map) along with an Array.filter. Also, as #jzzfs pointed out, your error shows currentProduct could be undefined, so you can pass a default value in your findRecommendedProducts function.
Try the below code.
findRecommendedProducts(currectProduct: Product = {} as Product) {
this.productService.getProducts()
.pipe(
map((products: Product[]) => products.filter(product => product.type == currectProduct.type && product.id == currectProduct.id))
)
.subscribe((data: Product[]) => {
this.recommandedProducts = data;
});
};
Now your subscribed data should directly return the recommendedProducts.
Looks like the currectProduct passed onto findRecommendedProducts is undefined, since the logged this.allProducts do contain values.
With that being said, notice that when you define product: Product, you've only defined its type but you have not initialized it. The default value is therefore undefined -- if I remember correctly.
So change product: Product to product: Product = {};, for instance or pass a value to it within the constructor or within ngInit.

Showing the loading spinner icon on all components

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

Angular 2 - http.get never being call

Im learning Angular 4 and have run into a problem that I cannot seem to find a solution to. Here is the context:
I have a simple app that displays info about US Presidents.
The backend is a rest API provided by webapi...this works fine.
The front end is an Angular app.
Ive distilled the problem down to 3 components, 1 data service and 1 model.
Here is the model:
export class President {
constructor(
public id: number,
public presidentName: string,
public presidentNumber: number,
public yearsServed: string,
public partyAffiliation: string,
public spouse: string) {}
}
The 3 components are
1. SearchComponent
2. HomeComponent
3. PresidentComponent
When the app bootstraps, it loads the ApplicationComponent - it is the root component:
import { Component, ViewEncapsulation } from '#angular/core';
#Component({
selector: 'my-app',
template: `
<search-component></search-component>
<home-component></home-component>
`
})
export class ApplicationComponent {}
PresidentComponent is a child component of HomeComponent. When home component loads, it makes an http call to the api to get a list of presidents and renders 1 presidentComponent for each row returned. This works fine.
What Im trying to do is implement a search feature where the dataService exposes an EventEmitter and provides the search method as shown here:
import { Injectable, EventEmitter, Output } from '#angular/core'
import { President } from '../models/President'
import { Http } from '#angular/http';
import 'rxjs/add/operator/toPromise';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class DataService {
constructor(private http: Http) {
}
searchEvent: EventEmitter<any> = new EventEmitter();
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any): Observable<President[]> {
let encParams = encodeParams(params);
console.log(encParams);
return this.http
.get(this.presidentUrl, {search: encParams})
.map(response => response.json());
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this.http.get(this.presidentUrl)
.map(response => response.json());
}
}
/**
* Encodes the object into a valid query string.
* this function is from the book Angular2 Development with TypeScript
*/
function encodeParams(params: any): URLSearchParams {
return Object.keys(params)
.filter(key => params[key])
.reduce((accum: URLSearchParams, key: string) => {
accum.append(key, params[key]);
return accum;
}, new URLSearchParams());
}
The Search Component houses the search form and when the search button is clicked, it executes the onSearch() function and calls emit on the data service:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.searchEvent.emit(this.formModel.value);
}
}
Then, in the HomeComponent, I want to subscribe to this event and execute a search via the dataservice when it fires:
ngOnInit(): void {
//when component loads, get list of presidents
this.dataService.getPresidents()
.subscribe(
presidents => {
console.log('sub');
this.presidents = presidents;
},
error => console.error(error)
)
//when search event is fired, do a search
this.dataService.searchEvent
.subscribe(
params => {
console.log('in home.ts subscribe ' + JSON.stringify(params));
this.result = this.dataService.search(params);
},
err => console.log("cant get presidents. error code: %s, URL: %s"),
() => console.log('done')
);
}
When I run this in the browser, everything works except the http call is never executed. If I subscribe() to the http.get call in the dataservice itself, it executes but why should I have to do that when I have a subscription being setup on the HomeComponent?
I want to handle the Observable in the HomeComponent and update the list of presidents that is being displayed in the UI based on the search result.
Any advice is greatly appreciated.
The entire idea of using EventEmitter in the service is not right. The EventEmitter should be used with #Output properties to send data from the child component to its parent.
Even though the EventEmitter is a subclass of the Subject, you shouldn't be using it in services. So inject the service into your component, subscribe to its observable in the component, and emit an event using EventEmitter to the parent component if need be.
In the code this.result = this.dataService.search(params);, result is an observable. You have not made a subscription.
In that case you should have used the async pipe to display the data.
Why not use Subject from rxjs. Here is what i am proposing:
DataService:
import { Observable, Subject } from "rxjs";
import 'rxjs/add/operator/catch';
#Injectable()
export class DataService {
private _dataSubject = new Subject();
constructor(private http: Http) {
this.http.get(this.presidentUrl)
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
);
}
// simple property for the url to the api
get presidentUrl {
return "http://localhost:51330/api/presidents";
}
search(params: any){
let encParams = encodeParams(params);
console.log(encParams);
this.http
.get(this.presidentUrl, {search: encParams})
.map(response => this._dataSubject.next(response.json()))
.catch(err => this._dataSubject.error(err));
}
getParties(): String[] {
return ['Republican', 'Democrat', 'Federalist', 'Whig', 'Democratic-Republican', 'None'];
}
getPresidents(): Observable<President[]> {
return this._dataSubject;
}
SearchComponent:
onSearch(){
if(this.formModel.valid){
console.log('emitting event from search.ts');
this.dataService.search(this.formModel.value);
}
}
With these modifications you should be able to have only 1 subscriber in homeCompoent and then get new data emitted every time onSearch() is called.

Categories

Resources