11

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.

Bunyamin Coskuner
  • 8,719
  • 1
  • 28
  • 48
  • Even though I'm not sure how to answer this - I think it's a great question and NX looks like a great framework. Would love to see more action on this question from people who know NX... – Aviad P. May 15 '18 at 20:52
  • @AviadP. I'll update the question with my trials so far. I, also, think of putting bounty on it today if you are still interested. – Bunyamin Coskuner May 16 '18 at 06:04
  • When you subscribe to any event it gives you an object back. You can store this in some global dictionary and later call an `unsubscribe` on the same. You can use different keys for storing id then one initiated by web or the one which needs to be overridden by the native pages. Then the native pages can call `unsubscribe` on the `web` ones. https://stackoverflow.com/questions/47496383/how-to-unsubscribe-from-ngrx-store. I am not a angular dev, but I believe the approach should work? – Tarun Lalwani May 17 '18 at 19:16
  • it can be done with Observables in Rx style, but before suggesting any concrete answer to you, please clarify , have you been able to make calls to native codes (java in android for now) from your javascript webapp ? If yes than, are you using @JavascriptInterface or something else ? – abhay tripathi May 18 '18 at 03:41
  • @abhaytripathi yes, I have. Also, yes to javascriptInterface. I'll update my question. – Bunyamin Coskuner May 18 '18 at 06:13
  • @abhaytripathi I posted an answer. Check it out. – Bunyamin Coskuner May 18 '18 at 06:28

1 Answers1

4

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.

Bunyamin Coskuner
  • 8,719
  • 1
  • 28
  • 48