Single Page Applications with SennaJS and Liferay Faces

Single Page Applications with SennaJS and Liferay Faces

One of Liferay 7’s most exciting new features is SennaJS, a Single Page Application engine. SennaJS makes the portal more user-friendly in many ways. For example, when a link is clicked and navigation occurs, SennaJS requests the necessary portlet markup via XHR and renders it in the browser so that the whole portal page is not reloaded. As part of our latest release of Liferay Faces, we’ve added support for SennaJS so that JSF portlet developers can take advantage of this amazing feature.

How do I use SennaJS with Liferay Faces portlets?

Liferay Faces Bridge enables support for SennaJS in Liferay 7 by default. Utilizing SennaJS is as simple as using h:link, h:outputLink, or any other component which renders an <a> tag.1 If the link navigates to another page in your portal, SennaJS will automatically handle the navigation and cause an XHR GET which will load only the changed parts of the page. You can try this live in the Liferay Faces Showcase h:link example by clicking the To Param page > link. Clicking the link causes an XHR GET via SennaJS which is shown visually with a blue loading bar at the top of the page. You can compare this to a full-page GET by changing the end of the URL from param back to navigation and hitting enter. The full-page navigation is not only slower, but it causes more blinking and changes on the page.

Disabling SennaJS

Although SennaJS improves the user experience, it may be unnecessary for (or, rarely, incompatible with) certain use cases. If your portlet requires that SennaJS be disabled, then simply add <single-page-application>false</single-page-application> to the <portlet> section of your liferay-portlet.xml. You can also disable SennaJS on a portal-wide basis (by adding javascript.single.page.application.enabled=false to your portal-ext.properties file) or an element by element basis (Ex: <a data-senna-off="true">).

Under The Hood: How Liferay Faces Integrates with SennaJS

The Liferay Faces team worked closely with Bruno Basto, a Liferay front-end engineer, and Eduardo Lundgren, one of the creators of SennaJS, to ensure that Liferay 7’s usage of SennaJS would be compatible with Liferay Faces. Fortunately, SennaJS and Liferay 7 were already working together elegantly before we attempted to add Liferay Faces to the mix. Liferay 7 required only a few minor tweaks to allow Liferay Faces to take advantage of SennaJS. Bruno Basto implemented these changes. Thanks to him, SennaJS avoids loading duplicate resources in the <head> section by automatically tracking all CSS and JS resources and assuming that JS resources should not be unloaded.2,3

Since SennaJS is enabled by default in Liferay 7, the Liferay Faces team only needed to disable it for JSF components which were incompatible with its features. SennaJS assumes all links cause navigation, and it uses an XHR to obtain the markup that would otherwise be obtained via a full-page GET. Since JSF commandLink components are intended to submit a form rather than simply navigate to another page, they must not invoke SennaJS’s functionality. Likewise, SennaJS automatically causes forms to submit via XHR. However this feature is not compatible with JSF, since JSF expects to be in total control of form submissions.4 In order to avoid these incompatibilities, we used the data-senna-off attribute to disable SennaJS for all commandLinks and forms in Liferay 7.

With the minor changes listed above, Liferay Faces now allows JSF developers to enjoy the major benefits of SennaJS in Liferay 7!

  1. …with the exception of commandLink components. See the “Under the Hood” section for more details.
  2. Even though SennaJS tracks all the JavaScript files that are loaded, there is no way for it to generically unload all the components from those files, so it assumes it should not unload a JS element. This has the beneficial side effect of not reloading the same JS multiple times when a user navigates from one page to another and back again with SennaJS.
  3. All of these improvements benefit Liferay portlet developers who use JSP technology as well.
  4. Of course, JSF has its own functionality for submitting forms via ajax.
Blogs
This didn't seem to fit in the blog but Inácio Nery (https://github.com/inacionery) also added a nice feature that allows developers to specify how SennaJS will treat resources that they added to the <head> section. As alluded to above, Liferay 7 unloads/removes CSS resources from the <head> section on navigation, but it does not remove <script>s. This is great default functionality because CSS resources can be unloaded but scripts cannot. However Inácio Nery added the ability to change this functionality by allowing developers to customize the data-senna-track attribute of their <head> resources (https://github.com/liferay/liferay-portal/commit/870041f3434c559ea135dc7635799210a004c194). If the developer does not specify a data-senna-track attribute, <script>s and CSS will default to the functionality mentioned above. However, if the developer wants to change this for certain resources, they need only add the data-senna-track attribute to override the default. For example, to ensure that CSS is not unloaded during SennaJS navigation, add data-senna-track="permanent" to your CSS element:

<link href="example/example.css" data-senna-track="permanent" />

<script>s are not unloaded/removed during navigation (meaning they default to data-senna-track="permanent"), but you can set them to be:

<script src="example/example.js" data-senna-track="temporary" />

