Using Liferay Fragments as modular webcontent templates
Since Liferay DXP 7.1 , there is the possibility to display webcontent templates with dynamic display pages and page fragments, on top of the 'old ' way of using Freemarker templates. Using display pages has a lot of benefits. Unfortunately, using display pages gives a lot less control over how content fields are shown compared to using Freemarker templates.
In this article I will explain how you can use page fragments as modular Freemarker templates; therefore using the best of both methods.
Let's say we use the display pages the default way. We create a webcontent structure named 'Article' with some fields (e..a header image, content, a video link and relevant links). We make a display page and set this as the default display page for all content with structure 'Article'. We also developed some page fragments that we can map to the webcontent fields. This works great for some basic content, but for use-cases that are a bit more complex a lot of problems arise:
- If an article has no relevant video, or relevant links, the fragment that is mapped to this content is still rendered.
- Each rendered liferay editable field has to be visible to be able to be mapped. Links have two editable parts, namely the text and the link itself. While the text is easy to map, the hidden link is not.
- Displaying repeatable content fields from the webcontent structure is not quite possible.
- Liferay-editable fields insert a lot of extra html when they are rendered which is not desirable and can even cause invalid html.
A lot of these problems can be solved by just reverting back to using the 'old' way of display content, namely using webcontent templates. In these templates we have full control on which and when webcontent fields are shown.
Why not just use webcontent templates then? Well, templates can become very clunky, especially if your structure is quite complex. Also, if you have multiple structures (e.a. news, article, events), you might keep duplicating the same code over and over again. Lastly, if we want to switch the order of certain blocks of content, it would be very error-prone to copy and paste everything. It would be much better if we could have small templates that we can re-use and rearrange to our heart's content in a handy interface. Luckily, we can!
First, create a page fragment in Site builder -> Page fragments. While editing the fragment, we can use alternative freemarker syntax to add some retrieval logic.
Liferay has a way of keeping track of the asset on a display page with a request variable: LIFERAY_SHARED_LAYOUT_ASSET_ENTRY. All we need is to retrieve this variable and find the entryId. That way we can retrieve the article content with the classPK.
[#assign currentEntry = (request.getAttribute("LIFERAY_SHARED_LAYOUT_ASSET_ENTRY"))! /] [#if currentEntry?has_content] [#assign journalArticleLocalService = serviceLocator.findService("com.liferay.journal.service.JournalArticleLocalService") /] [#assign currentArticle = journalArticleLocalService.fetchLatestArticle((currentEntry.classPK)!0, 0) /] <!--fetch latest APPROVED article --> [/#if]
Don't forget! Make sure you have removed the serviceLocator as a restricted variable in Control panel - Configuration - System settings - Template Engines - Freemarker engine, or check the 'Improve your solution' section for a way to retrieve the article without serviceLocator.
And now we can use saxReaderUtil to retrieve all the fields
[#if currentArticle?has_content] [#assign document = saxReaderUtil.read(currentArticle.getContentByLocale(locale.toString()) )/] [#assign rootElement = document.getRootElement() ] [#assign xPathSelector = (xPathSelector.selectSingleNode(rootElement).getStringValue()?trim)!""/] [/#if]
Here is some example code for retrieving more complex fields:
<#-- Nested fields --> [#assign xPathSelector = saxReaderUtil.createXPath("dynamic-element[@name='ParentItem']") /] [#assign parentNode = (xPathSelector.selectSingleNode(rootElement))!/] [#assign xPathSelector = saxReaderUtil.createXPath("dynamic-element[@name='NestedItem']") /] [#assign nestedItem = ((xPathSelector.selectSingleNode(parentNode).getStringValue())?trim)!''/] <#-- Repeatable fields --> [#assign xPathSelector = saxReaderUtil.createXPath("dynamic-element[@name='RelevantLink']") /] [#assign linkList = (xPathSelector.selectNodes(rootElement))!/] [#list linkList as link] [#if link?has_content && (link.getStringValue()?trim)?has_content] [#assign xPathSelector = saxReaderUtil.createXPath("dynamic-element[@name='LinkText']") /] [#assign linkText = ((xPathSelector.selectSingleNode(link).getStringValue())?trim)!''/] [#assign xPathSelector = saxReaderUtil.createXPath("dynamic-element[@name='LinkUrl']") /] [#assign linkUrl = ((xPathSelector.selectSingleNode(link).getStringValue())?trim)!''/] [/#if] [/#list] <#-- Images --> [#assign xPathSelector = saxReaderUtil.createXPath("dynamic-element[@name='HeaderImage']") /] [#assign imageString = ((xPathSelector.selectSingleNode(rootElement).getStringValue())?trim)!''/] [#if imageString?has_content] [#assign imageJson = imageString?eval/] [#assign fileEntryId = ((imageJson.fileEntryId)!'0')?trim /] [#assign fileEntry = dlAppLocalService.getFileEntry((ileEntryId?number) /] [#if fileEntry?has_content] [#assign imageUrl = themeDisplay.getPortalURL() + themeDisplay.getPathContext() + "/documents/" + themeDisplay.getScopeGroupId() + "/" + fileEntry.getFolderId() + "/" + fileEntry.getTitle()?url('ISO-8859-1') /] [/#if] [#assign altImage = (imageJson.alt?trim)!'' /] [/#if] <#-- Webcontent --> [#assign xPathSelector = saxReaderUtil.createXPath("dynamic-element[@name='WebcontentItem']") /] [#assign webcontentItem = (xPathSelector.selectSingleNode(rootElement).getStringValue())!""/] [#assign webcontentItemJson = (webcontentItemJson.getStringValue()?trim)?eval /] [#assign webcontentClassPK = (webcontentItemJson.classPK)!0 /] [#assign webcontentArticle = journalArticleLocalService.fetchLatestArticle(webcontentClassPK?number, 0, 0)! /] [#if webcontentArticle?has_content] <!-- We can output the title & url here if we want, or read fields from this content as well --> [#assign document = saxReaderUtil.read(webcontentArticle.getContentByLocale(locale.toString()))/] [/#if]
Putting it all together
So how we can create a fully fledged display page for an article? We can create some mini fragments to display each field separately. This way, it is quite easy to only display the fragment if the field has content (e.a. to not display the iframe if a youtube url is not included). This way we have full control on the html and whether content is displayed, while keeping the benefit of a nice UI where we can add fragments and re-use and re-order to our heart's desire.
For an example, please see the source code I have included . Here is what this would look like on on a display page in edit mode, and the rendered article itself.
And here is what it would look like in the rendered view:
An even better solution
While the described method works like a charm, there are some things we can do to improve it.
We are using a lot of boilerplate code to retrieve the entry and the fields. We can create a Freemarker macro for the logic of selecting nodes and/or create our own LocalService that we add to the Freemarker context with a template context contributor. This way, we can even make use of the liferay's utility classes that aren't available in the standard context, like DlUrlHelperUtil. Also, this way we can keep serviceLocator as a restricted variable, and only expose what we need.
See the source code I have included at the bottom of this blog for an example of a template context contributor, it is quite easy. In this improved solution I have only made one fragment, but you can rewrite the mini templates to use the new services easily.
Some last tips
- Don't forget to handle null values in Freemarker and use default value operators. Also, don't forget to trim values you retrieve from the xml with ?trim.
- If you have a lot of content structures it would be wise to use the same basic content structure as a parent structure for the structure fields that are similar between structures (e.a. header image, intro & content) This way, you won't have to worry about naming errors and the fragments can be used for multiple content structures.
- You can use this code in application display templates (ADT) as well. I have used it to display a link from the article in a related assets ADT.
- If you hide certain fragments based on whether a field is empty, add something like a placeholder for EDIT mode. Otherwise, it might be quite hard to edit fragments or even see which fragments are placed on the display page. You can see an example of this in my source code.