Liferay Custom Tag Libraries

Implementing a custom Tag Library with reusable components

Introduction

During a Liferay project implementation we often have similar pieces of UI and functionality in different widgets - and that's not surprising, as UI/UX should be consistent across the entire portal. We, as developers, try to avoid code duplication in different ways:

  • Create a "common" OSGi service that can be used in multiple widgets;
  • Create a "shared" Javascript library;
  • Use common styling;
  • Create a "partical" JSP that be included on different views;
  • Use Liferay-provided Tag Libraries.

Although we can make BE code (as well, as JS/CSS) easily reusable, it's not that straightforward with JSP/markup: we can include a "partial" JSP with common code in different places - but this works only within a single module (and Liferay widgets typically live in different ones), we can use Liferay tag libraries, but they may not satisfy all the requirements (or the code leveraging them will become duplicated itself).

A possible solution here is a custom tag library: you can create project-specific components (tags), and use them in different widgets, encapsulating UI and functionality inside the tags implementation. 

Use Case

On one of the latest projects we had to implement a lot of widgets with form submission functionality that contains various data, including dates. And we had to decide how we render the date inputs - there were multiple options, but we didn't have a final decision when we started working on widgets implementation:

  • We could use a liferay-ui:input-date component, but it's outdated and has a legacy UI;
  • We wanted to use a Clay Date Picker, which is more visually appealing - but it can't be used outside React world, as it does not have an appropriate component in Clay Tag Library;
  • We could also use simple input fields with mask/validation;
  • Or we could integrate a 3rd-party JS DatePicker (but there are a lot of them, and we still had to choose a proper one).

Making a wrong decision at that stage could result in a pretty much re-work on later phases:  when tens of widgets with hundreds of date fields are already implemented - changing the approach (e.g. replace all input-date components with simple inputs or 3rd-party DatePickers) could take hours of work and still could be the error-prone one.

So, I suggested to do it in a different way: not to come up with a final decision at all :)

What I did? Just created a tag library with a custom date-picker tag, that simply rendered a liferay-ui:input-date inside, and asked all the team members to use this tag when they need to render a date picker. This way we "encapsulated" the implementation of date picker in a custom tag, and could easily change it when we need.

Later on we found a nice JS Datepicker - Flatpickr, and integrated it by changing just a few lines of code in the tag implementation. Widget's code using the tag was not touched at all. If this library does not satisfy project needs in future - we can easily replace it with any other one in the same way.

The created tag library was also extended by adding other "shared" components: phone number input field, custom file upload, etc. This way we prevented code duplication, and made the implementation encapsulated, flexible and reusable.

Liferay Example

As always, the best documentation is Liferay sources. Let's take a look at captcha-taglib - a small module that contains the 'captcha' tag to render a captcha.

The "key" points here are:

  • bnd.bnd descriptor contains a Provide-Capability declaration with a 'jsp.taglib' osgi extender:
    Provide-Capability:\
        osgi.extender;\
           osgi.extender="jsp.taglib";\
           uri="http://liferay.com/tld/captcha";\
           version:Version="${Bundle-Version}"
  • taglib-mappings.properties file contains a path to TLD (tag library descriptor):
    liferay_captcha=/META-INF/liferay-captcha.tld
  • liferay-captcha.tld contains the 'captcha' tag descriptor;
  • CaptchaTag is the tag implementation, that extends the IncludeTag and renders a JSP; 
  • page.jsp is a JSP, rendered by the tag implementation.

Using this example, you can implement your own tag libraries. 

Implementation

Now, let's start tag library implementation, and implement a small one with a "date-picker" component that wraps the Liferay date picker (see 'Use Case' for details). The module structure is the following:

The bnd.bnd file should contain the OSGi extender declaration for the jsp.taglib:

