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
[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