Technology Blogs by SAP
Learn how to extend and personalize SAP applications. Follow the SAP technology blog for insights into SAP BTP, ABAP, SAP Analytics Cloud, SAP HANA, and more.
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member
UPDATE Feb 2, 2017: Also checkout the blog post of @Christoph Kraemer which introduces ReduxModel as an alternative (see my comment at the bottom for my own experiences with Redux vs MobX)

Short Summary

This blog post is about using the state management library MobX together with UI5 to manage state in complex UI5 applications (as alternative to UI5 JSONModel). The benefits of this approach are a better separation between UI and business logic, easier testable code, eventually less number of bugs and in some cases even better performance of your app.

If you've heard the fancy term "(functional) reactive programming" before, you'll be glad to hear that MobX is a library which applies a flavor of this and brings reactivity to UI5. However compared to other reactive programming libraries (for example RxJS) it hides a lot of the complexity and hence is much easier to learn and integrate into existing projects, teams or frameworks like UI5.

In fact because mobx is so simple to learn and reason about, I won't have to talk much about theories and reactive programming concepts in my blog post, but show you by example the benefits.

For ui5 and mobx to work seamlessly together we're going to use https://github.com/geekflyer/mobx-ui5


The approach was successfully applied for ui components in the product SAP Predictive Maintenance & Service: https://eaexplorer.hana.ondemand.com/_item.html?id=11527#!/overview

Introduction

In the sections below I will show based on examples with increasing complexity the motivation behind the usage of MobX in a UI5 application.

If you're just interested in the usage of MobX directly (without reading "1. Motivation and limitations...") you can go directly to section 2: "State management with MobX."

1. Motivation and limitations of UI5 JSONModel


As an experienced UI5 developer you will know that it is a good practice to put most of your state into the model (e.g. JSONModel, ODataModel) and bind your controls (e.g. a sap.m.Text) to certain in paths in that model. Whenever a user performs an action (like a button click) you update in the event handler the state of the model and the dependent controls will update / rerender automatically. The (discouraged bad-practice) alternative would be to not update the model but instead get a reference to the control to be updated by this.byId(controlId) and set the control property directly (e.g. myText.setText('new text') )

1.1. Simple example with JSONModel


Lets look at a simple example of the best practice using JSONModel, having 1 button and a text that should be changed once the user clicks the button.

To create a comprehensive example with a minimum of boilerplate I'm using just fragments and a app.js with a pseudo-controller on the fragment instead of Views and real Controllers:

app.fragment.xml:
<VBox xmlns="sap.m">
<Button press=".onButtonPress" text="Press me!"/>
<Text text="{/text}"/>
</VBox>

app.js:
  var state = {
text: 'initial text'
};

var model = new JSONModel(state);

var fragment = sap.ui.xmlfragment('my.example.app', {
onButtonPress: function() {
model.setProperty('/text', 'changed text');
}
});

fragment.setModel(model);

 

You can see the running example here: https://plnkr.co/edit/Hs2pd5B6hbrDxTyJ9jDe?p=preview

Now what happens is very simple - once the user presses the button the onButtonPress event handler is invoked which then updates the value in the model via model.setProperty('/text', 'changed text'); and consequently the textfields' text is changed to changed text.

 

1.2. Simple example with JSONModel in observation mode


So far so simple.

However, some of you might wonder, why can't you simply change the text directly on the state object. via state.text = 'changed text'; ? Wouldn't that be more simple and elegant? The problem is if you do that the JSONModel won't get notified of this change and hence the textfield won't get updated.

However in recent UI5 versions a new feature called observation mode was introduced for the JSONModel. In order to activate it you have to pass true as the second param to the JSONModel constructor. When having observation mode enabled you can set the property directly on the state object and the model will still get notified. The way it works is by converting the state objects properties into ES5 getters and setters and then hooking into the setter function (this is also one of primitives MobX uses). However you don't have to necessarily understand this magic trick in order to use it.

Now lets use the observation mode in the example:

app.js
 var state = {
text: 'initial text'
};

var model = new JSONModel(state, true);

var fragment = sap.ui.xmlfragment('my.example.app', {
onButtonPress: function() {
state.text = 'changed text';
}
});

fragment.setModel(model);

running code: https://plnkr.co/edit/XZTjbM?p=preview

