Skip to Content
Author's profile photo Laszlo Kajan

Adding Reactive State Management with Validation to Existing UI5 Application – a Tutorial

Executive summary

Tutorial for SAPUI5/OpenUI5 with reactive state management, using MobX model (live demo: Plunkerfinished tutorial code on GitHub).

Reactive state management is good because it makes application state management as easy as working with a spreadsheet – think ‘Excel’ or ‘Google Sheets’. It is used by SAP in its SAP Predictive Maintenance & Service application.

As a programmer, you might forget that changing some data might influence a seemingly unrelated component in a corner case. MobX won’t forget. It will automatically track every change.

Starting from a SAPUI5 app with validation and MessageManager, this tutorial shows how you can extend an existing UI5 app with reactive state management step-by-step. No need to rewrite everything. Reactive features can be introduced gradually on top of the original code.

Built with Christian Theilemann’s unofficial UI5 bindings for MobX.

Author and motivation

Laszlo Kajan is a full stack Fiori/SAPUI5 expert, present on the SAPUI5 field since 2015.

The motivation behind this blog post is to pick up the UI5 reactive state management story where it ends in Christian’s blog post: validation. Apart from validation, it is shown how reactive change management can be introduced gradually, during the iterative development of an existing application. This is a very important aspect, as it dramatically lowers the threshold for, and vastly broadens the applicability of reactive state management in UI5 applications.

The UI5 bindings used in this tutorial are a fork of Christian’s work, pending merging.

Huge thanks to Christian Theilemann for his inspiration, bindings, excellent blog post ‘Reactive state management in SAPUI5 via MobX‘, and insights linked therein.

The UI5 application to extend

The starting point of this tutorial is the ‘StartingApp’ branch of openui5-mobx-tutorial on GitHub: a ‘normal’ SAPUI5 application without surprises.

  1. Get a copy of the branch into your favorite IDE, e.g. the SAP WebIDE
  2. Once imported, start the application (app for short) either as a web app (index.html), or in the Fiori Launchpad sandbox (Component.js).
  3. Try the app:
    1. There is a MessagePopover for validation messages. There is a Submit button, which is to be enabled when the form is valid, and so ready for submission (pressing the button doesn’t do anything in this tutorial). There are 3 input fields and 3 control areas that show what is held in the domain model (the data domain is Snow White and some dwarfs). There is a button to set to first name to ‘Snow’ programmatically, simulating a change that is not due to direct user input.
    2. Type ‘SnowWhite’ in the ‘First Name’ field and commit (i.e. move the focus away or press Enter)
      1. Observe how the model now has ‘SnowWhite’ for the ‘First Name’
    3. Press the ‘Set her first name to ‘Snow” button
      1. ‘First Name’ is set to ‘Snow’
    4. Type ‘Sn0w’ (with the digit zero) in the ‘First Name’ field and commit
      1. Observe the validation error message: ‘Enter a value matching …’
      2. Observe how there is one ‘Error’ message in the MessagePopover, but no message text.
        This is a bug in the MessagePopover (e.g. in SAPUI5 version 1.44.12): messages with unescaped curly brackets [{}] fail to show.
        The message comes from the validation method of the type of the Input element for ‘First Name’ (which is ‘String’), through the MessageManager. The message model of the latter is set into the MessagePopover, which is convenient: messages show up without further coding. But in this simple setup we can’t escape message texts with curly brackets.
      3. Observe how the model still has ‘Snow’ in it.
        This is very important. In our pattern of model-view-controller (MVC), we suddenly have a model we didn’t ask for. The control somehow holds ‘Sn0w’, while it is only supposed to provide view for our domain model. Clearly it now displays its own model, disconnected from our domain model. This leads to surprises when you try to fix the invalid input:
    5. Press ‘Set her first name to ‘Snow” again
      1. Observe how this time nothing happens!
        If our app didn’t show what the model holds for ‘First Name’, this would be very confusing. What’s happening is this:

        1. The model holds ‘Snow’
        2. User enters invalid input ‘Sn0w’. The model is not updated because the input fails validation.
        3. We set ‘Snow’ into the domain model using the button. However this does not represent a change from the model’s perspective, and therefore the view – the control – is not updated. It continues to show the invalid input and the validation (value) state text.
    6. Test how the control reacts when the before-invalid-input value is not ‘Snow’: in this case pressing the ‘Set her first name’ button is effective
    7. Observe how the number of validation error messages is displayed and updated on the message popover button.This is done by listening to ‘change’ events of a list binding created for this purpose:
      // Application.controller.js
      this._oMessageModelBinding = new ListBinding(oMessageManager.getMessageModel(), "/");
      this._oMessageModelBinding.attachChange(this._updateMessageCount, this);​
    8. Fill correct values to all fields (empty first name and last name is valid). The ‘Submit’ button becomes enabled.
      1. This is done by listening to change events of all input fields and the ‘Set her name’ button, and calling ‘validateDomain()’ of the controller. ‘validateDomain()’ in turn calls helper function ‘_validateInput()’ for each input field. ‘_validateInput()’ uses the input control type to validate the input value.
        Calling ‘_validateInput()’ for each input field mimics the not-uncommon solution where full form validation is done by walking the control graph of a root form element, in an attempt to validate every input field and obtain an overall validation result (see Generic UI5 form validator).
      2. The form – at present – is simply valid if all input fields are valid
  4. How would you implement the following validation logic?:
    • Snow White must have at least one valid name, i.e. at least one of the name input fields must be filled and valid
    • In case both fields are filled, both must be valid
    • Validation messages must continue to indicate the problem, if any