Bundle-Name: LifeDev Tag Library
Bundle-SymbolicName: com.lifedev.taglib
Bundle-Version: 1.0.0
Provide-Capability:\
    osgi.extender;\
       osgi.extender="jsp.taglib";\
       uri="http://lifedev.com/tld/lifedev";\
       version:Version="${Bundle-Version}"
Web-ContextPath: /lifedev-taglib

The build.gradle file should contain an additional dependency 'javax.servlet.jsp', that is required for a tag handler implementation:

dependencies {
    compileOnly group: "com.liferay.portal", name: "release.portal.api"
    compileOnly group: "javax.servlet.jsp", name: "javax.servlet.jsp-api", version: "2.3.1"
    cssBuilder group: "com.liferay", name: "com.liferay.css.builder", version: "3.1.3"
}

The taglib-mappings.properties file should be added inside the META-INF folder and declare a path to the tag library descriptor:

lifedev=/META-INF/lifedev.tld

The lifedev.tld file contains information about the tag library and tag descriptors:

<?xml version="1.0"?>

<taglib
        version="2.1"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-jsptaglibrary_2_1.xsd"
>
    <tlib-version>1.0</tlib-version>
    <short-name>lifedev</short-name>
    <uri>http://lifedev.com/tld/lifedev</uri>

    <tag>
        <description>Renders a datepicker component.</description>
        <name>datepicker</name>
        <tag-class>com.lifedev.taglib.DatePickerTag</tag-class>
        <body-content>JSP</body-content>
        <attribute>
            <description>The datepicker field name.</description>
            <name>name</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <description>The datepicker field label.</description>
            <name>label</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <description>The date value.</description>
            <name>date</name>
            <rtexprvalue>true</rtexprvalue>
            <type>java.util.Date</type>
        </attribute>
    </tag>

</taglib>

Here we define a "lifedev" custom tag library identified by "http://lifedev.com/tld/lifedev" URI, that declares a "datepicker" tag. For the tag we also specify description,  tag handler class, and "body-content" element as "JSP" to indicate this is a JSP-based tag. Also, we define three attributes in the tag descriptor: name, label and date (datepicker value).

The DatePickerTag class is the tag implementation. It has three fields defined (name/label/date), as declared in tag descriptor. These fields are set as request attributes in a setAttributes method:

@Override
protected void setAttributes(HttpServletRequest httpServletRequest) {
    httpServletRequest.setAttribute("lifedev:datepicker:name", _name);
    httpServletRequest.setAttribute("lifedev:datepicker:label", _label);
    httpServletRequest.setAttribute("lifedev:datepicker:date", _date);
}

and cleaned up in a cleanUp one:

@Override
protected void cleanUp() {
    super.cleanUp();
    _name = null;
    _label = null;
    _date = null;
}

The getPage method returns a path to JSP file:

@Override
protected String getPage() {
    return _PAGE;
}

The setPageContext is overwritten to set the ServletContext:

@Override
public void setPageContext(PageContext pageContext) {
    super.setPageContext(pageContext);
    setServletContext(_servletContextSnapshot.get());
}

Note: the ServletContext instance is obtained using a "Snapshot" reference approach:

private static final Snapshot<ServletContext> _servletContextSnapshot = new Snapshot<>(
DatePickerTag.class, ServletContext.class, "(osgi.web.symbolicname=com.lifedev.taglib)");

In a similar way you can instantiate an instance of the OSGi service you need:

private static final Snapshot<MyService> _myService = new Snapshot<>(DatePickerTag.class, 
MyService.class);

Keep in mind: tag implementation classes are not OSGi components, and you can't use @Reference here.

Finally, a datepicker.jsp JSP file renders the tag content. It fetches request attributes defined in a tag handler class and uses them to render a date picker, using liferay-ui:input-date implementation:

<%@ include file="../init.jsp" %>

<%
    String fieldName = (String) request.getAttribute("lifedev:datepicker:name");
    String fieldLabel = (String) request.getAttribute("lifedev:datepicker:label");
    Date date = (Date) request.getAttribute("lifedev:datepicker:date");
    if (date == null) {
        date = new Date();
    }
    Calendar calendar = CalendarFactoryUtil.getCalendar();
    calendar.setTime(date);