That's already a bit simpler and elegant imho.
Unfortunately there are a few limitations of the observation mode in the JSONModel, most notably it doesn't notify the model when you have an array and add new items to it. For the model get notified I have to call model.refresh() after adding the new item.

1.3. Complex example with JSONModel in observation mode


After having shown some very simple example of the status quo with JSONModel, let's do a little bit more challenging example.

1.3.1 Requirements of example app


The task is the following:

We want an app which displays a list of (simplified) orders. Each order consists of an id, productName and quantity.

E.g.:  `{id: 5, productName: 'Gummy bears', quantity: 15}`

- The app should provide the ability to add new orders, to remove the last order and to edit any order (change the product or quantity).

So far not really challenging. However lets add some special requirements:

- We want to display the total number of orders
- We want to display the total number of items (order x quantity)
- We want to display a summary of quantities grouped by product. So for each product we want to see how many instances of it have been ordered in the past in total. For example if I had 3 orders of Gummy bears, each with a quantity of 15, the summary should display a total of 45 gummy bears.
- All of the totals and summaries should be updated immediately when I do any change operation to the list of orders (add, remove, edit).

The final application (yep I won't win a design award for this ;-)) should look like this:

In red you can see the parts which contain the totals and summary and which are the challenging requirements.


You can see the final code and running app here: https://plnkr.co/edit/cqLRN8?p=preview

1.3.2 Implementation of the app's basics


Let's analyze our requirements a bit.

What is our state?
In its pure form the application's state consists only of:

  • The list of orders, which can be represented as an array of objects. E.g.: [{id: 5, productName: 'gummy bears', quantity: 15].

  • The form state of the 'new order' form - productName and quantity in the form fields. This is somewhat temporary state, but still state.


Those 2 things we would most likely put in the model and then bind controls against this state.
We would initialize the state object like this:
  var state = {
newOrder: {
productName: 'my product',
quantity: 1
},
orders: [],
};

this is how I would bind the new order form and the order list agains this state model:

new order form:
 <form:SimpleForm>
<Label text="Product Name"/>
<Input value="{/newOrder/productName}" />
<Label text="Quantity"/>
<Input value="{/newOrder/quantity}"/>
<Button press=".onOrderAdd" text="Add Order"/>
</form:SimpleForm>

order list:
<Table items="{/orders}">
<columns>
<Column>
<Text text="OrderId" />
</Column>
<Column
demandPopin="true">
<Text text="Product"/>
</Column>
<Column
demandPopin="true">
<Text text="Quantity"/>
</Column>
</columns>
<items>
<ColumnListItem>
<Text text="{='id: ' + ${id}}"/>
<Input value="{productName}" valueLiveUpdate="true"/>
<Input value="{quantity}" valueLiveUpdate="true"/>
</ColumnListItem>
</items>
</Table>

When someone clicks the "Add Order" button the onOrderAdd event handler will add a new item to the order array. However - and here's already the first gotcha - in order for the JSONModel to get notified of adding a new array item I have to call `model.refresh()` after adding the item:
  var fragment = sap.ui.xmlfragment('my.example.app', {
onOrderAdd: function() {
state.orders.push({id: ++orderId, productName: state.newOrder.productName, quantity: state.newOrder.quantity});
model.refresh();
}
})

Ok, that was not so difficult, even though you have to be already careful of not forgetting to call model.refresh() after adding the array item.

1.3.3 Implementation of the derived totals and summary


Now, how about the totals and the summary?

Obviously those are not state, but they can be completely derived from the list of orders state array.

The issue with UI5 formatter functions

As a seasoned UI5 developer you would now think, alright - no problem, let's use a ui5 formatter function which computes those totals dynamically whenever anything in the underlying state changes. However in practice there are some problems with that, some of which make formatters not the right fit for the above use case. maksim.rashchynski has already given some examples of the limitations of formatters here https://blogs.sap.com/2016/02/02/check-please-or-dynamic-properties-in-jsonmodel/ , but let's repeat some of them quickly:

  • formatters have to explicitly specify their dependencies (which properties in the model they depend on)

  • those dependencies cannot be an array or objects as a whole, but only specific properties. You can for example not specify the length property of an array as dependency and expect the formatter to update automatically once someone adds a new item to that array.

  • you cannot use formatters to dynamically compute an array / object / aggregation like the list of summary  items. you can only use them to dynamically compute a property value. In the case of arrays you can actually use "filters" and "sorters" to somewhat dynamically compute another array but those options are limited to specific transformations.


besides there are a few annoyances when using lots of formatter functions:

  • even if you "reuse" a formatter function in multiple places in your view, you always have to specify the dependencies (path / parts) again and again, which can mean a lot of boilerplate / repetition in your view.

  • If you reuse formatters and have the situation that a reused formatter observes the exact same data (basically you want to show a value in multiple places on the UI), UI5 will evaluate the formatter function multiple times, even though it would be obviously more performance efficient to simply reuse the result of the first formatter evaluation and show it in multiple places. It is also not possible to build a hierarchy of formatters where one more formatters "reuses" the intermediate result of another formatter. All computations run separately for each usage in the UI. For many simple use cases this behaviour is a not a problem, but if you do complex calculations, iterate large arrays etc. performance can become an issue.

  • even if formatters are sometimes kind of business logic, you cannot easily define them directly on your model or state object, but they have to become a property of the controller.


Because of those limitations, especially the ones with regards to having arrays as dependencies and computing another array we cannot really use formatters for the complex example above.

Alternative to UI5 formatter functions - triggering recomputation of derived values manually

So what can we do instead?

In my example implementation with JSONModel I did the following.
I introduced 2 new properties on the state object, one being summary and one being total. Also we will need the length property of the orders array, even though as mentioned earlier the binding will not automatically update once its changing.

The enhanced state object looks like this:
 var state = {
newOrder: {
productName: 'my product',
quantity: 1
},
orders: [],
total: 0,
summary: []
};

And this is how we bind the controls against it:
  <Label text="{= 'Total items: ' + ${/total}}" design="Bold"/> 
<VBox>
<Label text="Summary by Product" design="Bold"/>
<VBox items="{/summary}">
<Text text="{= ' - ' + ${productName} + ': ' + ${quantity}}"/>
</VBox>
</VBox>
<Table headerText="{path: '/orders/length', formatter: '.headerText'}" items="{/orders}">

Notice how I'm using for the table header a formatter function and order.length as dependency, despite the fact that I know it won't automatically update.

As mentioned earlier the total and summary can be computed from the orders array. So let's add some methods to do the computation to the fragment pseudo-controller:
var fragment = sap.ui.xmlfragment('my.example.app', {
...,
refreshTotal: function(){
state.total = state.orders.reduce(function(accumulator, order){ return accumulator + parseInt(order.quantity);}, 0);
},
refreshSummary: function(){
var summaryMap = {};
state.orders.forEach(function(order){
var productName = order.productName;
if (typeof summaryMap[productName] === 'number') {
summaryMap[productName]+= parseInt(order.quantity);
} else {
summaryMap[productName] = parseInt(order.quantity);
}
});
var summary = Object.keys(summaryMap).map(function(productName) {
return {productName: productName, quantity: summaryMap[productName]};
});

state.summary = summary;
}
});

Well nice, we now have the logic which updates the summaries.
Whats the problem then?

The problem with manually triggering recomputation of derived values


The problem is that this "refresh" logic won't be called automatically when I add, remove or edit an order. In fact whenever I change anything in the orders array, I have to manually call afterwards the refreshTotal , refreshSummary and model.refresh() methods to ensure the summary, totals and list header are updated.

In my example app this looks like the following:
var fragment = sap.ui.xmlfragment('my.example.app', {
onOrderAdd: function() {
state.orders.push({id: ++orderId, productName: state.newOrder.productName, quantity: state.newOrder.quantity});
this.refreshSummary();
this.refreshTotal();
model.refresh();
},
onOrderRemove: function(){
state.orders.pop();
this.refreshSummary();
this.refreshTotal();
model.refresh();
},
onOrderChange: function(){
this.refreshSummary();
this.refreshTotal();
model.refresh();
},

Also I have to listen to the liveChange event of the input fields in the order table (when someone edits an existing order) and call the onOrderChange method:
      <ColumnListItem>
<Text text="{='id: ' + ${id}}"/>
<Input value="{productName}" liveChange="onOrderChange" valueLiveUpdate="true"/>
<Input value="{quantity}" liveChange="onOrderChange" valueLiveUpdate="true"/>
</ColumnListItem>

Calling model.refresh() too often is possibly an expensive operation. It triggers the checkUpdate function of every binding created by the model, even if the bound data wasn't affected by your state change directly or indirectly. The checkUpdate function does a deep equality comparison (which can become costly for large state trees) of the binding's current value with the new value in the model. If you have a lot of controls and hence bindings in your app this can cause performance issues.

If I forget to call any of the refresh methods in some places, my app won't crash immediately but instead I will get subtle and hard to debug bugs because of inconsistent stale data. From my own experience I can say those bugs are the most annoying as they are harder to diagnose and reproduce - you typically won't get a stacktrace etc. As your app grows and the more people work on it, the higher the chances that somebody changes the state but forgets to call any of the refresh methods and your displayed data is stale and inconsistent.

This is exactly the situation I ran into in a recent project and which motivated me to reimplement the critical parts of that project using UI5 with MobX. If you've had the chance to checkout the github page of MobX you'd see its philosophy quoted as:

Anything that can be derived from the application state, should be derived. Automatically.

The last word Automatically is very important for us. If you were able to follow my code example, you'd note I also derived something from the application state. However I was triggering the recomputation manually, telling the app when to refresh the derived data, instead of letting the derivations run automatically whenever any of their dependencies change.

Loosely said, in HANA the equivalent of what I was doing would be to have a table A with orders and a Table B which contains the summary aggregates of table A. Since B is a stateful table I have to manually ensure that table B is emptied and recomputed once something in A changes (the equivalent would be to call for example a stored procedure at certain trigger points). The better alternative would be to create a view C, which is automatically computed once someone is interested in the data (and only then) and cannot have stale aggregates. Also as long as none of the "table dependencies" used by the view (in our case table A) have changed the view C should not recompute for every consumer again, but instead return a cached version of it (in HANA there's an aggregate cache which does something similar).

This is somewhat the direction MobX brings us too, but actually even further.

 

2. State management with MobX and UI5


I hope my words and examples in the last section inspired your motivation to read on.

So what is MobX anyways?

MobX, slogan is "Simple, scalable state management." and their philosophy is "Anything that can be derived from the application state, should be derived. Automatically."

In other words mobx helps you to maintain minimal actual state in your application and derive everything from that, meaning views, texts, items, state and visibility of controls, automatically. Automatically here means it will trigger the recomputation at the right point in time, avoid running the same computation twice for multiple consumers / observers, suspends computations if nobody is interested in it etc. It helps you to build a reactive application state machine. It's not a UI rendering or control library, but instead would be used in conjunction with other libraries or frameworks.

I won't go too much into the internals of MobX itself, but I highly recommend you to quickly go through the "Gist" and "Concepts & Principles" section of mobx: https://mobx.js.org/intro/overview.html

2.1 References


MobX is not some fancy experiment, but a long standing, battle-tested library (nowadays in version 3) used in small to very big projects. It has more than 7000 stars on github and is the second most popular state management library in the React.js ecosystem after Redux (and in my opinion it's the better of the two). The father of mobx is Michel Weststrate  who works at mendix in the Netherlands. While a lot of the documentation and examples of mobx use ES6 + ES7 syntax and React it can be absolutely used with UI5 or any other UI framework (it can even be used server-side in node.js) and plain ES5 syntax. For that matter it also supports Internet Explorer 9 and higher.

We used MobX recently for the development of some complex configuration UI's (lots of forms etc.) in SAP Predictive Maintenance & Service, the product I'm working on: https://eaexplorer.hana.ondemand.com/_item.html?id=11527#!/overview .The results were extremely positive, meaning less code, faster implementation, less bugs and it was a joy to use MobX in that context.

2.2. Simple MobX (without UI5) example


In a nutshell, what mobx does its turning your JavaScript state object into an observable structure. That means interested parties can "subscribe" and "observe" changes that someone makes on this structure. For you as an application developer it's actually not that important to know about the observability, subscriptions etc, as MobX does most of its magic automatically behind the scenes and in a very efficient manner. You can mutate your observable state structure mostly with normal javascript constructs like you would change any other object and dependents / observers are notified of your change.

The following example demonstrates some of the basics.

First, I create an initial state object which is passed to mobx.observable. Mobx will then create a copy that object but make it a sort of magic observable object on whose changes I can later subscribe.

The first thing you will notice is that I defined a getter named 'message' on the state object. MobX will turns this into a "computed property".  Like all all the regular properties I can also subscribe to changes of computed properties. Whenever any of the dependencies of the computed property changes (in this case name and tasks.length) it will run the getter function, recompute the value and notify me in case the result of the getter has changed. It is actually smart enough to notify me only if the result of the getter function has changed, not just the dependencies. If you have a fairly complex calculation but it's result is the same as the previous run, it will not notify it's observer again.

The next thing you'll see is mobx.autorun.

This simply runs the given function when any of the used dependencies inside the function change. In real application development you won't use mobx.autorun that often. I use it mostly for debugging and to perform side effects on non-observable objects.
After having set up my state and implicitly subscribed to changes in it (via mobx.autorun) I'm modifying my state object. Note how I can change the name and push array items and it will only re-run the autorun whose dependency has changed.
  var state = mobx.observable({
name: 'Frank',
tasks: [],
get message(){
return 'Hi ' + this.name + ' there are ' + this.tasks.length + ' pending tasks.';
}
});

mobx.autorun(function() {
console.log(state.name);
});
mobx.autorun(function() {
console.log(state.message)
});

// prints 'Jane'
// prints 'Hi Jane there are 0 pending tasks';
state.name = 'Jane';

// prints 'Hi Jane there are 1 pending tasks.'
state.tasks.push('something');

// prints 'Hi Jane there are 2 pending tasks.'
state.tasks.push('somethingelse');

https://plnkr.co/edit/XCgsRW?p=preview   (You have to open your browser console to see the log for this example, there's no UI)


2.3 UI5 MobxModel


For MobX to work nicely with UI5 I've built a custom model implementation which wraps an mobx observable and basically bridges from the mobx observable to ui5 controls and vice-versa. The implementation is based on reading the mobx documentation and the UI5 JSONModel source code.
It can be found here: https://github.com/geekflyer/mobx-ui5 . In my example code I'm loading the MobxModel code directly from github, but if you want to consume it in your own (possibly private) project you can just copy the code into your project. You can then either register the namespace `sap.ui.mobx' or change it to a namespace of your choice by changing the constant in namespace.js https://github.com/geekflyer/mobx-ui5/blob/master/src/namespace.js#L3 .
In future when I have some time I might also release the code on npm and / or bower, just let me know if you have an urgent need for that.

Once you've properly imported the MobxModel you can just instantiate it (very similar to JSONModel) like this:
var state = mobx.observable({
foo: 'bar'
});

var model = new MobxModel(state);

this.getView().setModel(model);

Once you've constructed the model and set it to the view or fragment, you most likely won't interact with it directly not much anymore.
When working with mobx, the idea is you work with the state object/s directly and just manipulate them. That means you will rarely or hopefully never have to call model.setProperty , model.getProperty or model.refresh`.

instead you just manipulate your state object like state.foo = 'baz'.

In fact using MobxModel your real model becomes the state object itself (we will add some business logic to the state object later). The MobxModel is merely an adapter of your observable real model to UI5. On a side note: I thought a long time whether I should name the class MobxModel or MobxUI5Adapter and I'm still undecided if it might be more adequate to change it's name to MobxUi5Adapter (curious for your thoughts ;-)).

If you use TwoWay binding, which is also supported via MobxModel, behind the scenes UI5 will still call model.setProperty, but you don't have to worry about that and your state object will be automatically updated.

2.4 Rewriting the complex example in MobX


If you've jumped directly to section 2 of my blog post I request you to read now at least the initial sections of 1.3. where I describe the "requirements" of my hypothetical app. The app that we're now going to rewrite using mobx.

As you've already seen in the simple mobx example, we can use mobx computed properties to dynamically compute complex values, including arrays and objects which update automatically.

In our previous complex app example with JSONModel we had the requirement of providing a summary and total of the orders which required us to dynamically compute an array. Due to limitations of UI5 formatters we couldn't use them to compute an array but instead had to introduce new state properties and keep them manually in sync with the orders array. Now with mobx we don't have to keep them manually in sync anymore. Instead we can just define a mobx computed property via a getter and then mobx will ensure it's always in sync with the orders array.

The refresh<something> methods on the fragment controller, which were performing side-effects, can be turned into side-effect free and easier testable getters that are attached directly to the state object.

The result looks like this:
var state = mobx.observable({
newOrder: {
productName: 'my product',
quantity: 1
},
orders: [],
get summary() {
var summaryMap = {};
this.orders.forEach(function(order){
var productName = order.productName;
if (typeof summaryMap[productName] === 'number') {
summaryMap[productName]+= parseInt(order.quantity);
} else {
summaryMap[productName] = parseInt(order.quantity);
}
});
var summary = Object.keys(summaryMap).map(function(productName) {
return {productName: productName, quantity: summaryMap[productName]};
});
return summary;
},
get total(){
return this.orders.reduce(function(accumulator, order){ return accumulator + parseInt(order.quantity);}, 0);
},
get headerText() {
var orderCount = this.orders.length;
if (orderCount === 0) {
return 'There are no orders';
} else if (orderCount === 1) {
return "There is 1 order";
} else {
return 'There are ' + orderCount + ' orders';
}
}
});

Note that the actual state now really is only the orders and the newOrder form states. All the other things are really just functions to derive values from the state. The cool thing is however you can bind your controls against those computed properties and they automatically notify their observers (e.g. MobxModel / UI5 controls) once any of their dependencies and the result of their computation have changed. To UI5's binding / binding syntax those getters look like any other property.

Because we moved all the computation to the mobx state object, our fragment controller now looks fairly small:
 var orderId = 0;
var model = new MobxModel(state);

var fragment = sap.ui.xmlfragment('my.example.app', {
onOrderAdd: function() {
state.orders.push({id: ++orderId, productName: state.newOrder.productName, quantity: state.newOrder.quantity});
},
onOrderRemove: function(){
state.orders.pop();
}
});

fragment.setModel(model);

and our view / fragment actually stayed almost the same (I just removed the formatter reference of the headerText). Note how I'm binding "/total" "/summary" and "/headerText" directly against the computed properties in my state object. No formatter functions or anything.
<VBox xmlns="sap.m" xmlns:form="sap.ui.layout.form">
<form:SimpleForm>
<Label text="Product Name"/>
<Input value="{/newOrder/productName}" />
<Label text="Quantity"/>
<Input value="{/newOrder/quantity}"/>
<Button press=".onOrderAdd" text="Add Order"/>
</form:SimpleForm>
<Label text="{= 'Total items: ' + ${/total}}" design="Bold"/>
<VBox>
<Label text="Summary by Product" design="Bold"/>
<VBox items="{/summary}">
<Text text="{= ' - ' + ${productName} + ': ' + ${quantity}}"/>
</VBox>
</VBox>
<Table headerText="{/headerText}" items="{/orders}">
<columns>
<Column>
<Text text="OrderId" />
</Column>
<Column
demandPopin="true">
<Text text="Product"/>
</Column>
<Column
demandPopin="true">
<Text text="Quantity"/>
</Column>
</columns>
<items>
<ColumnListItem>
<Text text="{='id: ' + ${id}}"/>
<Input value="{productName}" valueLiveUpdate="true"/>
<Input value="{quantity}" valueLiveUpdate="true"/>
</ColumnListItem>
</items>
</Table>
<HBox>
<Button press=".onOrderRemove" text="Remove Last Order"/>
</HBox>
</VBox>

The running application using mobx can be found here: https://plnkr.co/edit/zqrOKW?p=preview

It looks and works exactly like the JSONModel app before, but the code is a bit simpler.

2.5. more simplification of complex example


When you develop with mobx you will quickly realize how you can move a lot of business logic out from the controllers. In fact you might have noticed that our state object currently doesn't call any ui5 specific method anymore and has no external dependencies. So why not move it into a seperate file? In my example I moved the whole state object into a state.js file. The state object is now self contained and easily testable. It uses just plain javascript code and has no dependencies to DOM elements, controls, ui5 classes etc. Also it actually contains most of the apps logic and the fragment controller is almost empty. However there's one more optimization we can do. If you look at the onOrderAdd and onOrderRemove event handlers they actually contain some business logic about the structure of an order item etc.. We can quickly fix that by moving the respective methods to the state object and simply delegate from the controller to them. The final app.js and state.js then looks like this:

final app.js
sap.ui.define(['sap/ui/mobx/MobxModel', './state'], function(MobxModel, state) {

var model = new MobxModel(state);

var fragment = sap.ui.xmlfragment('my.example.app', {
onOrderAdd: state.addOrder.bind(state),
onOrderRemove: state.removeLastOrder.bind(state)
});

fragment.setModel(model);
fragment.placeAt('main');

});

final state.js
sap.ui.define([], function(){ 

var orderId = 0;

var state = mobx.observable({
newOrder: {
productName: 'my product',
quantity: 1
},
orders: [],
get summary() {
var summaryMap = {};
this.orders.forEach(function(order){
var productName = order.productName;
if (typeof summaryMap[productName] === 'number') {
summaryMap[productName]+= parseInt(order.quantity);
} else {
summaryMap[productName] = parseInt(order.quantity);
}
});
var summary = Object.keys(summaryMap).map(function(productName) {
return {productName: productName, quantity: summaryMap[productName]};
});
return summary;
},
get total(){
return this.orders.reduce(function(accumulator, order){ return accumulator + parseInt(order.quantity);}, 0);
},
get headerText() {
var orderCount = this.orders.length;
if (orderCount === 0) {
return 'There are no orders';
} else if (orderCount === 1) {
return "There is 1 order";
} else {
return 'There are ' + orderCount + ' orders';
}
},
});

state.addOrder = function() {
this.orders.push({id: ++orderId, productName: this.newOrder.productName, quantity: this.newOrder.quantity});
};

state.removeLastOrder = function(){
this.orders.pop();
};

return state;
});

2.6 Possible improvements to state.js and mobx best practices


The app.js and the fragment already look quite clean now. However after moving everything to state.js the state.js itself looks a bit too messy and possibly deals with too many things at the same time. I won't show any further optimizations in this blog post, but let's briefly talk about them:

First of all the state is a singleton which makes it hard to reset the state during a test run. So you should probably convert it into a class or factory function which produces fresh state objects.

Furthermore in general it is a good practice to create different classes for your domain objects and so called domain object stores to manage collections of your objects of them. In our case a domain object would be the order . So we need an Order class. Furthermore we need an OrderStore to manage multiple orders.

Also if you look the precisely the headerText computed property and the newOrder state are not really generic business logic, but are only needed in the context of the current application. Those you would typically separate into a uiState observable and or / model. With mobx you can make all the instances of your domain classes observable themselves. Also you can have multiple models where one model observes another etc.

In fact I highly recommend you to read https://mobx.js.org/best/store.html which gives some best practice that we followed successfully in our project.

3. Limitations


I've shown how to use mobx with ui5, mostly as an alternative for ui5's JSONModel.

What remains to be discussed is how this compares for example to the ODataModel and how mobx can be used in the context of an odata application.

Also there are a few limitations to the UI5 MobxModel implementation:

- currently it only implements a subset of the UI5 Model interface. E.g. ContextBinding and TreeBinding won't work. However this can be added as needed - I just haven't yet had a use case in my projects for this together with mobx.
- while I've successfully used mobx and MobxModel in a complex app (way more complex than the examples) there are probably a few edge cases in the UI5 binding API that MobxModel doesn't support or implements currently incorrectly. I've added a couple of tests to the MobxModel repository but they don't cover all possible usages of UI5 models.

Last but not least I just want to stress that while I'm working for SAP, I'm not affiliated with the UI5 team and to-date MobxModel remains an unofficial UI5 model implementation.

4. Summary and Outlook


I've shown how to use mobx with ui5 for complex, state driven applications.
Even though I tried to show a fairly complex example, this just touches the surface of mobx possibilities'. One thing I'm in particular interested is how to put all your validation logic into your mobx domain classes. One approach I've implemented already on a small scale is to bind your Input fields against a valueState and valueStateMessage computed property in an observable ui state store. Furthermore you can also provide a setter (set) for computed properties in mobx. I'll probably share my experiences in another blog post once this topic reaches maturity.

I hope you liked the approach and I'm excited for your feedback and questions.

Best Regards Christian
9 Comments