SAPUI5 más allá del hola mundo: Component, JSONModel y data binding
Construye una tabla de multiplicar en SAPUI5 con Component, dos modelos JSON, binding de un input y de una tabla, textos traducibles y validación con MessageToast.
El hola mundo de SAPUI5 pinta un texto en pantalla. El siguiente paso es lo que hace cualquier app real: el usuario escribe algo, la app calcula y muestra el resultado, sin tocar el DOM a mano. Para eso existen los modelos y el data binding.
Vamos a construir una tabla de multiplicar: escribes un número, pulsas Calcular y aparece su tabla del 1 al 10.
Lo que vas a practicar:
- arrancar la app con un Component (como las apps Fiori de verdad);
- dos JSONModel con nombre: uno para lo que escribe el usuario y otro para lo que muestra la tabla;
- binding de un
Input(en los dos sentidos) y de unaTable(de agregación); - textos en archivos i18n, en español e inglés;
- validación con
MessageToast.
Requisitos
- Node.js 20 o superior.
- Un editor (VS Code o SAP Business Application Studio).
No necesitas un sistema SAP: UI5 se carga desde su CDN público.
1. El proyecto
tabla-multiplicar/
├── 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: la única dependencia es la CLI de UI5, que hace de servidor de desarrollo:
{
"name": "tabla-multiplicar",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "ui5 serve -o index.html"
},
"devDependencies": {
"@ui5/cli": "^4"
}
}
ui5.yaml: le dice a la CLI que esto es una aplicación y cómo se llama:
specVersion: "4.0"
metadata:
name: demo.tabla
type: application
2. index.html: arrancar con un Component
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Tabla de multiplicar</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>
La diferencia con un hola mundo está en dos líneas:
on-init="module:sap/ui/core/ComponentSupport": en vez de crear controles en un script, UI5 busca en la página losdivcondata-sap-ui-componenty arranca el componente que indican.resource-roots: «todo lo que empiece pordemo.tablaestá en esta carpeta». Así UI5 encuentrademo/tabla/Component.js, la vista y el controlador.
3. manifest.json: la ficha de la app
{
"_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"
}
}
}
El manifest (o app descriptor) describe la app sin código: su identificador, sus librerías y su vista raíz. El Fiori Launchpad lo lee para saber cómo arrancarla.
4. Component.js: los modelos
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);
// Lo que escribe el usuario
this.setModel(new JSONModel({ number: 7 }), "form");
// Lo que muestra la tabla
this.setModel(new JSONModel({ items: [] }), "result");
// Textos traducibles
this.setModel(new ResourceModel({ bundleName: "demo.tabla.i18n.i18n" }), "i18n");
}
});
});
manifest: "json": el componente lee su configuración delmanifest.json.IAsyncContentCreation: la vista raíz se crea de forma asíncrona, que es lo recomendado.- Modelos con nombre (
form,result,i18n): al definirlos en el componente, todas sus vistas los ven. En la vista se usan como{form>/number}.
¿Por qué dos modelos JSON y no uno? Por claridad: form guarda la entrada del usuario y result, lo calculado. Cuando la app crezca, sabrás siempre dónde vive cada dato.
5. La vista: el 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>
Aquí está el corazón del ejercicio. Tres tipos de binding:
| Binding | Ejemplo | Qué hace |
|---|---|---|
| De propiedad, en los dos sentidos | value="{form>/number}" |
El input muestra el valor del modelo y, cuando el usuario escribe, actualiza el modelo solo. Con valueLiveUpdate="true", en cada tecla |
| De agregación | items="{result>/items}" |
La tabla crea una fila por cada elemento del array. El ColumnListItem es la plantilla de esa fila |
| Relativo | {result>factor} |
Dentro de la plantilla, la ruta no empieza por /: se resuelve contra el elemento de esa fila |
Fíjate en text="× {result>factor}": se puede mezclar texto fijo y binding en la misma propiedad.
submit=".onCalculate" hace que pulsar Enter en el input calcule igual que el botón.
6. El controlador: la lógica
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);
}
});
});
Observa lo que no hace: no busca el input por su id ni toca la tabla. Lee del modelo form, escribe en el modelo result, y el binding se encarga de que la pantalla refleje los cambios. Esa separación es la idea central de UI5.
Number(…): lo que escribe el usuario llega como texto. Conviértelo y valida lo que de verdad necesitas (un entero mayor que cero): una comprobación como== 0dejaría pasar un-3o un2.5.- El mensaje sale de i18n:
getResourceBundle().getText("errorPositive"). Ningún texto visible queda escrito en el código.
7. Textos traducibles
i18n.properties (por defecto) e 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
Trampa habitual: si solo tienes
i18n.properties(en español) ei18n_en.properties, un navegador en español puede ver la app en inglés. Al no encontrari18n_es, UI5 prueba primero su idioma de respaldo, que es el inglés, y solo después el archivo por defecto. Crea siempre el archivo de cada idioma que soportes.
8. Ejecútala
npm install
npm start
Se abre el navegador con la app. Prueba:
- Pulsa Calcular con el 7 que viene por defecto: aparece la tabla del 7.
- Escribe 12 y pulsa Enter: la tabla se actualiza.
- Escribe -3: aparece el aviso y la tabla no cambia.
- Añade
?sap-ui-language=ena la URL: la app sale en inglés.
Resumen
| Pieza | Responsabilidad |
|---|---|
index.html |
Cargar UI5 y arrancar el componente |
manifest.json |
Describir la app: id, librerías, vista raíz |
Component.js |
Crear los modelos que comparten las vistas |
| Vista XML | Qué se ve y a qué dato está enlazado cada control |
| Controlador | La lógica: leer y escribir en los modelos |
| i18n | Todos los textos visibles, por idioma |
Siguiente paso: declarar los modelos en el manifest.json (sección sap.ui5/models) en vez de crearlos en Component.js, y cambiar el JSONModel por un OData V4 servido por RAP. La vista apenas cambia: sigue haciendo binding, solo que ahora contra un servicio real.
Recibe los nuevos tutoriales
Un correo cuando publique algo nuevo. Sin spam; te das de baja cuando quieras.
Te enviaré un correo para confirmar la suscripción. Datos tratados según la política de privacidad.