A good user experience is the measure of a well-designed site. A user’s time is highly valuable. The last thing you want is for someone to grow frustrated with your site because of constant page reloads. A Single Page Application (SPA) avoids this issue. Single Page Applications drastically cut down on load times by loading only a single HTML page that’s dynamically updated as the user interacts and navigates through the site. This provides a more seamless app experience by eliminating page reloads. SPA is enabled by default in your apps and sites and requires no changes to your workflow or code!
This tutorial covers these key topics:
- The benefits of SPAs
- What is SennaJS?
- How to enable SPA in Liferay DXP
- How to configure SPA settings
- How to listen to SPA lifecycle events
The Benefits of SPAs
Let’s say you’re surfing the web and you find a really rad site that happens to be SPA enabled. All right! Page load times are blazin’ fast. You’re deep into the site, scrolling along, when you find this great post that just speaks to you. You copy the URL from the address bar and email it to all of your friends with the subject: ‘Your Life Will Change Forever.’ They must experience this awe-inspiring work!
You get a response back almost immediately. “This is a rad site, but what post are you talking about?” it reads.
“What!? Do my eyes deceive me?” you exclaim. You were in so much of a hurry to share this life-changing content that you neglected to notice that the URL never updated when you clicked the post. You click the back button, hoping to get back to the post, but it takes you to the site you were on before you ever visited this one. The page history didn’t update as you navigated through the app; Only the main app URL was saved.
What a bummer! “Why? Why have you failed me, site?” you cry.
If only there was a way to have a Single Page Application, but also be able to link to the content you want. Well, don’t despair my friend. You can have your cake and eat it too, thanks to SennaJS.
What is SennaJS?
SennaJS is Liferay DXP’s SPA engine. SennaJS handles the client-side data, and AJAX loads the page’s content dynamically. While there are other JavaScript frameworks out there that may provide some of the same features, Senna’s only focus is SPA, ensuring that your site provides the best user experience possible.
SennaJS provides the following key enhancements to SPA:
SEO & Bookmarkability: Sharing or bookmarking a link displays the same content you are viewing. Search engines are able to index this content.
Hybrid rendering: Ajax + server-side rendering lets you disable pushState
at
any time, allowing progressive enhancement. You can use your preferred method to
render the server side (e.g. HTML fragments or template views).
State retention: Scrolling, reloading, or navigating through the history of the page takes you back to where you were.
UI feedback: The UI indicates to the user when some content is requested.
Pending navigations: UI rendering is blocked until data is loaded, and the content is displayed all at once.
Timeout detection: If the request takes too long to load or the user tries to navigate to a different link while another request is pending, the request times out.
History navigation: The browser history is manipulated via the History API, so you can use the back and forward history buttons to navigate through the history of the page.
Cacheable screens: Once a surface is loaded, the content is cached in memory and is retrieved later without any additional request, speeding up your application.
Page resources management: Scripts and stylesheets are evaluated from
dynamically loaded resources. Additional content can be appended to the DOM
using XMLHttpRequest
. For security reasons, some browsers won’t evaluate
<script>
tags from content fragments. Therefore, SennaJS extracts scripts from
the content and parses them to ensure that they meet the browser loading
requirements.
You can see examples and read more about SennaJS at its website.
Now that you have a better understanding of how SennaJS benefits SPA, you can learn how to enable and configure options for SPA within Liferay DXP next.
Enabling SPA
Enabling SPA is easy. Since this module is included by default, you shouldn’t
have to do anything. If you’ve removed it, deploy
com.liferay.frontend.js.spa.web-[version]
module and enable it, and you’re all
set to use SPA.
SPA is enabled by default in your apps and sites, and requires no changes to your workflow or existing code!
Next you can learn how to customize SPA settings to meet your own needs.
Customizing SPA Settings
Depending on what behaviors you need to customize, you can configure SPA options in one of two places. SPA caching and SPA timeout settings are configured in System Settings. If you wish to disable SPA for a certain link, page, or portlet in your site, you can do so within the corresponding element itself. All SPA configuration options are covered here.
Configuring SPA System Settings
To configure system settings for SPA, follow these steps:
-
In the Control Panel, navigate to Configuration → System Settings.
-
Select Infrastructure under the PLATFORM heading.
-
Click Frontend SPA Infrastructure.
The following configuration options are available:
Cache Expiration Time: The time, in minutes, in which the SPA cache is cleared. A negative value means the cache should be disabled.
Navigation Exception Selectors: Defines a CSS selector that SPA should ignore.
Request Timeout Time: The time, in milliseconds, in which a SPA request times out. A zero value means the request should never timeout.
User Notification Timeout: The time, in milliseconds, in which a notification is shown to the user stating that the request is taking longer than expected. A zero value means no notification should be shown.
Now that you know how to configure system settings for SPA, you can learn how to disable SPA for elements in your site next.
Disabling SPA
Certain elements of your page may require a regular navigation to work properly. For example, you may have downloadable content that you want to share with the user. In these cases, SPA must be disabled for those specific elements.
To disable SPA across an entire Liferay DXP instance, you can add the following
line to your portal-ext.properties
:
javascript.single.page.application.enabled = false
If there is a portlet or element that you don’t want to be part of the SPA, you have some options:
- Blacklist the portlet to disable SPA for the entire portlet
- Use the
data-senna-off
annotation to disable SPA for a specific form or link
To blacklist a portlet from SPA, follow these steps:
-
Open your portlet class.
-
Set the
com.liferay.portlet.single-page-application
property to false:com.liferay.portlet.single-page-application=false
If you prefer, you can set this property to false in your
portlet.xml
instead by adding the following property to the<portlet>
section:<single-page-application>false</single-page-application>
-
Alternatively, you can override the
isSinglePageApplication
method of the portlet to returnfalse
.
To disable SPA for a form or link follow these steps:
-
Add the
data-senna-off
attribute to the element. -
Set the value to
true
.
For example <a data-senna-off="true" href="/pages/page2.html">Page 2</a>
That’s all you need to do to disable SPA in your app.
Now that you know how to disable SPA, you can learn how to specify how resources are loaded during navigation.
Specifying How Resources Are Loaded During Navigation
By default, Liferay DXP unloads CSS resources from the <head>
element on
navigation. JavaScript resources in the <head>
, however, are not removed on
navigation. This functionality can be customized by setting the resource’s
data-senna-track
attribute. Follow these steps to customize your resources:
-
Select the resource you want to modify the default behavior for.
-
Add the
data-senna-track
attribute to the resource. -
Set the
data-senna-track
attribute topermanent
to prevent a resource from unloading on navigation.Alternatively, set the
data-senna-track
attribute totemporary
to unload the resource on navigation.
The example below ensures that the JS resource isn’t unloaded during navigation:
<script src="myscript.js" data-senna-track="permanent" />
Next you can learn about the available SPA lifecycle events next.
Listening to SPA Lifecycle Events
During development, you may need to know when navigation has started or stopped in your SPA. SennaJS makes this easy by exposing lifecycle events that represent state changes in the application. The following events are available:
beforeNavigate: Fires before navigation starts. This event passes a JSON object with the path to the content you are navigating to and whether to update the history. Below is an example event payload:
{ path: '/pages/page1.html', replaceHistory: false }
startNavigate: Fires when navigation begins. Below is an example event payload:
{ form: '<form name="form"></form>', path: '/pages/page1.html',
replaceHistory: false }
endNavigate: Fired after the content has been retrieved and inserted onto the page. This event passes the following JSON object:
{ form: '<form name="form"></form>', path: '/pages/page1.html' }
These events can be leveraged easily by listening for them on the Liferay global object. For example, the JavaScript below alerts the user to “Get ready to navigate to” the URL that has been clicked, just before SPA navigation begins:
Liferay.on('beforeNavigate', function(event) {
alert("Get ready to navigate to " + event.path);
});
The alert takes advantage of the payload for the beforeNavigate
event,
retrieving the URL from the path
attribute of the JSON payload object.
Figure 1: You can leverage SPA lifecycle events in your apps.
Due to the nature of SPA navigation, global listeners that you create can become problematic over time if not handled properly. You’ll learn how to handle these listeners next.
Detaching Global Listeners
SPA provides several improvements that highly benefit your site and users, but
there is potentially some additional maintenance as a consequence. In a
traditional navigation scenario, every page refresh resets everything, so you
don’t have to worry about what’s left behind. In a SPA scenario, however, global
listeners such as Liferay.on
, Liferay.after
, or body delegates can become
problematic. Every time you execute these global listeners, you add yet another
listener to the globally persisted Liferay
object. The result is multiple
invocations of those listeners. This can obviously cause problems if not
handled.
To prevent this, you need to listen to the navigation event in order to detach
your listeners. For example, you would use the following code to detach the
event listeners of a global category
event:
var onCategory = function(event) {...};
var clearPortletHandlers = function(event) {
if (event.portletId === '<%= portletDisplay.getRootPortletId() %>') {
Liferay.detach('onCategoryHandler', onCategory);
Liferay.detach('destroyPortlet', clearPortletHandlers);
}
};
Liferay.on('category', onCategory);
Liferay.on('destroyPortlet', clearPortletHandlers);
Now you know how to configure and use SPA in Liferay DXP!