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