UI5: routing parametrico

Aggiornamento 06/01/2020: migliorato controller della pagina di dettaglio, maggiori informazioni nel testo.

Nella scorsa puntata abbiamo visto come usare le funzionalità di routing di UI5 per navigare tra una pagina e l'altra. In questo post vedremo un esempio di route parametrica, in cui cioè è previsto l'inserimento nel pattern di un parametro libero. Useremo questo parametro per passare da una pagina principale con una lista di oggetti a una pagina di dettaglio con informazioni sullo specifico oggetto selezionato. Il parametro che passeremo dalla vista principale alla vista di dettaglio sarà l'id dell'oggetto da visualizzare.

Partiamo da una applicazione minima generata come visto qui. Iniziamo con il creare una pagina MainView, che come visto in precedenza conterrà semplicemente il componente app.

 <!-- it.tutorial.provaUI5\uimodule\webapp\view\MainView.view.xml -->
 
 <mvc:View controllerName="it.tutorial.provaUI5.controller.MainView"
  displayBlock="true"
  xmlns="sap.m"
  xmlns:mvc="sap.ui.core.mvc">

  <App id="idAppControl" />
  
</mvc:View>

Nel controller del MainView, nel metodo onInit, mettiamo la creazione del nostro modello dati fittizio. Il modello, che chiameremo "products", è costruito a partire da un JSON hardcoded;  viene salvato nell'owner component (anzichè in una singola vista), in modo tale da essere disponibile in tutta l'applicazione.

// it.tutorial.provaUI5\uimodule\webapp\controller\MainView.controller.js

sap.ui.define([
  "it/tutorial/provaUI5/controller/BaseController",
  "sap/ui/model/json/JSONModel"
], function(Controller, JSONModel) {
  "use strict";

  return Controller.extend("it.tutorial.provaUI5.controller.MainView", {

    onInit: function(){

      // Modello GLOBALE (this.getOwnerComponent()...)
      let productsModel = new JSONModel({
        elenco: [
          {
            id: 'first',
            name: 'Primo Prodotto',
            price: 1000.0
          },
          {
            id: 'second',
            name: 'Secondo Prodotto',
            price: 2000.0
          },
          {
            id: 'third',
            name: 'Prodotto Economico',
            price: 50.0
          }

        ]
      });
      this.getOwnerComponent().setModel(productsModel, "products");

    }

  });
});

Pagina di lista

Creiamo la prima pagina, che chiameremo "ListView". In questa pagina mostreremo una lista dei prodotti presenti nel nostro modello. Come sempre bisogna creare view e controller.

<!-- it.tutorial.provaUI5\uimodule\webapp\view\ListView.view.xml -->

<mvc:View controllerName="it.tutorial.provaUI5.controller.ListView"
  displayBlock="true"
  xmlns="sap.m"
  xmlns:mvc="sap.ui.core.mvc">

  <Page title="Elenco Prodotti">
    <List
      items="{products>/elenco}">
      <items>
        <StandardListItem
          title="{products>name}"
          description="{products>price}"
          type="Navigation"
          press="onPress"
        />
      </items>	
    </List>
  </Page>
  
</mvc:View>

Mi raccomando: specificare il type "Navigation" nel list item. Il valore di default è "Inactive", che lo rende non cliccabile e può causare una discreta quantità di insulti in fase di debug.

Si noti che la lista usa come sorgente di dati l'oggetto "/elenco" nel modello "products".

items="{products>/elenco}"

Lo slash davanti a "elenco" è necessario per specificare il percorso assoluto del nostro array all'interno del modello. Il template dell'oggetto della lista (StandardListItem) ha invece riferimenti relativi al singolo elemento dell'array, quindi non richiede lo slash. E' comunque necessario specificare il nome del modello.

title="{products>name}"

Il controller della ListView è così definito.

// it.tutorial.provaUI5\uimodule\webapp\controller\ListView.controller.js

sap.ui.define([
  "it/tutorial/provaUI5/controller/BaseController"
], function(Controller) {
  "use strict";

  return Controller.extend("it.tutorial.provaUI5.controller.ListView", {

    onPress: function(event) {
      const object = event.getSource()
        .getBindingContext("products").getObject();
      var oRouter = sap.ui.core.UIComponent.getRouterFor(this);
      oRouter.navTo("ProductDetail", {
        id: object.id
      });      
    }    

  });

});

Come si vede, non è richiesto alcun codice per la visualizzazione, che è già gestita dai binding nella view. Quello che viene definito è la funzione onPress, chiamata al click su un elemento della lista. Nello specifico, la funzione svolge tre operazioni:

  • Recupera l'oggetto cliccato nella lista.
  • Recupera il router dell'applicazione.
  • Chiede al router di navigare su una specifica route ProductDetail (che sarà quella associata alla pagina di dettaglio) passando un parametro aggiuntivo "id" che contiene l'id dell'oggetto cliccato.

