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.
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 aTable(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 fordivs withdata-sap-ui-componentand starts the component they name.resource-roots: “everything starting withdemo.tablalives in this folder”. That is how UI5 findsdemo/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 frommanifest.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== 0would let-3or2.5through.- 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) andi18n_en.properties, a Spanish browser may show the app in English. Wheni18n_esis 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:
- Press Calculate with the default 7: the 7 times table appears.
- Type 12 and press Enter: the table updates.
- Type -3: the warning shows up and the table stays as it was.
- Add
?sap-ui-language=esto 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.
Get new tutorials by email
One email when something new is published. No spam; unsubscribe anytime.
I will email you to confirm your subscription. Data handled under the privacy policy.