Ciao a tutti!
Il problema di oggi è una roba che molti di voi faranno da una vita; siccome però mi sono trovato su un progetto a farlo, ci ho messo un po' ma sono arrivato anch'io! :)
Il problema è molto semplice: abbiamo un componente sul frontend che triggera il caricamento via AJAX di una porzione di HTML che, tuttavia, contiene del JS e che quindi dev'essere parsato.
Ovviamente, poi, se quando invio la form ci sono degli errori di validazione, quando torno in pagina dovrei:
- trovare caricato il frammento di HTML;
- trovare valorizzato il frammento di HTML con i valori selezionati.
Nel mio caso funzionale, l'obiettivo era che, dopo aver utilizzato l'inline search, dall'id dell'oggetto selezionato dovevo caricare tutte le entità figlie ad esso collegate e:
- permettere di selezionarli singolarmente;
- permettere di selezionarli tutti in un click.
Per non reinventare la ruota, l'idea che mi è venuta era quella di utilizzare il SearchContainer
insieme alla funzionalità del RowChecker
, così che mi venisse gratis tutta la parte di codice di impaginazione dei figli ma anche, appunto, la possibilità di selezioare tutti o alcuni dei record visualizzati.
Però sono in una JSP già caricata.. E come faccio a caricare questa roba, che, siccome parliamo di SearchContainer
, dev'essere renderizzata usando le taglib di portale?
Beh, la risposta è semplice: AJAX!
Ok, ma.. Come? :)
Cercando un po' sui progetti che abbiamo fatto, mi sono ricordato di una roba che aveva fatto Paolo Gambetti e che avevo molto elegante; quindi ho recuperato tutto e messo tutto insieme!
Vediamo ora un po' di codice..
Caricamento asincrono via AJAX dell'HTML (con relativo parsing)
La prima cosa che ho fatto, è stata quella di mappare in una funzione JS di pagina, la logica di caricamento e popolamento del componente. Questo l'ho fatto ovviamente perché devo gestire due casi:
- quando si carica la pagina la prima volta e, al trigger sul front end, devo caricare il frammento HTML;
- quando si ricarica la pagina e il componente deve riapparire popolato!
La funzione JS è molto semplice:
Liferay.provide(window, '<portlet:namespace/>loadSalesPoints', function(customerId) {
// Questa chiamata serve perché una volta che ho caricato il componente,
// questo viene registrato e al caricamento successivo ho un errore;
// ma se lo rimuovo funziona tutto! :)
Liferay.destroyComponent('<portlet:namespace/>salespointsSearchContainer');
customerIdField.val(customerId);
var portletURL = Liferay.PortletURL.createRenderURL();
portletURL.setPortletId('<%=PortletKeys.CALENDAR %>');
portletURL.setPlid(<%= plid %>);
portletURL.setWindowState('<%=LiferayWindowState.EXCLUSIVE.toString() %>');
portletURL.setParameter('customerId', customerId);
portletURL.setParameter('mvcPath', '/html/calendar/excel/planCalendar/show_sales_points.jsp');
pvContainerField.plug(A.Plugin.IO, {
failureMessage: 'In elaborazione...',
parseContent: true,
showLoading: true,
after: {
success: function(event) {
<c:if test="<%= !SessionErrors.isEmpty(renderRequest) %>">
var salesPointId = "<%= ParamUtil.getString(renderRequest, "salesPointIds")%>";
var salesPointArray = salesPointId.split(',');
// Recupero tutti i field con name "<portlet:namespace/>rowIds"
// leggo i loro valori e se corrispondono setto il flag checked
A.all('input[name=<portlet:namespace/>rowIds]').each(function (field) {
for (var i = 0; i < salesPointArray.length; i++) {
var arrValue = salesPointArray[i];
if (field.val() == arrValue) {
field.setAttribute('checked', true);
}
}
});
</c:if>
}
},
uri: portletURL.toString(),
where: 'replace'
});
pvContainerField.io.start();
}, ['aui-base', 'aui-io-plugin-deprecated', 'liferay-portlet-url']);
Come sicuramente avrete notato, ci sono questi accorgimenti:
- nella definizione della funzione, uso il
namespace
per renderla univoca: questo viene fatto così se finsice in pagina più volte almeno viene sendboxata; - subito dopo il caricamento via AJAX del frammento HTML (
after: success: {}
) uso un <c:if/>
per capire se sono tornato in pagina a causa di un errore oppure se sono in creazione; questo ovviamente mi serve per ripopolare il componente con i valori corretti; - la magia del caricamento avviene in automatico quando chiamo la funzione: è stata bindato sul componente
A.Plugin.IO
, che permette (anche se deprecato) il caricamento via AJAX dell'HTML che mi serve; - la magia del parsing mi viene offerta gratis sempre da A.Plugin.IO: grazie all'attributo
parseContent: true
viene attivato l'eval del JS nella pagina (figo!); - grazie alla direttiva
where: replace
, l'HTML che sarà servito lato server farà la sostituzione del mio markup.
Direi che non c'è bisogno di molte altre spiegazioni; il codice è abbastanza semplice ma, se avete dubbi, lasciateli nei commenti che rispondiamo! ;)
La JSP che viene caricata
Beh, questa è proprio "semplice":
- riceve via GET il parametro dell'id del record padre;
- dal db recupera la lista di figli (escludo la paginazione, ma qui c'è un dettaglio che spiegherò più sotto);
- uso il
SearchContainer
e abilito il RowChecker
.
Questo è il codice:
<%@ page import="com.liferay.portal.kernel.dao.search.RowChecker" %>
<%@ include file="/META-INF/resources/html/init.jsp"%>
<%
long customerId = ParamUtil.getLong(request, "customerId");
%>
<liferay-ui:search-container delta="200"
deltaconfigurable="false" emptyresultsmessage="no-entries-were-found"
rowChecker="<%=new RowChecker(renderResponse) %>"
total="<%= SalespointLocalServiceUtil.countByG_C(scopeGroupId, customerId) %>">
<liferay-ui:search-container-results
results="<%= SalespointLocalServiceUtil.findByG_C(scopeGroupId, customerId)%>"/>
<liferay-ui:search-container-row
classname="it.dvel.example.project.calendar.model.Salespoint"
keyproperty="salespointId" modelvar="salesPoint">
<liferay-ui:search-container-column-text>
<%= SalespointAddressFormatter.format(salesPoint)%>
</liferay-ui:search-container-column-text>
</liferay-ui:search-container-row>
<liferay-ui:search-iterator paginate="false"/>
</liferay-ui:search-container>
Qui l'unica cosa degna di nota è l'abilitazione del RowChecker
che ho evidenziato sul codice qui sopra!
Ovviamente tutto questo funziona quando, da qualche parte nel mio JS in pagina, io chiamo la funzione che abbiamo mappato in precedenza:
<portlet:namespace/>loadSalesPoints(result.id);
E questo è tutto quello che dovrebbe servirvi per far funzionare il giro come indicato qui sopra! :)
C'è ancora un punto, però, che secondo me vale la pena segnalare!
Io nel mio caso sono stato fortunato; la numerica dei record figli è sempre molto bassa (al massimo 20 righe); perché vi dico questo, però?
Beh, perché forse non lo sapete ma il SearchContainer
ha una limitazione: non può caricare più di 200 record in una finestra (mi pare che fossero tipo 1.000 nella 6.2 ma nella 7 sono stati abbassati a 200).. Questo, ovviamente, per performance e buona gestione della memoria.
Quindi ricordate: quando usate il SearchContainer
e volete presentare in una botta sola tutti i record, fare in modo che il vostro numero massimo sia minore o uguale al limite che vi ho esposto sopra!
Alla prossima! ;)