Pagina di dettaglio

A questo punto possiamo creare la pagina di dettaglio, che chiameremo DetailView. Come al solito creiamo view e controller. Cominciamo dal controller.

// it.tutorial.provaUI5\uimodule\webapp\controller\DetailView.controller.js

sap.ui.define([
  "it/tutorial/provaUI5/controller/BaseController",
  "sap/ui/model/json/JSONModel"
], function(Controller, JSONModel) {
  "use strict";

  return Controller.extend("it.tutorial.provaUI5.controller.DetailView", {

  onInit: function(){
    
    var oRouter = this.getRouter();
    oRouter.getRoute("ProductDetail")
      .attachMatched(this._onRouteMatched, this);	
    
    // Creazione e assegnazione modello pagina
    const prodottoModel = new JSONModel();
    this.getView().setModel(prodottoModel);
    
  },

  _onRouteMatched : function (oEvent) {

    const args = oEvent.getParameter("arguments");

    const prodotti = this.getOwnerComponent()
      .getModel('products').getData().elenco
    const prodotto = prodotti.find(function(item){
      return item.id === args.id
    })

    // Assegnazione dati al modello della pagina
    this.getView().getModel().setData(prodotto);

  }

  });

});

Nel controller della pagina di dettaglio abbiamo dovuto implementare due funzioni. La prima è la solita onInit; in questa attacchiamo alla nostra route ProductDetail un callback (mediante le api del router), che verrà chiamato al momento dell'apertura della pagina stessa. Sempre in questo metodo andiamo a inizializzare (senza contenuto) il modello che verrà usato poi dalla view per mostrare i dati relativi al prodotto selezionato.

La seconda è il callback vero e proprio. In questa funzione facciamo quattro cose:

  • Recuperiamo il parametro "id" passato in precedenza.
  • Recuperiamo l'elenco completo di prodotti dal nostro modello principale.
  • Cerchiamo nell'elenco il prodotto con l'id che stiamo cercando.
  • Impostiamo il prodotto trovato come dato nel modello che abbiamo precedentemente associato alla pagina.
Nota: in una precedente versione di questo post avevo gestito differentemente il modello in questo controller. Invece che creare il modello nella onInit e poi fare un setData nel callback della route, mi limitavo a creare e associare un nuovo modello alla vista sempre dalla route. Ripensandoci, ricreare un modello a ogni visualizzazione della pagina non è una buona idea, e può portare a memory leak e spreco di risorse. Meglio creare una sola volta il modello e aggiornarlo all'occorrenza.

Grazie a questo callback, l'oggetto indicato dall'id è ora associato alla nostra vista. La vista sarà così:

<mvc:View controllerName="it.tutorial.provaUI5.controller.DetailView"
  displayBlock="true"
  xmlns="sap.m"
  xmlns:mvc="sap.ui.core.mvc"
  xmlns:f="sap.ui.layout.form">

  <Page title="Dettagli prodotto">
        
    <VBox class="sapUiSmallMargin">
      <f:SimpleForm title="Informazioni di base">  
        <f:content>
          <Label text="Id"/>
          <Text text="{/id}"/>
          <Label text="Nome"/>
          <Text text="{/name}"/>
          <Label text="Prezzo"/>
          <Text text="{/price}"/>
        </f:content>
      </f:SimpleForm>
    </VBox>

  </Page>
  
</mvc:View>

Non è nulla di particolare. L'unica nota interessante è che nei componenti Text andiamo ad assegnare i collegamenti alle singole proprietà dell'oggetto che abbiamo bindato prima; il modello è anonimo, quindi basta specificare il nome della proprietà.

text="{/name}"

Routing

Per concludere, aggiungiamo le relative entry nelle routes e nei targets del manifest.json.

...
      "routes": [
        {
          "name": "FirstRoute",
          "pattern": "",
          "target": [
            "ListView"
          ]
        },
        {
          "name": "ProductDetail",
          "pattern": "detail/{id}",
          "target": [
              "DetailView"
          ]
        }		
      ],
...

La prima route non ha niente di speciale. La seconda, invece, ha una particolarità: nel pattern è inserito un parametro arbitrario denominato "id" (identificato dalle parentesi graffe).

Per quanto riguarda i target, invece:

...      
      "targets": {
        "ListView": {
          "viewType": "XML",
          "viewLevel": 1,
          "viewId": "ListView",
          "viewName": "ListView"
        },
        "DetailView": {
            "viewType": "XML",
            "viewLevel": 1,
            "viewId": "DetailView",
            "viewName": "DetailView"
        }
      }
...

Voilà! Se tutto è fatto come si deve dovremmo avere questo risultato:

Si noti come la finestra di dettaglio riporta nell'url il pattern come definito nella seconda route, completo di id:

localhost:8080/index.html#/detail/first