← Back to tutorials

SAPUI5 beyond hello world: Component, JSONModel and data binding

Build a multiplication table in SAPUI5 with a Component, two JSON models, input and table binding, translatable texts and MessageToast validation.

También disponible en español →

The SAPUI5 hello world puts some text on screen. The next step is what every real app does: the user types something, the app computes and shows the result, without touching the DOM by hand. That is what models and data binding are for.

We will build a multiplication table: type a number, press Calculate, and its table from 1 to 10 appears.

What you will practice:

  • starting the app with a Component (like real Fiori apps);
  • two named JSONModels: one for what the user types and one for what the table shows;
  • binding an Input (two-way) and a Table (aggregation);
  • texts in i18n files, in Spanish and English;
  • validation with MessageToast.

Requirements

  • Node.js 20 or later.
  • An editor (VS Code or SAP Business Application Studio).

You do not need an SAP system: UI5 loads from its public CDN.

1. The project

multiplication-table/
├── package.json
├── ui5.yaml
└── webapp/
    ├── index.html
    ├── manifest.json
    ├── Component.js
    ├── controller/App.controller.js
    ├── view/App.view.xml
    └── i18n/
        ├── i18n.properties
        ├── i18n_es.properties
        └── i18n_en.properties

package.json: the only dependency is the UI5 CLI, which acts as the development server:

{
  "name": "multiplication-table",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "ui5 serve -o index.html"
  },
  "devDependencies": {
    "@ui5/cli": "^4"
  }
}

ui5.yaml: tells the CLI this is an application and what it is called:

specVersion: "4.0"
metadata:
  name: demo.tabla
type: application

2. index.html: start with a Component

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Multiplication table</title>
  <script
    id="sap-ui-bootstrap"
    src="https://ui5.sap.com/resources/sap-ui-core.js"
    data-sap-ui-theme="sap_horizon"
    data-sap-ui-compat-version="edge"
    data-sap-ui-async="true"
    data-sap-ui-on-init="module:sap/ui/core/ComponentSupport"
    data-sap-ui-resource-roots='{ "demo.tabla": "./" }'>
  </script>
</head>
<body class="sapUiBody">
  <div data-sap-ui-component
       data-name="demo.tabla"
       data-id="container"
       data-settings='{ "id": "tabla" }'>
  </div>
</body>
</html>

The difference from a hello world is in two lines:

  • on-init="module:sap/ui/core/ComponentSupport": instead of creating controls in a script, UI5 looks for divs with data-sap-ui-component and starts the component they name.
  • resource-roots: “everything starting with demo.tabla lives in this folder”. That is how UI5 finds demo/tabla/Component.js, the view and the controller.

3. manifest.json: the app’s ID card

{
  "_version": "1.58.0",
  "sap.app": {
    "id": "demo.tabla",
    "type": "application",
    "i18n": "i18n/i18n.properties",
    "title": "{{appTitle}}",
    "description": "{{appDescription}}",
    "applicationVersion": { "version": "1.0.0" }
  },
  "sap.ui5": {
    "dependencies": {
      "minUI5Version": "1.120.0",
      "libs": {
        "sap.m": {},
        "sap.ui.core": {},
        "sap.ui.layout": {}
      }
    },
    "rootView": {
      "viewName": "demo.tabla.view.App",
      "type": "XML",
      "id": "app"
    }
  }
}

The manifest (or app descriptor) describes the app without code: its ID, its libraries and its root view. The Fiori Launchpad reads it to know how to start the app.

4. Component.js: the models

sap.ui.define([
  "sap/ui/core/UIComponent",
  "sap/ui/model/json/JSONModel",
  "sap/ui/model/resource/ResourceModel"
], (UIComponent, JSONModel, ResourceModel) => {
  "use strict";

  return UIComponent.extend("demo.tabla.Component", {
    metadata: {
      manifest: "json",
      interfaces: ["sap.ui.core.IAsyncContentCreation"]
    },

    init() {
      UIComponent.prototype.init.apply(this, arguments);

      // What the user types
      this.setModel(new JSONModel({ number: 7 }), "form");

      // What the table shows
      this.setModel(new JSONModel({ items: [] }), "result");

      // Translatable texts
      this.setModel(new ResourceModel({ bundleName: "demo.tabla.i18n.i18n" }), "i18n");
    }
  });
});
  • manifest: "json": the component reads its configuration from manifest.json.
  • IAsyncContentCreation: the root view is created asynchronously, which is the recommended way.
  • Named models (form, result, i18n): defined on the component, all its views can see them. In the view they are used as {form>/number}.

Why two JSON models instead of one? Clarity: form holds the user’s input and result holds what was computed. As the app grows, you will always know where each piece of data lives.

5. The view: binding

