AlloyUI e applicazioni web

III parte: Funzioni per l‘auto-completedi

Lavorando con le applicazioni web, prima o poi ci si trova a dover realizzare un campo di testo ad auto-completamento in cui l'utente scrive ed il sistema propone immediatamente alcune risposte, analogamente a quanto avviene con Google Suggest. Alloy UI mette a disposizione un widget apposito per l'auto-completamento, vediamo quindi come usarlo.

Caso d'uso

La creazione di un campo ad auto-completamento è generalmente un'operazione che include la collaborazione di HTML, CSS, JavaScript e del sano codice Java. In questo articolo vedremo come fare a realizzare un campo ad auto-completamento sfruttando un widget della libreria di Alloy UI, molto interessante ma al solito poco documentato.

Sul sito [1] è possibile vedere una demo live del funzionamento di tale widget ma, superato il primo momento di stupore, ci si rende conto che tutti i dati dell'auto-completamento sono statici in pagina e quindi l'esempio è veramente molto di base e poco utile per la casistica standard del problema.

Supponiamo quindi di voler realizzare un'ipotetica pagina di gestione di un ufficio in cui dobbiamo selezionare il nome del responsabile attraverso l'autocomplete; ciò significa che avremo generato con il Service Builder le seguenti 2 entità:

  • Ufficio, con chiave primaria ufficioId e con un campo foreign key responsabileId;
  • Persona, con chiave primaria personaId e con un campo nominativo.

Iniziamo quindi ad analizzare la parte da visualizzare in pagina.

Pagina JSP: form HTML

Analizziamo il codice seguente:

 

 

 

          

Il codice sopra rappresenta semplicemente il form HTML che conterrà il campo di testo e segue gli standard di Liferay; tuttavia sono necessarie alcune spiegazioni.

Innanzitutto vengono recuperati dalla request 2 oggetti: ufficio e persona. Il primo serve per configurare il model-context di Alloy e istruire di conseguenza le taglib di input; il secondo, invece, non serve tanto all'autocomplete quanto piuttosto alla fase di visualizzazione del responsabile prevalorizzato.

Infatti se pensiamo alla casistica in cui si debba modificare il responsabile, ci rendiamo subito conto che bisogna visualizzare la pagina con il campo di testo valorizzato al nome del responsabile; ma tale nome non fa parte dell'ufficio (che ne possiede solamente una foreign key) ma bensì della persona impostata come responsabile. Pertanto, lato server, è necessario recuperare un'istanza dell'oggetto Persona e portarlo in pagina. Inoltre lo URL a cui punta il form è ovviamente un actionURL dal momento che dobbiamo fare un submit ed eseguire della logica di business.

Ricordiamo poi che tipicamente ogni campo autocomplete è caratterizzato da una casella di testo che conterrà del testo descrittivo per l'utente e da un campo nascosto che contiene il valore che deve essere poi gestito via codice (di solito è una primary key): nel caso specifico abbiamo il campo nominativo che sarà l'autocomplete vero e proprio e il campo nascosto responsabileId che rappresenta l'identificativo del responsabile selezionato (valorizzato via JavaScript).

Pagina JSP: Alloy UI

Sempre nella pagina JSP dobbiamo inserire tutto il codice JavaScript che si occupa di renderizzare l'autocomplete e di effettuare la chiamata Ajax per recuperare i dati delle persone da visualizzare.

Siccome la porzione di codice è abbastanza lunga, i commenti sono inseriti direttamente nel codice stesso, affinche' al lettore siano presentate le spiegazioni mentre scorre il codice.

<!—
       Questo URL rappresenta l'indirizzo invocato per la chiamata Ajax; 
       è di tipo resource perche' deve essere restituito un oggetto JSON. 
       Il parametro serve solamente a differenziare lato server che tipo
       di operazione eseguire.
