Cookies help us deliver our services.

By using our services, you agree to our use of cookies. Learn more

I understand

D'vel Blog

By the end of the 2010 I work as Liferay Expert for D'vel snc, for which I often write technical articles on its blog.

This blog is really interesting and full of articles; if you're curious, this is the link: blog.d-vel.com.

Here is a list of the latest articles written.

blogdvel
  1.  

    Ciao a tutti!

    Tutti sapete che la tabella Layout contiene le pagine che vengono generate dal portale; forse non tutti però conoscete i typeSettings, un campo della tabella Layout all'interno del quale, tipicamente, il portale scrive l'associazione tra gli spazi dei singoli layouttpl e le relative portlet.

    Ma come possiamo usarli "a nostro piacimento"?

    Questa domanda, come sempre, mi è stata fatta da un gruppo di ragazzi che sta facendo un ottimo lavoro mentre fanno il porting dalla 6.2 EE alle 7.2 EE di una applicazione che non hanno scritta loro ma che hanno ereditato.

    (N.d.J: Della serie: doppio carpiato rovesciato, ma bendati e legati dopo aver girato in tondo per 5 minuti e poi fatti saltare da 35 metri d'altezza... Per darvi un'idea! :))

    E la domanda, più che lecita, mi è stata rivolta perché chi ha codificato prima di loro l'applicativo, ha pensato bene di utilizzare i typeSettings anche per salvare caratteristiche delle pagine.

    Posto che noi in D'vel è una vita che facciamo queste robe (ma ce la siamo sempre spicciata facile usando i Custom Attribute :)), m'intrigava la soluzione che avevano realizzato e quindi ho lavorato con loro per riuscire a fare il porting del codice sulla 7.2.

    Il caso funzionale

    Il caso funzionale che avevano mappato era semplice: in alcune pagine è presente una portlet (scusate: widget, siamo sulla 7.. :D); questo widget però deve ereditare alcuni parametri per essere configurato, così chi ha codificato l'applicativo ha pensato bene di salvare nei typeSettings questi parametri.

    (N.d.J: faccio notare che la portlet poteva essere semplicemente configurata sulle singole pagine, senza stare tanto ad impazzire; ma non chiedetemi perché è stata scelta questa strada: se la vita fosse semplice a noi non ci cercherebbe nessuno, quindi.. ;D).

    La vecchia implementazione

    Per riuscire a fare questa implementazione, i vecchi developer avevano proceduto in questo modo:

    • avevano fatto un bell'hook sulla portlet che gestiva il back end della gestione delle pagine;
    • avevano fatto una bella JSP che si agganciava al form-navigator della gestione delle pagine;
    • accedendo alla nuova voce all'interno del form-navigator avevano messo la loro bella JSP;
    • al submit i dati venivano salvati trasparentemente nella Layout, così che il gioco fosse fatto!

    Analizzando nel dettaglio la loro implementazione, in effetti posso anche riconoscere che è sicuramente più elegante e figa della nostra:

    • il cliente non accede ai "Campi personalizzati" ma al back end standard di prodotto;
    • all'interno delle voci di configurazione della pagina c'era la loro voce;
    • accedendo alla loro voce c'era una form "impaginata a modo" (e non autogenerata come quella dei Custom Fields) che non era in effetti niente male..

    Cavolo, allora la sfida si faceva interessante! :)

    La nuova implementazione

    La prima cosa che c'era da fare, quindi, era sostituire l'hook che, sulla 6.2, si agganciava con una JSP deployata sul portale, al form-navigator presente all'interno della gestione delle pagine.

    Nella 7, il form-navigator ovviamente si è evoluto ed è quindi diventato necessario sviluppare due Components per poterlo utilizzare / per potercisi collegare in maniera trasparente.

    Il primo serve per creare la categoria all'interno del menù di navigazione del portale; il secondo per creare le singole sezioni che ci sono all'interno di questo menu.

    Quindi abbiamo proceduto in questo modo; prima abbiamo creato la categoria:

      package it.dvel.playground.web.layout; import com.liferay.portal.kernel.language.LanguageUtil; import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorCategory; import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorConstants; import java.util.Locale; import org.osgi.service.component.annotations.Component; @Component( immediate = true, property = "form.navigator.category.order:Integer=10", service = FormNavigatorCategory.class) public class CustomLayoutFormNavigatorCategory implements FormNavigatorCategory { @Override public String getFormNavigatorId() { return FormNavigatorConstants.FORM_NAVIGATOR_ID_LAYOUT; } @Override public String getKey() { // Io ho fatto una PoC; voi fate i bravi e usate una COSTANTE!! :) return "custom-category"; } @Override public String getLabel(Locale locale) { return LanguageUtil.get(locale, "custom-category"); } }

    Fatto questo, abbiamo creato il pezzo di pagina che ci interessava.. O meglio: la entry che si sarebbe agganciata alla nostra category custom:

      package it.dvel.playground.web.layout; import com.liferay.portal.kernel.language.LanguageUtil; import com.liferay.portal.kernel.model.Layout; import com.liferay.portal.kernel.servlet.taglib.ui.BaseJSPFormNavigatorEntry; import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorConstants; import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorEntry; import java.util.Locale; import javax.servlet.ServletContext; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @Component( property = "form.navigator.entry.order:Integer=100", service = FormNavigatorEntry.class) public class CustomLayoutFormNavigatorEntry extends BaseJSPFormNavigatorEntry implements FormNavigatorEntry { @Override protected String getJspPath() { return "/html/admin_layout/my_custom_fields_to_manage.jsp"; } @Override public String getCategoryKey() { // Le costanti ragazzi: voi usate le costanti! ;) return "custom-category"; } @Override public String getFormNavigatorId() { return FormNavigatorConstants.FORM_NAVIGATOR_ID_LAYOUT; } @Override public String getKey() { // Non mi stancherò mai di ripetermi: usate le costanti! ;) return "custom-entry"; } @Override public String getLabel(Locale locale) { return LanguageUtil.get(locale, getKey()); } @Override @Reference( target = "(osgi.web.symbolicname=my-poc-module-web)", unbind = "-") public void setServletContext(ServletContext servletContext) { // Questo setter è strategico: permette al container di recuperare // la JSP che abbiamo indicato sopra direttamente dal nostro bundle // e non a raglio da chissà quale pacchetto! // Ricordatevi che questa roba sarà eseguita dal bundle standard della // gestione delle pagine! super.setServletContext(servletContext); } }

    E anche questa è fatta!

    Ora non rimaneva che sistemare la nostra JSP, all'interno del nostro bundle, e far gestire tutto al componente standard di portale.

    Ed ecco qui la JSP:

     <%@ include file="/META-INF/resources/html/init.jsp"%> <% // Recupero il plid del Layout visualizzato dalla request Long selPlid = ParamUtil.getLong(renderRequest,"selPlid"); // Recupero il Layout usando il plid Layout selLayout = LayoutLocalServiceUtil.getLayout(selPlid); UnicodeProperties layoutTypeSettings = null; if (selLayout != null) { // Se il Layout non è nullo, recupero le typeSettings layoutTypeSettings = selLayout.getTypeSettingsProperties(); } %> <liferay-ui:error-marker key="error-section" value="my-custom-fields-to-manage-error-message" /> <aui:model-context bean="<%= selLayout %>" model="<%= Layout.class %>" /> <h3><liferay-ui:message key="my-custom-fields-to-manage-title" /></h3><aui:fieldset cssClass="lfr-portrait-editor"> <% String riskType = GetterUtil.getString(layoutTypeSettings.getProperty("risk-type")); %> <aui:select label="risk-type" name="TypeSettingsProperties--risk-type--" showemptyoption="<%= true %>"> <aui:option label="type-car" selected="<%= "CAR".equals(riskType) %>" value="<%=Constants.CAR%>"/> <aui:option label="type-motorbike" selected="<%= "MOTORBIKE".equals(riskType)%>" value="<%=Constants.MOTORBIKE%>"/> <aui:option label="type-easy" selected="<%= "EASY".equals(riskType) %>" value="<%= Constants.EASY %>"/> <aui:option label="type-quote" selected="<%= "QUOTE".equals(riskType) %>" value="<%= Constants.VERTIQUOTE %>"/> <aui:option label="type-home" selected="<%= "HOME".equals(riskType) %>" value="<%= Constants.HOME %>"/> <aui:option label="type-other-vehicle" selected="<%= "OTHER_VEHICLE".equals(riskType) %>" value="<%=Constants.OTHER_VEHICLE%>"/> <aui:option label="type-ivass" selected="<%= "IVASS".equals(riskType) %>" value="<%= Constants.IVASS %>"/> </aui:select> </aui:fieldset>

    Ed eccolo qui, l'utimo tassello del puzzle! :)

    Una nota importante: come potete vedere, nel nome della select è stato inserito:

     TypeSettingsProperties--risk-type--

    che i più attenti di voi avranno già riconosciuto come un meccanismo "automatico" che viene usato dal portale per leggere e salvare arbitrari valori che arrivano dal web (di fatto è la stessa convenzione che si utilizza sul salvataggio delle configurazioni!).

    Detto questo, ovviamente, deploy, navigazione, test e... Funziona! ;)

    A questo punto, anche voi (come noi :D), adesso potete utilizzare le typeSettings di pagina come un modo più elegante e furbo per far configurare parametri al Cliente.

    .. Sempre che non abbiate una fretta del diavolo e i Custom Attribute non vi sembrino molto più smart e semplici da utilizzare!!

    Direi che anche per oggi è tutto: se avete dubbi o domande, come sempre, sono a vostra disposizione; scriveteci nei commenti e fateci sapere che ne pensate!

    Buona giornata a tutti! ;)

  2.  

    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:

    1. quando si carica la pagina la prima volta e, al trigger sul front end, devo caricare il frammento HTML;
    2. 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":

    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!

    Paginazione si, paginazione no: attenzione però al numero di risultati!

    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! ;)

  3. Buongiorno a tutti e ben ritrovati!

    Questo è proprio un periodo felice: riesco a scrivere un sacco di non-post (come quello di ieri ma anche come quello che sto scrivendo ora.. :D) che affrontano tematiche che.. Interessano praticamente solo me! ;D

    In realtà quello di oggi è un topic che mi da un po' da fare da sempre; ho sempre avuto il pallino di abbandonare Eclipse (o il Liferay Developer Studio) in funzione di una IDE che fosse un po' più robusta, stabile, funzionante, chi-più-ne-ha-più-ne-metta... :D

    Scherzi a parte, senza nulla togliere al Liferay Developer Studio 2.2.2 (che ad oggi l'Ing. Napolitano lista ancora nella sua personale top list delle versioni del LDS -di cui è un attento e preciso catalogatore seriale.. :D-), avevo da un po' cominciato a guardare al tema ma senza successo.

    L'antefatto...

    Poi è successo un fatto.. Nel weekend ho fatto upgrade del sistema operativo senza fare backup (prima volta in 20 anni.. :/).

    Beh, Big Sur (ho un mac..) ha un piccolo glitch per cui non viene più impostata correttamente la JAVA_HOME.

    Poco male, direte voi, mica tu Jader sei uno che sviluppa.. :D

    ... Vero!

    Però, però, però, c'è un però.. :D

    ... Questa settimana stavo proprio tenendo un corso guardacaso sulla 6.2 di LR.

    E il fatto che non mi setti più la JAVA_HOME (anche se a manazza sembra averla correttamente settata..), il Liferay Developer Studio 2.2.2. NON mi parte più! 

    [N.d.J.: Appunto che ho correttamente configurato l'init di Eclipse impostando a manazza l'argomento per la corretta JVM, ma niente. Parte ma si frizza sullo splash screen e sulla selezione del workspace..]

    L'esigenza..

    Quindi quello che fino a sabato mattina era solo un vezzo, ora è diventato una triste necessità! ;)

    Per terminare il corso non ho avuto grossi problemi: ho riesumato il mac che avevo dismesso durante il lockdown in favore del nuovo e quindi il corso è terminato correttamente.

    Però ci sono alcuni progetti sui quali abbiamo della maintenance attiva per i quali, purtroppo o per fortuna :), dobbiamo fare manutenzione.

    [N.d.J.: Dobbiamo.. Devo: perché né l'Inge, né gli altri colleghi ne vogliono sapere.. :D]

    La soluzione

    Quindi questa mattina, di buon'ora (come sono solito fare da quando sono in smart working :)), mi sono messo d'impegno per far funzionare il LRSDK62 su IntelliJ.. 

    Spoiler: la soluzione che segue è ovvia, lo so, talmente ovvia che i più, tra voi, sicuramente la bolleranno come "minchiata" :). La scrivo comunque, però, perché un giorno mi servirà e allora non dovrò rifare tutto il giro che ho fatto questa mattina! :)

    Ragionandoci un po' su ho avuto un'intuizione: il DevStudio non fa altro che gestire dinamicamente il classpath e poi via ANT eseguire dei task.

    Se sistemo a manazza il classpath e faccio in modo che ANT buildi, vinco facile.. :)

    Quindi:

    • ho aperto in IntelliJ LE SINGOLE PORTLET contenute all'interno del SDK;
      • Quindi i progetti contenuti nelle varie folder del SDK (portlets, layouttpl, hooks, etc..);
    • ho configurato il classpath (more on this later...) delle singole portlet impostando:
      • il classpath globale con tutte le dipendenze per far compilare i miei portlet;
      • il classpath dei singoli moduli puntando alla loro WEB-INF/lib interna (quando necessario);
    • ho configurato ANT (la versione corretta) e l'ho fatto funzionare (solo da command line, sorry...)

    Dettagli tecnici

    Questo è lo screenshot di come ho configurato il classpath globale:

    Come sicuramente avrete notato bisogna aggiungere (per praticità / velocità):

    • tutti i JAR contenuti nella lib del Tomcat bundle Liferay;
    • tutti i JAR contenuti nella lib/ext del Tomcat bundle Liferay;
    • questi singoli JAR, presi dalla <tomcat home>/webapps/ROOT/WEB-INF/lib:
      • commons-logging.jar
      • jstl-api.jar
      • jstl-impl.jar
      • log4j.jar
      • util-bridges.jar
      • util-java.jar
      • util-taglib.jar

    Fatto questo gli artefatti vi compileranno già; rimane da sistemare ANT.

    Per farlo funzionare, mi sono scaricato la versione apache-ant-1.9.15 e, a mano, lancio i singoli task da dentro alle folder dei singoli plugin.Questo approccio funziona: compila e fa deploy correttamente!

    Conclusioni

    Sicuramente starete dicendo peste e corna di questo post ;), mi sembra quasi di sentirvi! :)

    Però dovete ammettere che di articoli che spiegano questa cosa non ce ne sono proprio su internet... Forse perché, come dicevo all'inizio, forse è un problema soltanto mio.. :D

    Aggiungo che tutto quanto sopra funziona perché io ho già un SDK che è correttamente configurato: intendo che al suo interno è già presente, parametrizzato correttamente dal Liferay Developer Studio, il file build.jed.properties (che è il file che permette ad ANT di funzionare correttamente).

    Se così non fosse, nel senso che state iniziando un progetto nuovo sulla 6.2, beh, il mio consiglio è FERMARVI e cominciare il vostro nuovo  progetto su LR7.x perché, e sono serio, la vostra vita sarà molto più semplice e divertente! ;)

    In ultima analisi, è ovvio che questo paradigma (LR62 + IntelliJ) è un palliativo: quello che si perde nel non usare il Liferay Developer Studio è tanta roba (snippet, pannellini di configurazione, wizard.. Tanta roba davvero!).

    È vero anche però, per contro, che il tempo che guadagnerete lavorando con questa configurazione sarà mostruosamente più alto del tempo che Eclipse -io parlo per me, magari la vostra esperienza è diversa..- vi fa perdere di solito.. :)

    Detto questo, sperando di avervi aiutato a migliorare un po' la vostra performance lavorativa (o aver risolto il problema che BUG Sur ha introdotto.. :)), vi lascio e vi do appuntamento alla prossima! ;)

    Divertitevi e buon IntelliJ a tutti (anche con LR62)!! ;)

  4. Buongiorno a tutti!

    Quello di oggi non è un "vero" post (come quelli che siete abituati a leggere su questo blog ;)), ma è più un post di servizio..

    Questa mattina, giocando con SourceTree, ho fatto una hotfix su un ramo di produzione per un cliente e, all'atto di chiudere la hotfix mi sono imbattuto nel messaggio che vedete come titolo!

    Scrivo qui la soluzione, così la prossima volta che mi succederà non dovrò cercare su tutto il web il post che ho messo anche nelle risorse! :) 

    La soluzione è semplice: bisogna dire a SourceTree di utilizzare il git di sistema e non il suo interno.

    Come fare? Easy:

    If anybody using SourceTree, similar error was fixed by going to Preferences -> tab Git -> section Git version -> button Use System Git, and select from the dialog 'git'. It appears that after an update it forgot where it's command line tools are

    E, come aggiunge un altro utente nello stesso thread:

    Just remember to restart SourceTree then it works

    Per oggi è tutto; prometto che non appena avrò un minuto vi scriverò un post succoso degno dell'Ing. Napolitano, promesso! ;)

    Alla prossima!

    Source:

  5. Oggi vi parlerò di IFrame e di Javascript per un problema che ho avuto utilizzando Liferay ma che in realtà può essere applicato anche in altri contesti.

    La necessità che avevo era quella di visualizzare una pagina di Liferay 7.2 all'interno di un IFrame di una pagina di Liferay 6.2; inoltre avevo bisogno che l'IFrame si ridimensionasse in altezza automaticamente in modo da evitare il classico problema della doppia barra di scorrimento laterale.

    I più attenti di voi potrebbero dire: "Liferay ha già una portlet IFrame che fa esattamente quello che ti serve". Vero, peccato però che il ridimensionamento dell'IFrame non funzioni perchè vengono messi in atto dei controlli di sicurezza per cui Javascript non è in grado di recuperare dall'IFrame le informazioni necessarie per determinarne l'altezza; e nemmeno l'IFrame è in grado di invocare funzioni Javascript sul container esterno, per lo stesso motivo.

    Inoltre Liferay non consente che le sue pagine siano incluse in un IFrame (la famigerata Same Origin policy), quindi di problemi da risolvere ce ne sono diversi; ma procediamo per passi.

    Il primo problema che risolviamo è fare in modo che le pagine di Liferay 7.2 possano essere incluse in un IFrame; per questo dobbiamo creare il file system-ext.properties e posizionarlo nella cartella <tomcat>/webapps/ROOT/WEB-INF/classes. Il contenuto del file sarà l'elenco numerale di tutte le URL che devono essere incluse nell'IFrame, come ad esempio:

     http.header.secure.x.frame.options.0=/pagina-1 http.header.secure.x.frame.options.1=/web/guest/pagina-1 http.header.secure.x.frame.options.2=/en/page-1 http.header.secure.x.frame.options.3=/en/web/guest/page-2 http.header.secure.x.frame.options.4=... http.header.secure.x.frame.options.5=...

    A questo punto possiamo salvare il file, riavviare il server di Liferay 7.2 e finalmente riusciremo ad includere le specifiche pagine in un IFrame su Liferay 6.2.

    Bene, ma ora come facciamo a ridimensionare automaticamente l'IFrame in altezza? Per ottenere il risultato voluto dobbiamo riuscire a trasferire informazioni dall'IFrame al container. Per fare ciò dobbiamo scomodare Javascript ed in particolare la funzione postMessage che consente di inviare "messaggi" da una finestra ad un'altra del browser (anche non IFrame). Non entro nei dettagli della funzione ma se siete curiosi leggete qui.

    A questo punto, nella JSP della portlet da includere nell'IFrame aggiungiamo questo frammento di codice:

     <aui:script use="aui-base"> var content = A.one('#content'); var height = content.height(); parent.postMessage('{"height": ' + height + '}', '*'); </aui:script>

    Il codice non fa altro che recuperare l'elemento del DOM che rappresenta il contenuto della pagina e calcolarne l'altezza in pixel; dopodichè invia alla finestra parent (ossia il container dell'IFrame) un messaggio. Nel caso specifico il messaggio è una stringa che rappresenta un oggetto JSON con tutte le informazioni necessarie, nel nostro caso l'altezza in pixel; fate attenzione perchè la funzione postMessage consente di inviare anche oggetti ma, se non ricordo male, il buon vecchio IE supporta invece solamente stringhe.

    Ora possiamo spostarci su Liferay 6.2 dove dobbiamo andare ad intercettare il messaggio inviato dall'IFrame; il dove è ovviamente la JSP che visualizza l'IFrame e quindi avete 2 possibilità (nessuna delle quali verrà analizzata in questo post perché immagino siate già in grado di farlo da soli):

    1. fare un hook sulla portlet IFrame di Liferay 6.2
    2. farvi una vostra portlet custom per includere l'IFrame

    Quello che interessa a me adesso è solamente farvi vedere il codice per intercettare il messaggio:

     <aui:script use="aui-base"> var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent"; var messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message"; var listener = window[eventMethod]; // Ascolta i messaggi che arrivano dall'altra finestra listener(messageEvent, function(e) { var key = e.message ? "message" : "data"; // Contenuto del messaggio var data = e[key]; try { // Sapendo che il messaggio è in formato JSON, lo convertiamo var json = A.JSON.parse(data);  // Recupero del nodo del DOM che rappresenta l'IFrame var iframe = A.one('#<portlet:namespace />iframe'); // Se il nodo esiste, ne imposto l'altezza if (iframe) iframe.setStyle('height', json.height); } catch(e) { console.error(e); } }, false); </aui:script>

    Forte vero? Chiaramente vi ho fatto vedere solamente come modificare l'altezza ma nel messaggio potete inviare qualunque informazione e farne poi quello che volete.