← Volver a tutoriales

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.

Also available in English →

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 una Table (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 los div con data-sap-ui-component y arranca el componente que indican.
  • resource-roots: «todo lo que empiece por demo.tabla está en esta carpeta». Así UI5 encuentra demo/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 del manifest.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 == 0 dejaría pasar un -3 o un 2.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) e i18n_en.properties, un navegador en español puede ver la app en inglés. Al no encontrar i18n_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:

  1. Pulsa Calcular con el 7 que viene por defecto: aparece la tabla del 7.
  2. Escribe 12 y pulsa Enter: la tabla se actualiza.
  3. Escribe -3: aparece el aviso y la tabla no cambia.
  4. Añade ?sap-ui-language=en a 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.