%>

<div class="form-field">
    <label for="<%= fieldName %>">
        <liferay-ui:message key="<%= fieldLabel %>" />
    </label>
    <liferay-ui:input-date name="<%= fieldName %>"
                           yearValue="<%= calendar.get(Calendar.YEAR) %>"
                           monthValue="<%= calendar.get(Calendar.MONTH) %>"
                           dayValue="<%= calendar.get(Calendar.DATE) %>"
    />
</div>

And that's it - a custom tag is ready to to be used now.

Usage

As an example - let's create a simple "My Profile" widget, where user can update his information: first name, last name, email and date of birth. For the 'dateOfBith' field we can use the implemented "datepicker" tag. 

First, we need to add a taglib module dependency to the widget's module:

dependencies {
    compileOnly group: "com.liferay.portal", name: "release.portal.api"
    compileOnly project(":modules:lifedev-taglib")
    cssBuilder group: "com.liferay", name: "com.liferay.css.builder", version: "3.1.3"
}

Then  we need to declare the tag library in the init.jsp using the URI specified in a tag library descriptor:

<%@ taglib prefix="lifedev" uri="http://lifedev.com/tld/lifedev"  %>

Finally, we can use the tag on JSP:

<%@ include file="init.jsp" %>

<portlet:actionURL name="<%= MyProfilePortletKeys.MVC_COMMAND_EDIT_PROFILE %>" var="editProfileURL"/>
<aui:form action="${editProfileURL}" method="post">
    <div class="row">
        <div class="col-6">
            <aui:input name="firstName" label="my-profile.first-name" value="${user.firstName}"/>
        </div>
        <div class="col-6">
            <aui:input name="lastName" label="my-profile.last-name" value="${user.lastName}"/>
        </div>
    </div>
    <div class="row">
        <div class="col-6">
            <aui:input name="emailAddress" label="my-profile.email-address" value="${user.emailAddress}"/>
        </div>
        <div class="col-6">
            <lifedev:datepicker name="dateOfBirth" label="my-profile.date-of-birth" date="${user.birthday}" />
        </div>
    </div>
    <div class="row">
        <div class="col-12 text-right">
            <aui:button type="submit"/>
        </div>
    </div>
</aui:form>

The tag should render the liferay-ui:input-date component:

But the datepicker implementation logic is encapsulated now inside the tag, and can be easily modified if needed.

Next Steps

In this small sample we created a simple tag with three attributes - name, label and date. It could be enhanced by adding additional attributes, e.g. "required" to make date selection required, "readonly"/"disabled" to make it not editable, attributes for specifying allowed date range or other validation options, CSS styling options, etc. This way you can create a flexible and reusable component that can be used in different places/widgets with different configuration. 

Also, more sophisticated tags can be implemented, that can communicate with database (using "Snapshot" OSGi references), Headless APIs, or even external services. If you see a duplicated pieces of functionality in different widgets - a custom tag could be considered as an option. 

Code References

Sample for this Blog: https://github.com/vitaliy-koshelenko/lifedev-portal/tree/custom-tag-library

Sample in Liferay Sources: https://github.com/liferay/liferay-portal/tree/master/modules/apps/captcha/captcha-taglib

Conclusion

In this blog we have reviewed use cases when you might need a custom tag implementation, analyzed Liferay sources, developed a tag library with a custom datepicker tag, and used it in a custom widget.

If you have repeating functionality in different widgets, especially when the implementation details might change in future, you may consider a custom tag library as an option - to prevent code duplication and encapsulate the implementation details in a flexible and reusable component.  

 

Leave your comments and feedback.

Stand with Ukraine 

Enjoy 😏

Vitaliy Koshelenko

Liferay Architect at Aimprosoft

v.koshelenko@aimprosoft.com