Chances are, whatever implementation you have in mind, using reactive programming would result in cleaner, better-structured code. Better in the sense that it is easier to maintain, and less developer-mistake-prone. Let’s see.

The chimera app: traditional and reactive validation in one application

Let us implement the validation logic set forward above, using reactive state management:

  • Snow White must have at least one valid name, i.e. at least one of the name input fields must be filled and valid
  • In case both fields are filled, both must be valid
  • Validation messages must continue to indicate the problem, if any

Our goal is to implement the change as incrementally as possible, rewriting as little of the original app as possible.

Replacing JSONModel with MobxModel

  1. First, change the app to use a reactive model, an instance of MobxModel, instead of JSONModel
    1. Correct the shims and module path registration in Component.js:
      jQuery.sap.registerModuleShims({
      	"org/js/mobx/mobx.umd.min": {
      		exports: "mobx"
      	},
      	"org/js/mobxUtils/mobx-utils.umd": {
      		exports: "mobxUtils"
      	}
      });
      
      /* eslint-disable sap-no-hardcoded-url */
      jQuery.sap.registerModulePath("org.js.mobx", "https://cdnjs.cloudflare.com/ajax/libs/mobx/4.1.1");
      jQuery.sap.registerModulePath("org.js.mobxUtils", "https://unpkg.com/mobx-utils@4.0.0");
      jQuery.sap.registerModulePath("sap.ui.mobx", "https://raw.githubusercontent.com/laszloKajan/openui5-mobx-model/master/src");
      /* eslint-enable sap-no-hardcoded-url */​
    2. In model/models.js add new dependencies: ‘sap/ui/mobx/MobxModel’ and ‘org/js/mobx/mobx.umd.min’ as ‘MobxModel’ and ‘__mobx’ respectively
    3. Add ‘models.js’ as ‘models’ to ‘Application.controller.js’
    4. Move the ‘oDomainModel’ construction to models.js:
      // Application.controller.js
      var oDomainModel = models.createDomainModel();
      
      // models.js
      createDomainModel: function() {
      	var state = __mobx.observable({
      		SnowWhite: {}
      	});
      
      	var oModel = new MobxModel(state);
      	return oModel;
      }​
    5. Try the app
      1. Note how the view ‘Text’ controls marked “In model” are not updated, even though the model is. (You see this by the ‘Submit’ button getting (de)activated when input is (in)valid. Or you can use the debugger to observe.)
        This is because in our ES5-compatible implementation, changes to newly appearing object properties like ‘/SnowWhite/FirstName’ are not immediately tracked. This means the input binding of the label does not get notified of the change, since at the time the binding is created there is no ‘/SnowWhite/FirstName’ in the model yet.
        To solve this, it is enough to –
    6. Initialize all ‘static’ properties in the model:
      SnowWhite: {
      	FirstName: "",
      	LastName: "",
      	//
      	Age: undefined
      }​
      1. Run the app again: the ‘In model’ controls are now updated. The app behaves exactly like it did with JSONModel.