<mvc:View
  controllerName="demo.tabla.controller.App"
  xmlns:mvc="sap.ui.core.mvc"
  xmlns:form="sap.ui.layout.form"
  xmlns="sap.m"
  displayBlock="true">
  <App>
    <Page title="{i18n>appTitle}">
      <form:SimpleForm layout="ResponsiveGridLayout" editable="true">
        <Label text="{i18n>numberLabel}" labelFor="numberInput" />
        <Input
          id="numberInput"
          value="{form>/number}"
          type="Number"
          valueLiveUpdate="true"
          submit=".onCalculate" />
        <Button text="{i18n>calculate}" type="Emphasized" press=".onCalculate" />
      </form:SimpleForm>

      <Table
        id="resultTable"
        items="{result>/items}"
        noDataText="{i18n>noData}"
        class="sapUiSmallMarginTop">
        <columns>
          <Column><Text text="{i18n>colNumber}" /></Column>
          <Column><Text text="{i18n>colFactor}" /></Column>
          <Column hAlign="End"><Text text="{i18n>colResult}" /></Column>
        </columns>
        <items>
          <ColumnListItem>
            <cells>
              <Text text="{result>number}" />
              <Text text="× {result>factor}" />
              <ObjectNumber number="{result>result}" emphasized="true" />
            </cells>
          </ColumnListItem>
        </items>
      </Table>
    </Page>
  </App>
</mvc:View>

This is the heart of the exercise. Three kinds of binding:

Binding Example What it does
Property, two-way value="{form>/number}" The input shows the model value and, when the user types, updates the model on its own. With valueLiveUpdate="true", on every keystroke
Aggregation items="{result>/items}" The table creates one row per array element. The ColumnListItem is that row’s template
Relative {result>factor} Inside the template, the path does not start with /: it resolves against that row’s element

Note text="× {result>factor}": you can mix fixed text and binding in the same property.

submit=".onCalculate" makes pressing Enter in the input calculate, just like the button.

6. The controller: the logic

sap.ui.define([
  "sap/ui/core/mvc/Controller",
  "sap/m/MessageToast"
], (Controller, MessageToast) => {
  "use strict";

  return Controller.extend("demo.tabla.controller.App", {
    onCalculate() {
      const view = this.getView();
      const number = Number(view.getModel("form").getProperty("/number"));

      if (!Number.isInteger(number) || number <= 0) {
        const bundle = view.getModel("i18n").getResourceBundle();
        MessageToast.show(bundle.getText("errorPositive"));
        return;
      }

      const items = Array.from({ length: 10 }, (_, i) => ({
        number,
        factor: i + 1,
        result: number * (i + 1)
      }));

      view.getModel("result").setProperty("/items", items);
    }
  });
});

Notice what it does not do: it never looks up the input by ID or touches the table. It reads from the form model, writes to the result model, and binding makes the screen reflect the changes. That separation is UI5’s core idea.

  • Number(…): what the user types arrives as text. Convert it and validate what you actually need (a whole number greater than zero): a check like == 0 would let -3 or 2.5 through.
  • The message comes from i18n: getResourceBundle().getText("errorPositive"). No visible text is hard-coded.

7. Translatable texts

i18n.properties (default) and i18n_es.properties:

appTitle=Tabla de multiplicar
appDescription=Tabla de multiplicar con Component, JSONModel y binding
numberLabel=Número
calculate=Calcular
colNumber=Número
colFactor=Factor
colResult=Resultado
noData=Escribe un número y pulsa Calcular
errorPositive=Escribe un número entero mayor que cero

i18n_en.properties:

appTitle=Multiplication table
appDescription=Multiplication table with Component, JSONModel and binding
numberLabel=Number
calculate=Calculate
colNumber=Number
colFactor=Factor
colResult=Result
noData=Type a number and press Calculate
errorPositive=Type a whole number greater than zero

Common trap: if you only have i18n.properties (in Spanish) and i18n_en.properties, a Spanish browser may show the app in English. When i18n_es is missing, UI5 tries its fallback language first, which is English, and only then the default file. Always create a file for every language you support.

8. Run it

npm install
npm start

The browser opens with the app. Try this:

  1. Press Calculate with the default 7: the 7 times table appears.
  2. Type 12 and press Enter: the table updates.
  3. Type -3: the warning shows up and the table stays as it was.
  4. Add ?sap-ui-language=es to the URL: the app switches to Spanish.

Summary

Piece Responsibility
index.html Load UI5 and start the component
manifest.json Describe the app: ID, libraries, root view
Component.js Create the models shared by the views
XML view What is shown, and which data each control is bound to
Controller The logic: read from and write to the models
i18n Every visible text, per language

Next step: declare the models in manifest.json (the sap.ui5/models section) instead of creating them in Component.js, and swap the JSONModel for an OData V4 service served by RAP. The view barely changes: it still binds, only now against a real service.