devyumao / angular2-busy

Show busy/loading indicators on any promise, or on any Observable's subscription.
http://devyumao.github.io/angular2-busy/demo/asset/
MIT License
314 stars 102 forks source link

How to use this with a boolean (like with ngrx/store)? #48

Closed tderoo closed 7 years ago

tderoo commented 7 years ago

We are using ngrx/store in our application which has a central state upon which reducer actions mutate this state async via the ngrx/effects. So in the components we do not have the http promises directly. Instead we have an observable state in which we also have a isLoading boolean. What I would like to do is to bind a component to this boolean and display only if it resolves to true (might even do that multiple times for some pages). Does anyone know a clean solution for this scenario?

function loadCrewDutyListStartedHandler(state: DutyState,
    payload: { period: TimePeriod, crewId: string }) {
    return { ...state, isLoading: true };
}

function loadCrewDutyListSuccessHandler(state: DutyState,
    payload: { params: { period: TimePeriod, crewId: string }, result: Array<CrewDuty>}) {
    return { ...state, isLoading: false, crewDutyList: payload.result };
}

So how would you attach the busy indicator if you can only bind to the observable/promise but not tot the returned boolean value? In the code below it does not help to bind to the duties$ variable since it will resolve directly (based on the current state).

@Component({
    selector: 'resource-duty-list',
    templateUrl: 'duty-list.component.html'
})
export class DutyListComponent implements OnInit, OnDestroy {
    duties$: Observable<Array<CrewDuty>>;

    constructor(private store: Store<IAppState>) {}

    ngOnInit(): void {
        this.duties$ = this.store.map(s => s.resource.duty.crewDutyList);
        _etc._
swargolet commented 7 years ago

I created a reducer to handle showing and hiding ngBusy. This way state is handled by the store. I am then able to use this very easily by any other component by just changing the visible state of ngBusy through the store by either dispatching events to it or using effects.

Below code follows the style provided by ngrx in their example-app.

busy.reducer.ts

import { ActionReducer, Action } from '@ngrx/store';
import { createSelector } from 'reselect';

import * as busyActions from './busy.actions';

export interface State {
  visible: boolean;
};

export const initialState: State = {
  visible: false
};

export function reducer(state = initialState, action: busyActions.Actions): State {
  switch (action.type) {
    case busyActions.SHOW: {
      return {visible: true}
    }

    case busyActions.HIDE: {
      return {visible: false}
    }

    default: {
      return state;
    }
  }
}

export const isVisible = (state: State) => state.visible;

busy.actions.ts

import { Action } from '@ngrx/store';

export const SHOW = '[Busy] Show';
export const HIDE = '[Busy] Hide';

export class ShowAction implements Action {
 readonly type = SHOW;

 constructor() { }
}

export class HideAction implements Action {
  readonly type = HIDE;

  constructor() { }
}

export type Actions
  = ShowAction
  | HideAction

Within reducers index.ts

...
import * as fromBusy from './busy/busy.reducer';

export interface State {
  busy: fromBusy.State;
}

const reducers = {
  busy: fromBusy.reducer,
};

...

export const getBusyState = (state: State) => state.busy;
export const isBusyVisible = createSelector(getBusyState, fromBusy.isVisible)

busy-wrapper.component.ts There is probably a better way to do this. I'm open to suggestions to improve this.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Store } from '@ngrx/store';
import * as fromRoot from '../../core/store/index';

@Component({
  selector: 'app-busy-wrapper',
  template: `
    <div [ngBusy]="busySubs"></div>
  `,
})
export class BusyWrapperComponent implements OnInit, OnDestroy {
  private visible$: Observable<boolean>;
  private busySubs: Subscription;

  constructor(private store: Store<fromRoot.State>) {
    this.visible$ = store.select(fromRoot.isBusyVisible);
  }

  ngOnInit() {
    this.visible$
      .subscribe((isVisible) => {
        if (isVisible) {
          this.busySubs = new Subscription();
        } else if (this.busySubs && !isVisible) {
          this.busySubs.unsubscribe();
        }
      })
  }

  ngOnDestroy() {
    this.busySubs && this.busySubs.unsubscribe();
  }
}

Within one of your main components such as app.component

<app-busy-wrapper></app-busy-wrapper>

Now with this setup you can easily show and hide it by dispatching events to the store. For instance, if you are doing a service call via an effect, you can do the following:

  @Effect()
  search$: Observable<Action> = this.actions$
    .ofType(serviceActions.SEARCH)
    .map(toPayload)
    .switchMap(toPayload=> {
      return Observable.concat(
        Observable.of(new busyActions.ShowAction),
        this.serviceActions.doServiceCall(toPayload)
          .map(payload => new serviceActions.SearchSuccessAction(payload))
          .catch(() => Observable.of(new serviceActions.SearchErrorAction([])))
      )
    })

  @Effect()
  success$: Observable<Action> = this.actions$
    .ofType(serviceActions.SEARCH_SUCCESS)
    .map(() => new busyActions.HideAction())

  @Effect()
  error$: Observable<Action> = this.actions$
    .ofType(serviceActions.SEARCH_ERROR)
    .map(() => new busyActions.HideAction())
tderoo commented 7 years ago

Wow swargolet,

Thanks for the extensive example! Creating the wrapper component, like you suggest, makes it work indeed. Am wondering if in my (simple) scenario I would then be better of to create a completely independent busy component, but for now I'm trying this one. As so many components they appear easy at the start but quickly become more complicated once you start using them in earnest.

Thanks, Theo