Reactive validation

As we noted earlier, invalid input is not copied into the model. This makes it impossible for our reactive model to validate fields and, more importantly, field groups.

We fix this by changing the behavior of input field validation:

  • When parsing and subsequent validation is performed by the input control, some value must be passed to the model, no matter what
  • Parsing and validation must not throw an exception during input control validation
  • In case a value in the model is changed, the new value is validated reactively (regardless of the source of the change)

This can be achieved by:

  • Extending (i.e. subclassing) the types ‘sap.ui.model.type‘ for use with MoxModel,
  • Defining extended type aware computed model properties for validation results, and
  • Binding input control ‘valueState’ and ‘valueStateText’ attributes to the computed validation results.

Extend your application like this:

  1. Create folder ‘model/MobxModel’ with a copy of these files you copy from GitHub:
    1. ‘TypeFactory.js’ extends a given ‘sap.ui.model.type’ type for use with MobxModel
    2. ‘Validation.js’ defines utility functions for performing validation, and working with validation results
  2. Change ‘Application.view.xml’ to use the extended types for validation:
    1. Replace ‘sap.ui.model.type.String’ with ‘org.debian.lkajan.mobxTutorial.model.type.String’
  3. Change models.js:
    1. Define a dependency on ‘sap/ui/model/type/String’, ‘TypeFactory.js’ and ‘Validation.js’ as ‘String’, ‘TypeFactory’ and ‘Validation’ resp.
    2. Create a type instance for validation (both first and last name can use the same instance in our case):
      // Just after "use strict";
      var MobxModelTypeString = TypeFactory.createExtendedType(String, "org.debian.lkajan.mobxTutorial.model.type.String");
      var oMobxModelTypeStringName = new MobxModelTypeString({}, {
      	search: /^(|[^0-9\s]{3,})$/
      });
      • Note how the type is defined exactly like in the view. We could in fact reuse the model type instance in the view, setting it programmatically in ‘onInit()’.
    3. Define computed validation properties:
      SnowWhite: {
      	FirstName: "",
      	get FirstName$Validation() {
      		return Validation.getModelPropertyValidationByType(this, "FirstName", oMobxModelTypeStringName, "string", /* ignoreChanged */ true);
      	},
      
      	LastName: "",
      	get LastName$Validation() {
      		return Validation.getModelPropertyValidationByType(this, "LastName", oMobxModelTypeStringName, "string", /* ignoreChanged */ true);
      	},
      	...
      }​
      • Computed validation properties return the following object:
        {
          valid: boolean,
          valueState: sap.ui.core.ValueState,
          valueStateText: string
        }
      • Validation.getModelPropertyValidationByType() has the following interface:
        /**
         * Get model object property validation results by type validation. Non-changed state appears to be valid regardless of validity.
         *
         * @param {object} oObject - 		Model object
         * @param {string} sProperty -		Model object property name
         * @param {object} oType -			Property type instance
         * @param {string} sInternalType -	Type used to display and input property, c.f. model type
         * @param {boolean} bIgnoreChanged - Ignore (non-)changed state of property when setting valueState. true: valueState is set even if value hasn't been
         *										changed by user
         * @return {object} 				{valid: boolean, valueState: sap.ui.core.ValueState, valueStateText: string}
         */
        getModelPropertyValidationByType: function(oObject, sProperty, oType, sInternalType, bIgnoreChanged)​ {...
  4. Bind ‘valueState’ and ‘valueStateText’ to validation results in ‘Application.view.xml’:
    <Input id="inputSWFirstName"
    	value="{path: 'domain&gt;/SnowWhite/FirstName', type: 'org.debian.lkajan.mobxTutorial.model.type.String', constraints: {search: '^(|[^0-9\\s]{3,})$'}}"
    	valueState="{domain&gt;/SnowWhite/FirstName$Validation/valueState}"
    	valueStateText="{domain&gt;/SnowWhite/FirstName$Validation/valueStateText}" change="onChangeRevalidate"/>
    <Input id="inputSWLastName"
    	value="{path: 'domain&gt;/SnowWhite/LastName', type: 'org.debian.lkajan.mobxTutorial.model.type.String', constraints: {search: '^(|[^0-9\\s]{3,})$'}}"
    	valueState="{domain&gt;/SnowWhite/LastName$Validation/valueState}"
    	valueStateText="{domain&gt;/SnowWhite/LastName$Validation/valueStateText}" change="onChangeRevalidate"/>
  5. Test the app
    1. Validation messages are shown and invalid input is copied into the model
      1. Validation messages do not appear in the MessagePopover though, since they never get to the MessageManager
    2. The ‘Set her first name’ button always works as expected
    3. Form validation, so the ‘Submit’ button, now depends solely on the validity of the ‘Age’ input. This is because our current validation logic depends on what the input fields ‘think’ about validity and not the model.

Merging MessageManager and reactive validation

We must combine the traditional input control (binding) type-based validation logic, with the new reactive model validation logic.

We begin by collecting reactive validation results from the domain model (a data graph) to an array. We then transform the results to an array of validation messages, and concatenate these messages with those of the MessageManager. We bind the MessagePopover to this array of concatenated messages.

  1. In ‘Application.controller.js’, make ‘oAppModel’ a ‘MobxModel’ with new properties for domain validation result messages:
    var oAppModel = new MobxModel(__mobx.observable({
    	messageCount: 0,
    	canSubmit: false,
    	validationMessages: []
    }));​
    1. Add the missing dependencies ‘__mobx’, ‘MobxModel’ and ‘Validation’
  2. Add code for the reactive transformation of domain model validation results to validation messages after ‘oMessageManager.registerObject(this.getView(), true);’:
    //	Flatten validation results: transform domain model to validation array
    this.oObservableValidation = __mobx.observable({
    	results: [] // will be replaced by transformation to observable array
    });
    this._fAutorunDisposerObservableValidation = __mobx.reaction(
    	Validation.transformModelToValidationArray.bind(this, oDomainModel.getObservable()),
    	function(aValidationResults) {
    		this.oObservableValidation.results = aValidationResults;
    	}.bind(this),
    	true // Fire immediately
    );
    
    //	Transform validation array to validation message array
    this.oObservableValidationMessages = __mobx.observable({
    	messages: []
    });
    this._fAutorunDisposerObservableValidationMessages = __mobx.reaction(
    	function() {
    		return Validation.transformValidationArrayToValidationMessages(this.oObservableValidation.results);
    	}.bind(this),
    	function(aValidationMessages) {
    		this.oObservableValidationMessages.messages = aValidationMessages;
    	}.bind(this),
    	true // Fire immediately
    );
    
    //	Merge messages when validation message array changes
    this._fAutorunDisposerValidationArrayMerge = __mobx.reaction(
    	function() {
    		return this.oObservableValidationMessages.messages.peek();
    	}.bind(this),
    	this._mergeMessageModelMessages.bind(this), // changes oAppModel
    	true // Fire immediately
    );​
    • Validation results are only collected from fields that have a corresponding ‘<PropertyName>$Validation’ property
  3. Replace the ‘MessageManager’ message model listener, in order to merge messages instead of calling ‘_updateMessageCount()’:
    this._oMessageModelBinding.attachChange(this._mergeMessageModelMessages, this);​
    1. Update the detachChange() call in onExit accordingly
    2. Remove ‘_updateMessageCount()’
  4. Define ‘_mergeMessageModelMessages()’ as a new method of the ‘Application’ controller:
    _mergeMessageModelMessages: function() {
    
    	var oMessageManager = sap.ui.getCore().getMessageManager();
    
    	this.getView().getModel().getObservable().validationMessages =
    		oMessageManager.getMessageModel().getData().concat(this.oObservableValidationMessages.messages.peek());
    },​
  5. Change ‘oAppModel.messageCount’ to a computed property:
    get messageCount() {
    	return this.validationMessages.length;
    },​
  6. Change the instantiation and binding of ‘oMessagePopover’ to the merged array of messages:
    // Instantiation
    var oMessagePopover = new MessagePopover({
    	items: {
    		path: "/validationMessages",
    		template: oMessageTemplate
    	}
    });
    
    // onInit():
    oMessagePopover.setModel(oAppModel);​
  7. Remove the ‘change’ event from the first and last name inputs as ‘onChangeRevalidate’ is no longer needed here
  8. Edit validateDomain() in ‘Application.controller.js’ and comment out validation of the now-reactive model properties:
    validateDomain: function() {
    
    	try {
    		var bValid = true;
    
    		// Now reactive // bValid = bValid && this._validateInput("inputSWFirstName");
    		// Now reactive // bValid = bValid && this._validateInput("inputSWLastName");
    		bValid = bValid && this._validateInput("inputSWAge");
    
    		this.getView().getModel().setProperty("/validateDomainCallResult", bValid);
    	} catch (oEx) {
    		this.getView().getModel().setProperty("/validateDomainCallResult", false);
    		throw oEx;
    	}
    },​
    1. Instead of setting ‘/canSubmit’, set the application model property ‘/validateDomainCallResult’
    2. Turn ‘/canSubmit’ into a computed property that combines the results of the ‘traditional’, and reactive validation, and add ‘/validateDomainCallResult’ to the application model:
      get canSubmit() {
      	return this.validateDomainCallResult && that.oObservableValidation.results.length === 0;
      },​
      validateDomainCallResult: false,
      1. Remember to add ‘var that = this;’ to the top of onInit()
        ‘that.oObservableValidation.results.length’ can be used to assess validity, because we only collect ‘Error’ validation results into the array, and it is updated upon every change
  9. Test the app
    1. Validation message count, validation messages (also with curly brackets), and the ‘Submit’ button behave correctly
      • ‘Validation.transformValidationArrayToValidationMessages()’ takes care of escaping curly brackets [{}] in (reactive) messages
    2. Empty ‘First Name’ and ‘Last Name’ are still accepted

Reactive field group validation

Let us now implement the condition that “Snow White must have at least one valid name”.

We define a new computed property ‘FullName’ and validate it:

  1. In ‘models.js’, add the following properties to state.SnowWhite:
    get FullName() {
    	return (this.FirstName ? (this.FirstName + (this.LastName ? " " : "")) : "") + (this.LastName || "");
    },
    get FullName$Validation() {
    
    	var bValid = this.FirstName$Validation.valid && this.LastName$Validation.valid && Boolean(this.FullName);
    	return {
    		valid: bValid,
    		valueState: bValid ? "None" : "Error",
    		valueStateText: bValid ? "" : oI18nResourceBundle.getText("atLeastCorrectFirstOrLast")
    	};
    },
    ​
    1. Make ‘oI18nResourceBundle’ an argument of createDomainModel(), and call it like this in the controller:
      var oDomainModel = models.createDomainModel(
      	this.getOwnerComponent().getModel("i18n").getResourceBundle()
      );​
    2. Add to i18n.properties:
      atLeastCorrectFirstOrLast=Enter at least a correct first name or last name. If given, both must be correct.​
  2. Add a control to show the full name on the view:
    <f:FormElement label="">
    	<f:fields>
    		<Text text="`{domain&gt;/SnowWhite/FullName}'"/>
    	</f:fields>
    </f:FormElement>​
  3. Test the app:
    1. ‘Submit’ gets enabled and disabled correctly
    2. The validation message “Enter at least …” immediately appears in the MessagePopover
    3. We see no full name validation messages on the first and last name input fields

Displaying field group validation results

UI5 controls that were not yet changed by the user do not show validation messages, regardless of the value of the property they are bound to. Therefore we expect to see no message in the MessagePopover either, initially. On the other hand we expect the first and last name input controls to show an error ‘valueState’ with “Enter at least …” ‘valueStateText’ after modification.

For the former (i.e. no error state unless changed) we need to keep track of changes initiated by the user. For the latter (i.e. show field group ‘valueStateText’ on group input controls) we need ‘valueState’ and ‘valueStateText’ to show different values depending on validation results.

  1. In ‘models.js’, add and initialize ‘$Changed’ properties to every reactive model property:
    • FirstName$Changed: false,
    • LastName$Changed: false,
    • get FullName$Changed() { // Indicates "changed by user"
      	return this.FirstName$Changed || this.LastName$Changed;
      },
  2. Add ‘change’ event handlers to first and last name inputs in the view XML:
     ​change="onChangeSetChanged"

    implemented as (in controller):

    onChangeSetChanged: function(oEvent) {
    
    	var oModel = oEvent.getSource().getBinding("value").getModel();
    	var sPath = oEvent.getSource().getBinding("value").getPath();
    
    	oModel.setProperty(sPath + "$Changed", true);
    },
  3. In ‘models.js’, add a ‘master switch’ ‘$ignoreChanged’ to the root level of the ‘state’ observable:
    //
    $ignoreChanged: false // If true, set $Validation.valueState regardless of $Changed state​
    • We will use ‘$ignoreChanged’ to reveal the validation state of controls that were not yet changed by the user.
  4. Change ‘FullName$Validation’ to: (only ‘valueState’ is changed below)
    get FullName$Validation() {
    
    	var bValid = this.FirstName$Validation.valid && this.LastName$Validation.valid && Boolean(this.FullName);
    	return {
    		valid: bValid,
    		valueState: bValid ? "None" : (this.FullName$Changed || state.$ignoreChanged ? "Error" : "None"),
    		valueStateText: bValid ? "" : oI18nResourceBundle.getText("atLeastCorrectFirstOrLast")
    	};
    },​
  5. Change calls to ‘getModelPropertyValidationByType()’, replacing:
    /* ignoreChanged */ true​

    with

    state.$ignoreChanged
  6. Add the master switch to ‘Applicaiton.view.xml’ above ‘btnSubmit’:
    <ToggleButton text="{i18n>revealValueStateText}" pressed="{domain&gt;/$ignoreChanged}" />
  7. Add ‘i18n.properties’ text “revealValueStateText=Reveal value state”

Now change the binding of ‘valueState’ and ‘valueStateText’ of the first and last name:

  1. Edit the application view:
    <Input id="inputSWFirstName"
    	value="{path: 'domain&gt;/SnowWhite/FirstName', type: 'org.debian.lkajan.mobxTutorial.model.type.String', constraints: {search: '^(|[^0-9\\s]{3,})$'}}"
    	valueState="{parts: ['domain&gt;/SnowWhite/FirstName$Validation', 'domain&gt;/SnowWhite/FullName$Validation'], formatter: '.formatterValueStateFieldPair'}"
    	valueStateText="{parts: ['domain&gt;/SnowWhite/FirstName$Validation', 'domain&gt;/SnowWhite/FullName$Validation'], formatter: '.formatterValueStateTextFieldPair'}"
    	change="onChangeSetChanged"/>
    <Input id="inputSWLastName"
    	value="{path: 'domain&gt;/SnowWhite/LastName', type: 'org.debian.lkajan.mobxTutorial.model.type.String', constraints: {search: '^(|[^0-9\\s]{3,})$'}}"
    	valueState="{parts: ['domain&gt;/SnowWhite/LastName$Validation', 'domain&gt;/SnowWhite/FullName$Validation'], formatter: '.formatterValueStateFieldPair'}"
    	valueStateText="{parts: ['domain&gt;/SnowWhite/LastName$Validation', 'domain&gt;/SnowWhite/FullName$Validation'], formatter: '.formatterValueStateTextFieldPair'}"
    	change="onChangeSetChanged"/>:
    ​
  2. Implement ‘.formatterValueStateFieldPair’ and ‘.formatterValueStateTextFieldPair’ in the ‘Application’ controller as:
    formatterValueStateFieldPair: function(val1, val2) {
    	if (val1) {
    		return val1.valid ? val2.valueState : val1.valueState;
    	} else {
    		return "None"; // When the model is not yet assigned, we get val1 === null
    	}
    },
    
    formatterValueStateTextFieldPair: function(val1, val2) {
    	if (val1) {
    		return val1.valid ? val2.valueStateText : val1.valueStateText;
    	} else {
    		return "None"; // When the model is not yet assigned, we get val1 === null
    	}
    },​
    • The formatters return the second ‘valueState’ and ‘valueStateText’ in case the first ‘val1’ validation is correct
  3. Test the app
    1. The ‘Reveal value state’ button makes reactive validation messages appear (and disappear) regardless of changed state
    2. After changes, the validation message is shown
    3. If you input the first name ‘Sn0w’ and last name ‘White’, the controls show different validation messages, each appropriate for its content

We now have fully implemented the “Snow White must have at least one valid name” condition.

First name with alternative formatting and ‘valueStateText’

Take a look at the Plunker of the finished tutorial (sources on GitHub). This app contains an alternative input field for the first name, with different formatting, parsing and error ‘valueStateText’:

This is achieved by extending ‘sap.ui.model.type.String’ to define a new ‘StringWithApple’ type that provides its formatter, parser and validator. The type is instantiated in ‘models.js’, and used in a new computed validation property ‘FirstName$WithApple$Validation’. The new property is only used to provide the ‘valueState’ and ‘valueStateText’ of the alternative input field. The validation message of ‘<PropertyName>$Validation’ (so ‘FirstName$Validation’) continues to appear in the MessagePopover.

Reactive dynamic domain model content

To demonstrate dynamic changes to the domain model, let us now allow up to three dwarfs to be added, with the same name requirements as Snow White’s.

We want adding a dwarf to be as simple as an array push(). To demonstrate correct behavior when binding nonexistent properties, we predefine the dwarfs’ controls in the view XML. This complicates things, because the controls are initially bound to nonexistent properties of nonexistent dwarf objects. Such properties are not automatically tracked (i.e. observable).

  1. Add ‘Dwarfs: []’ and ‘get DwarfCount()’ to the observable ‘state’ variable of ‘createDomainModel()’ in ‘models.js’ as shown in the finished tutorial:
    Dwarfs: [],
    get DwarfCount() {
    	return this.Dwarfs.length;
    },​
  2. Add two new buttons ‘btnAddDwarf’ and ‘btnRemoveDwarf’ to the application view above the ‘ToggleButton’:
    <Button id="btnAddDwarf" text="{i18n>addDwarf}" enabled="{= ${domain&gt;/DwarfCount} &lt; 3}" press="onPressAddDwarf"/>
    <Button id="btnRemoveDwarf" text="{i18n>removeDwarf}" enabled="{= ${domain&gt;/DwarfCount} &gt; 0}" press="onPressRemoveDwarf"/>​
  3. Copy the ‘<f:FormContainer title=”{i18n>dwarfs}” visible=”true”>…</f:FormContainer>’ XML segment of the dwarfs from the finished tutorial to the application view XML
  4. Add press event handlers ‘onPressAddDwarf’ and ‘onPressRemoveDwarf’ to the controller:
    onPressAddDwarf: function(oEvent) {
    
    	var oDomainObservable = this.getView().getModel("domain").getObservable();
    
    	if (oDomainObservable.DwarfCount < 3) {
    		var oDwarf = models.createDwarf();
    		oDomainObservable.Dwarfs.push(oDwarf);
    	}
    },
    
    onPressRemoveDwarf: function(oEvent) {
    
    	var oDomainObservable = this.getView().getModel("domain").getObservable();
    
    	if (oDomainObservable.DwarfCount > 0) {
    		--oDomainObservable.Dwarfs.length;
    	}
    },​
    • Note how ‘onPressAddDwarf’ simply pushes a new dwarf object onto the ‘Dwarfs’ array of the domain model
  5. Add ‘createDwarf()’ to models.js:
    createDwarf: function() {
    	return {
    		FirstName: "",
    		LastName: ""
    	};
    }​
  6. Test the app:
    1. Toggle ‘Reveal value state’ and add dwarfs: dwarf names appear to be valid even when empty. We haven’t added any validation rules for the dwarfs yet.

Dynamic domain model content validation

We can add dwarf name validation reactively, as new dwarfs are push()ed to the ‘Dwarfs’ array of the domain model. This makes sure validation logic stays close to the domain model:

  1. Insert the following ‘// Dwarf handling’ ‘__mobx.observe()’ call to the implementation of ‘createDomainModel’, after the ‘var state = …’ command:
    // Dwarf handling
    __mobx.observe(state, "Dwarfs", function(change0) { // Returns a disposer
    	if (change0.type === "update") {
    		__mobx.intercept(state.Dwarfs, function(change) {
    			// New dwarf(s) added
    			if (change.type === "splice" && change.added.length) {
    				var oDwarfExtension = {
    					FirstName$Changed: false,
    					get FirstName$Validation() {
    						return Validation.getModelPropertyValidationByType(this, "FirstName", oMobxModelTypeStringName, "string", state.$ignoreChanged);
    					},
    					//
    					LastName$Changed: false,
    					get LastName$Validation() {
    						return Validation.getModelPropertyValidationByType(this, "LastName", oMobxModelTypeStringName, "string", state.$ignoreChanged);
    					},
    					//
    					get FullName() {
    						return (this.FirstName ? (this.FirstName + (this.LastName ? " " : "")) : "") + (this.LastName || "");
    					},
    					get FullName$Changed() { // Indicates "changed by user"
    						return this.FirstName$Changed || this.LastName$Changed;
    					},
    					get FullName$Validation() {
    						var bValid = this.FirstName$Validation.valid && this.LastName$Validation.valid && Boolean(this.FullName);
    						return {
    							valid: bValid,
    							valueState: bValid ? "None" : (this.FullName$Changed || state.$ignoreChanged ? "Error" : "None"),
    							valueStateText: bValid ? "" : oI18nResourceBundle.getText("atLeastCorrectFirstOrLast")
    						};
    					}
    				};
    
    				for (var i = 0; i < change.added.length; ++i) {
    					if (!__mobx.isObservableObject(change.added[i])) {
    						change.added[i] = __mobx.observable(change.added[i]);
    					}
    					var oDwarf = change.added[i];
    					__mobx.extendObservable(oDwarf, oDwarfExtension);
    				}
    			}
    			return change;
    		});
    	}
    }, true); // invokeImmediately​
    • The MobX observer and interceptor make sure that every time a dwarf object is added, it is changed into an observable, enhanced with computed properties for validation.
  2. Test the app:
    1. Toggle ‘Reveal value state’ and add dwarfs: validation is performed correctly. Validation messages appear correctly, the ‘Submit’ button reacts to changes as intended.

This concludes the tutorial.

MobX >= 4.0.0

Update 20180329: The finished tutorial sources now use MobX 4.1.1, which provides an interface for direct observable manipulation. Because of this, especially the part that concerns ‘$Changed’ properties is now simplified: these properties no longer have to be initialized in the model. This change is not yet reflected in the text of this tutorial.

Update 20180330: Button ‘btnMessagePopup’ and dwarf 3 have been changed from bindings to reactive property management. This makes a difference especially in the case of ‘btnMessagePopup’. The implementation with bindings –

<Button icon="sap-icon://message-popup" visible="true" text="{= ${/messageCount} &gt; 0 ? ${/messageCount} : ''}"
	type="{= ${/messageCount} &gt; 0 ? 'Emphasized' : 'Default'}" press="onValidationMessagesPress"/>​

– used 3 instances of the property binding ‘/messageCount’, each receiving and processing a ‘change’ event upon each property update. Reactive management of the ‘text’ and ‘type’ of the button allows one single, debounced update for the whole control.

Summary

Following this tutorial, you extended a normal UI5 application with reactive logic. You changed the original app to implement new features, such as field group validation and dynamic model content. The overhead to incorporate the reactive state management was minimal: most of your coding was concerned with the new features.

You are new experienced in reactive programming for UI5 and see its advantages. You also know where to find further information on the topic.

Further reading

Afterword

Thank you for going through this tutorial. I hope you found it useful. Perhaps it raised questions as well. Do follow the above, or other links to satisfy your curiosity – a sure way to deepen your understanding.

If you plan to use what was shown above, definitely check out Christian Theilemann’s “Reactive state management in SAPUI5 via MobX“, and his comments at the bottom of “Building a SAPUI5 application with Predictable State Container“.

Assigned Tags

      1 Comment
      You must be Logged on to comment or reply to a post.
      Author's profile photo Laszlo Kajan
      Laszlo Kajan
      Blog Post Author

      This may be a little bit ahead of its time.