-->
 

       

 
 


       // Per la gestione della sovraimpressione
       var overlayManager = new A.OverlayManager(); 
       overlayManager.register(‘other-conflicting-overlays');
       // definizione della fonte dei dati, ossia la URL Ajax 
       var dataSource = new A.DataSource.IO({
              source: ‘<%= autocompleteURL %>'
       });
       // creazione dell'oggetto Alloy per l'autocomplete
       var autocomplete = new A.AutoComplete({
              // id del div che contiene il campo di input (vedi JSP sopra)
              contentBox: ‘#autocompleteAjaxBox',
              // classe CSS da applicare
              cssClass: ‘autocomplete-ajax-input',
              // oggetto che rappresenta il datasource (definito sopra)
              dataSource: dataSource,
              // obbliga la selezione di un valore
              forceSelection: true,
              // id del campo di testo di input
              input: ‘#nominativo',
              // nome del campo restituito dal JSON da visualizzare
              matchKey: ‘nominativo',
              // numero massimo di elementi visualizzati
              maxResultsDisplayed: 30,
              // schema dei dati restituiti via JSON
              schema: {
                     // nome dell'oggetto JSON
                     resultListLocator: ‘response',
                     // elenco dei campi dell'oggetto JSON
                     resultFields: [‘personaId', ‘nominativo'] 
              },
              // tipo dei dati restituiti
              schemaType:'json',
              on: {
                     containerExpand: function(e){ 
                            overlayManager.register(this.overlay);
                            overlayManager.bringToTop(this.overlay);
                     },
                     itemSelect: function(event) {
                            // questo evento viene scatenato alla selezione di una voce
                            // dell'autocomplete e serve per valorizzare il campo di
                            // input e quello nascosto, per il submit
                            if (event != null && event.details != null &&
                                                    event.details[1] != null &&
                                                    event.details[1].nominativo != null) {
                                   var detail = event.details[1];
                                   A.one(‘#nominativo')
                                                    .val(detail.nominativo);
                                   A.one(‘#responsabileId')
                                                    .val(detail.responsabileId);
                            }
                     },
                     textboxKey: function(event) {
                            // resetto l'ID quando l'utente inizia a digitare qualcosa. 
                            // serve ad evitare che l'utente visualizzi un elenco di voci
                            // dall'autocomplete senza però selezionarne alcuno, evitando
                            // quindi di inviare al submit il vecchio valore di id.
                            // resettando il valore, l'utente deve selezionare qualcosa.
                            A.one(‘#responsabileId').val(0);
                     }
              }
       });
      
       // preparazione della query di richiesta 
       autocomplete.generateRequest = function(query) { 
              return {
                     request: ‘&q=‘ + query
              };
       }
 
       // visualizzazione del campo autocomplete
       autocomplete.render();

Parte Java

L'ultimo pezzo mancante riguarda la parte Java che deve ricevere i dati inseriti dall'utente nella casella di input e preparare il risultato JSON; tutto si fa all'interno della portlet. Ricordiamo che lo URL Ajax che viene invocato è di tipo resourceURL, proprio perche' deve restituire contenuto non-HTML, nel nostro caso JSON. Anche qui, raccomandiamo la lettura dei commenti riportati nel codice.

@Override
public void serveResource(ResourceRequest resourceRequest,
                          ResourceResponse resourceResponse)
                                 throws IOException, PortletException {
    ThemeDisplay themeDisplay 
    = (ThemeDisplay) resourceRequest.getAttribute(WebKeys.THEME_DISPLAY);
    String cmd = resourceRequest.getParameter(Constants.CMD);
             
    // verifico il valore del comando che arriva dalla resourceURL definita nella JSP
    if ("autocomplete".equalsIgnoreCase(cmd)) {
        // recupero la query, ossia ciò che digita l'utente
        // il parametro "q" è definito sempre nella JSP, nella sandbox AlloyUI
        String query = resourceRequest.getParameter("q");
                        
        // preparo gli oggetti JSON
        JSONObject json = JSONFactoryUtil.createJSONObject();
        JSONArray results = JSONFactoryUtil.createJSONArray();
        // nome dell'oggetto JSON restituito in pagina
        // "response" è il nome definito nella JSP come "resultListLocator"
        json.put("response", results);
                        
        List persone;
        try {
            long groupId = themeDisplay.getScopeGroupId();
            if (query == null || "*".equals(query)) {
                // se l'utente digita l'asterisco oppure preme 
                // sul pulsante di ricerca senza aver digitato nulla,
                // devo visualizzare tutte le persone
                persone = PersonaLocalServiceUtil.findByGroupId(groupId);
            } else {
                // se l'utente ha digitato qualcosa devo eseguire
                // la query opportuna per recuperare ciò che serve; 
                // qui ognuno definisce il proprio metodo di ricerca
                persone = PersonaLocalServiceUtil.findByGroupIdKeywords(groupId, 
                                                                                                                      "%"+query+"%");
            }
        } catch (SystemException e) {
            // in caso di errori restituisco una lista vuota
            persone = new ArrayList();
        }
                        
        // per ogni persona restituita dalla query creo il relativo oggetto JSON
        // e lo inserisco nella lista da visualizzare in pagina
        for (Persona persona: persone) {
            JSONObject listEntry = JSONFactoryUtil.createJSONObject();
            // questi sono i campi dell'oggetto JSON, definiti sopra
            // nella sandbox AlloyUI
            listEntry.put("personaId", persona.getPersonaId());
            listEntry.put("nominativo", persona.getNominativo());
                                    
            results.put(listEntry);
        }
                        
        // restituisco la lista di persone in pagina
        PrintWriter writer = resourceResponse.getWriter();
        writer.println(json.toString());
        } else {
            super.serveResource(resourceRequest, resourceResponse);
        }
 }

Prevalorizzazione

Questo chiude il giro dell'autocomplete, ma manca ancora un piccolo pezzo da ricordare. Come detto in precedenza, nel caso in cui si debba tornare in pagina per la modifica, occorre inserire nella request alcuni oggetti che servono a prevalorizzare i campi del form:

  • ufficio, serve a prevalorizzare i 2 campi nascosti (ossia l'identificativo dell'ufficio e del responsabile);
  • persona, serve a prevalorizzare la casella di input con il nome del responsabile (che ha finalità solo descrittive per l'utente).

Pertanto nel metodo doView della portlet è necessario ricordarsi di valorizzare opportunamente tutto; quello che segue è un possibile esempio:

@Override
 
public void doView(RenderRequest renderRequest, RenderResponse renderResponse) 
              throws IOException, PortletException {
        try {
              // è compito della processAction valorizzare eventualmente l'ufficio 
              Ufficio ufficio = (Ufficio) renderRequest.getAttribute("ufficio"); 
              // inizializzo l'oggetto da portare in pagina 
              if (ufficio == null) 
              ufficio = new UfficioImpl();           
 
              Persona persona = new PersonaImpl(); 
              // recupero i dati della persona, se presente 
              if (ufficio.getResponsabileId() != 0)
                     persona = PersonaLocalServiceUtil.findByPrimaryKey(
                                                    ufficio.getResponsabileId());
                    
              // metto gli oggetti nella request
              renderRequest.setAttribute("ufficio", ufficio); 
              renderRequest.setAttribute("persona ", persona);          
 
              super.doView(renderRequest, renderResponse);
       } catch (Exception e) { 
              throw new PortletException(e);
       }
}

Submit del form

Al submit del form (che ricordiamo essere associato a un actionURL) verranno passati tutti i campi presenti in pagina, ossia:

  • ufficioId, identificativo dell'ufficio ossia del model del form;
  • responsabileId, identificativo del responsabile valorizzato con l'autocomplete;
  • nominativo, questo in realtà non serve alle logiche di business.

Quindi, facendo un esempio:

public void save(ActionRequest actionRequest, ActionResponse actionResponse) 
              throws Exception {
       long ufficioId = ParamUtil.getLong(actionRequest, "ufficioId");
       long responsabileId = ParamUtil.getLong(actionRequest, "responsabileId");
       // questo in realtà potrebbe non servire
       String nominativo = ParamUtil.getString(actionRequest, "nominativo");
      
       // Se l'ufficio non viene trovato, viene sollevata un'eccezione;
       // si lascia al lettore la gestione del try/catch
       Ufficio ufficio = UfficioLocalServiceUtil.findByPrimaryKey(ufficioId);
 
       if (responsabileId > 0) {
              // Viene fatto un controllo sull'esistenza della Persona.
              // Se la persona non viene trovata, viene sollevata un'eccezione;
              // si lascia al lettore la gestione del try/catch
              Persona persona = PersonaLocalServiceUtil.findByPrimaryKey(responsabileId);
              ufficio.setResponsabileId(persona.getPersonaId());
 
              ufficio = UfficioLocalServiceUtil.updateUfficio(ufficio);
       }
      
       actionRequest.setAttribute("ufficio", ufficio);
 
}

Conclusioni

In conclusione, si è visto come realizzare un campo di auto-completamento da inserire nelle proprie portlet, che si interfaccia direttamente con il portale per il recupero dei dati. Spero possa essere utile per tutti e farvi risparmiare tante ore di sonno!

Riferimenti

[1] Autocomplete demo

http://www.liferay.com/community/liferay-projects/alloy-ui/demo?title=community-liferay-projects-alloy-ui-demos-autocomplete

 

[2] Alloy UI

http://www.liferay.com/community/liferay-projects/alloy-ui/

 

[3] Alloy UI demo

http://www.liferay.com/community/liferay-projects/alloy-ui/demos

 

[4] Alloy UI API

http://alloyui.com/deploy/api/

 

[5] Alloy UI issue tracker

http://issues.liferay.com/browse/AUI

 

 

 

 

Condividi

Pubblicato nel numero
181 febbraio 2013
Marco nasce verso la fine degli anni Settanta e si trova presto alle prese con monitor e tastiera di un vecchio Olivetti M24 (a tutt
Articoli nella stessa serie
Ti potrebbe interessare anche