Merim
Merim

Reputation: 1363

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.

Upvotes: 9

Views: 7940

Answers (3)

Kliment Ru
Kliment Ru

Reputation: 2137

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

Upvotes: 4

Daniel Netzer
Daniel Netzer

Reputation: 2232

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.

Upvotes: 2

bygrace
bygrace

Reputation: 5988

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.

Upvotes: 2

Related Questions