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.
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)
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 Observable
s of rxjs
and Action
s 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With