A word of caution, this will not actually unload the objects created by the JS resource. Potentially, this could create duplicate objects and cause problems because SennaJS will not avoid loading a <script> multiple times as it does with data-senna-track="permanent" (the default for <script>s).

See the SennaJS docs for more details: http://sennajs.com/docs#trackableresources

I am seeing an decrease in page loading speed without SennJS so I tried adding the following to my liferay-portlet.xml instead of turning SennaJS of completely:

<!DOCTYPE liferay-portlet-app PUBLIC "-//Liferay//DTD Portlet Application 7.0.0//EN" "http://www.liferay.com/dtd/liferay-portlet-app_7_0_0.dtd">

<liferay-portlet-app>     <portlet>         <portlet-name>packages</portlet-name>         <icon>/icon.png</icon>         <requires-namespaced-parameters>false</requires-namespaced-parameters>         <ajaxable>false</ajaxable>         <header-portlet-css>/resources/css/main.css</header-portlet-css>         <single-page-application>false</single-page-application>      </portlet>     <portlet>

 

But it caused the portlet not load and this exception in Liferay:

Caused by: com.liferay.portal.kernel.xml.DocumentException: Error on line 12 of document  : The content of element type "portlet" must match "(portlet-name,icon?,virtual-path?,struts-path?,parent-struts-path?,configuration-path?,configuration-action-class?,indexer-class*,open-search-class?,scheduler-entry*,portlet-url-class?,friendly-url-mapper-class?,friendly-url-mapping?,friendly-url-routes?,url-encoder-class?,portlet-data-handler-class?,staged-model-data-handler-class*,template-handler?,portlet-layout-listener-class?,poller-processor-class?,pop-message-listener-class?,social-activity-interpreter-class*,social-request-interpreter-class?,user-notification-definitions?,user-notification-handler-class*,webdav-storage-token?,webdav-storage-class?,xml-rpc-method-class?,application-type*,control-panel-entry-category?,control-panel-entry-weight?,control-panel-entry-class?,asset-renderer-factory*,atom-collection-adapter*,custom-attributes-display*,permission-propagator?,trash-handler*,workflow-handler*,preferences-company-wide?,preferences-unique-per-layout?,preferences-owned-by-group?,use-default-template?,show-portlet-access-denied?,show-portlet-inactive?,action-url-redirect?,restore-current-view?,maximize-edit?,maximize-help?,pop-up-print?,layout-cacheable?,instanceable?,remoteable?,scopeable?,single-page-application?,user-principal-strategy?,private-request-attributes?,private-session-attributes?,autopropagated-parameters?,requires-namespaced-parameters?,action-timeout?,render-timeout?,render-weight?,ajaxable?,header-portal-css*,header-portlet-css*,header-portal-javascript*,header-portlet-javascript*,footer-portal-css*,footer-portlet-css*,footer-portal-javascript*,footer-portlet-javascript*,css-class-wrapper?,facebook-integration?,add-default-resource?,system?,active?,include?)". Nested exception: The content of element type "portlet" must match "(portlet-name,icon?,virtual-path?,struts-path?,parent-struts-path?,configuration-path?,configuration-action-class?,indexer-class*,open-search-class?,scheduler-entry*,portlet-url-class?,friendly-url-mapper-class?,friendly-url-mapping?,friendly-url-routes?,url-encoder-class?,portlet-data-handler-class?,staged-model-data-handler-class*,template-handler?,portlet-layout-listener-class?,poller-processor-class?,pop-message-listener-class?,social-activity-interpreter-class*,social-request-interpreter-class?,user-notification-definitions?,user-notification-handler-class*,webdav-storage-token?,webdav-storage-class?,xml-rpc-method-class?,application-type*,control-panel-entry-category?,control-panel-entry-weight?,control-panel-entry-class?,asset-renderer-factory*,atom-collection-adapter*,custom-attributes-display*,permission-propagator?,trash-handler*,workflow-handler*,preferences-company-wide?,preferences-unique-per-layout?,preferences-owned-by-group?,use-default-template?,show-portlet-access-denied?,show-portlet-inactive?,action-url-redirect?,restore-current-view?,maximize-edit?,maximize-help?,pop-up-print?,layout-cacheable?,instanceable?,remoteable?,scopeable?,single-page-application?,user-principal-strategy?,private-request-attributes?,private-session-attributes?,autopropagated-parameters?,requires-namespaced-parameters?,action-timeout?,render-timeout?,render-weight?,ajaxable?,header-portal-css*,header-portlet-css*,header-portal-javascript*,header-portlet-javascript*,footer-portal-css*,footer-portlet-css*,footer-portal-javascript*,footer-portlet-javascript*,css-class-wrapper?,facebook-integration?,add-default-resource?,system?,active?,include?)".  

I really hope you guys are paying attention to this SennaJS issue.  I notice that it was the cause of the column toggler issue I was having as well.