Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to override some of the actions in ngrx store?

We have an angular project at work (to be developed, have not started yet). It's quite complex and has complicated data flows. Also, we have two kinds of users in our application. Manager and users. These users will see similar views but there are some different custom views for each. Manager will have access to more functionalities as you would imagine. In order to manage this quite big and complex application in a scalable way, we follow NX pattern. I.e. multiple apps within single repo. At the end, within single repo I have following applications.

Apps

Most of the development will be done in common app and different views and customization will be done in both mngr-app and user-app respectively.

We, also, think of employing ngrx in our application for state management. I've seen multiple examples and tutorials to how to do that. Everything is OK so far. This part is just for the background info.

Our problem starts after this. Our business team also wants us to develop both iOS and Android applications with webviews containing the web application (I forgot to mention it is a responsive web app). So, everything we have done will be shipped within a webview to mobile users. However, the business team, also, wants us to develop some custom native views for mobile applications.

Let's take a look at the following example: (this is from ngrx example)

Add book collection

When user clicks on Add Book to Collection button, an action type of [Collection] Add Book is dispatched to the store and an effect will take care of it as follows:

@Effect()
addBookToCollection$: Observable<Action> = this.actions$
  .ofType(collection.ADD_BOOK)
  .map((action: collection.AddBookAction) => action.payload)
  .switchMap(book =>
    this.db.insert('books', [ book ])
      .map(() => new collection.AddBookSuccessAction(book))
      .catch(() => of(new collection.AddBookFailAction(book)))
  );

This is the normal flow of the web application.

Our business team wants us to build some sort of custom logic for mobile applications, so that when users navigates to this page in a mobile application (either iOS or Android), instead of adding a book to collection it'll open up a native page and user will take actions on that native page. What I mean by this is that they want web application to behave differently when present in a mobile application. I can achieve this with bunch of if(window.nativeFlag === true) within web application. However, this is just a dirty hack we want to avoid. Since, we are using ngrx and rxjs we feel like this can be done with Observables of rxjs and Actions of ngrx.

What we have tried so far is to expose store and actions object to DOM so that we can access it within mobile app.

  constructor(private store: Store<fromRoot.State>, private actions$: Actions) {
    window['store'] = this.store;
    window['actions'] = this.actions$;
  }

With this way, we can subscribe to [Collection] Add Book event as follows

actions().ofType('[Collection] Add Book').subscribe(data => {
    console.log(data)
})

and get notified when a book is added to the collection. However, web application still does what it does normally and we cannot override this behavior.

My question is how to subscribe to some ngrx actions from mobile applications and cancel out web application behavior?

Edit

I've come up with a solution on my own, however any other solutions are appreciated and I'll give the bounty if you can come up with a better one.

like image 970
Bunyamin Coskuner Avatar asked May 14 '18 12:05

Bunyamin Coskuner


1 Answers

What I have been able to do so far

I have written following bridge within web application.

window['bridge'] = {
  dispatch: (event: string, payload = '') => {
    // I had to use zone.run, 
    // otherwise angular won't update the UI on 
    // an event triggered from mobile.
    this.zone.run(() => {
      this.store.dispatch({type: event, payload: payload});
    });
  },
  ofEvent: (event: string) => {
    // I register this event on some global variable 
    // so that I know which events mobile application listens to.
    window['nativeActions'].push(event);
    return this.actions.ofType(event);
  }
};

From Android application, I'm able to listen to [Collection] Add Book as follows:

webView.loadUrl("
    javascript:bridge.ofEvent('[Collection] Add Book')
       .subscribe(function(book) {
           android.addBook(JSON.stringify(book.payload));
       });
");

This will trigger my addBook method, and I'll save the book on some class to use it later.

@JavascriptInterface
public void addBook(String book) {
    Log.i("addBook", book);
    this.book = book;
}

I, also, add a button to android application to delete this book.

webView.evaluateJavascript("
      bridge.dispatch('[Collection] Remove Book', "+this.book+")
", null);

With this piece of code, I was able to listen to an event triggered by web application and save the result somewhere. I, also, was able to trigger some event from android application to delete the book in the web application.

Also, earlier I mentioned that I registered this event on a global variable.

I changed my @Effect to following

@Effect()
addBookToCollection$: Observable<Action> = this.actions$.pipe(
  ofType<AddBook>(CollectionActionTypes.AddBook),
  map(action => action.payload),
  switchMap(book => {
    if (window['nativeActions'].indexOf(CollectionActionTypes.AddBook) > -1) {
      return of();
    } else {
      return this.db
        .insert('books', [book])
        .pipe(
        map(() => new AddBookSuccess(book)),
        catchError(() => of(new AddBookFail(book)))
        );
    }
  })
);

With if (window['nativeActions']..) I was able to check if mobile application registered to this particular event. However, I would not perform this control on every @Effect I have. At this point, I'm thinking of writing some custom RxJs operator to abstract away this check from my effects. I feel like I'm close to an answer, but I'll appreciate any input.

Custom RxJs operator

I've written following custom operator and solved my problem.

export function nativeSwitch(callback) {
    return function nativeSwitchOperation(source) {
        return Observable.create((subscriber: Observer<any>) => {
            let subscription = source.subscribe(value => {
                try {
                    if (window['nativeActions'].indexOf(value.type) > -1) {
                        subscriber.complete();
                    } else {
                        subscriber.next(callback(value));
                    }
                } catch (err) {
                    subscriber.error(err);
                }
            },
            err => subscriber.error(err),
            () => subscriber.complete());

            return subscription;
        });
    };
}

Changed my effect to following:

@Effect()
addBookToCollection$: Observable<Action> = this.actions$.pipe(
  ofType<AddBook>(CollectionActionTypes.AddBook),
  nativeSwitch(action => action.payload),
  switchMap((book: Book) =>
    this.db
      .insert('books', [book])
      .pipe(
      map(() => new AddBookSuccess(book)),
      catchError(() => of(new AddBookFail(book)))
      )
  )
);

This does exactly what I want. Since, I call subscriber.complete() if given event is registered by mobile application, the method within switchMap never gets called.

Hopefully, this will help someone.

like image 150
Bunyamin Coskuner Avatar answered Oct 11 '22 05:10

Bunyamin Coskuner