Een introductie tot op maat gemaakte Liferay Forms hero image
CONTENT & COLLABORATIONLIFERAY
19/11/2019 • Koen Olaerts

An introduction to Liferay Forms customization

Since the dawn of time people have been collecting all sorts of things: comic books, video games, stamps, old currency and even rocks minerals. However… times change and most of us are no longer interested in collecting minerals. Instead, we collect other people’s personal information and opinions. With the coming of Liferay 7.0, Liferay has blessed us with a tool to do so. This blog post outlines Liferay Forms and explains how you can customize them to your liking!

Enter Liferay Forms!

The gracious overlords at Liferay have recognized our desire to gather data and have provided us with a guide on how we should be collecting information from users. However, our data-hungry stomachs are not easily pleased when it comes to collecting other people’s information. So what does Liferay Forms have to offer us? Liferay Forms is a tool which allows users to create dynamic and flexible surveys or questionnaires. By default, Liferay Forms offers a wide range of field types and settings. Let’s take a quick look!

Default field types

Field type   Used to
Form Text Inform the user about the (need of the) form. Requires no input.
Text Field Fill in one or multiple lines of plain text
Select from list Select an entry from a preconfigured dropdown list
Single selection Select a single option
Date Provide a date
Single checkbox Confirm a statement
Multiple selection Select one or multiple options

Default field type settings

Setting

Form Text

Text Field

Select from list

Single Selection

Date

Single Checkbox

Multiple Selection

Required field X X X X X X
Help text X X X X X X
Show label X

X X X X X
Placeholder X
Predefined value X X X X X X
Field validation X
Field visibility expression X X X X X X X
Repeatable X X X X
Prefill from external provider X

Additionally, there are some general settings which can be configured for each form, such as:

  • enabling a CAPTCHA,
  • redirecting to a different URL after a successful form submission,
  • sending an email notification to administrators whenever a form is submitted,
  • using one or more pages per form,
  • and more!

Customizable layout

In order to convince the user to give up their precious personal information willingly, forms need to look as attractive as possible. To that purpose, Liferay has provided a way to customize the entire look and feel of the form, including its font and colour. The most interesting feature is the use of a multi-column approach. With this, it’s possible to put multiple fields on a single row and customize this for each subsequent row, as shown in the picture below.

Are you not entertained?

To be perfectly honest, neither am I. Liferay Forms is most certainly an upgrade from its predecessor in 6.2 and provides a way to quickly construct a dynamic survey. However, the default settings and field types feel rather limited and leave something to be desired. Some things I personally missed were:

  • there is no way to prefill the form with data that is already stored in the database,
  • the external data providers are limited to the field ‘Select from list’.

Using precious data that is already stored in the database saves the user from redundant work. It also confronts them with the gruesome truth that we already know so much about them.

Creating custom Liferay Forms fields

One day a rather large old(er) man, with a gray pointy hat, approached me and asked me to do just that: autofill a field with data that is already stored in the database. I graciously accepted this quest and thus my adventure with custom form fields began.

Where to start?

The gracious overlords at Liferay have once more blessed us with a gift, this time with the gift of documentation. In order to get familiar with the concept of creating a custom form field, I dove right into their archives and started looking for useful information. Being the kind and gentle soul that I am, I’m willing to share with you what I found:

Creating a form field module can be troublesome and tiresome. Luckily for us there is a Maven archetype available to do the work for us:

mvn archetype:generate \
    -DarchetypeGroupId=com.liferay \
    -DarchetypeArtifactId=com.liferay.project.templates.form.field \
    -DgroupId=com.liferay \
    -DartifactId=my-form-field-project \
    -Dpackage=com.liferay.docs \
    -Dversion=1.0 \
    -DclassName=MyFormField \
    -Dauthor=Joe Bloggs \
    -DliferayVersion=7.0

This archetype creates a module with the correct structure described in anatomy of a form field module. Building and deploying this module adds the newly made field to Liferay Forms.

Note: building this module should compile your template described in my-form-field.soy and generate a file called ‘my-form-field.soy.js’. If this isn’t the case, consider manually compiling and adding the file using an online compiler as a temporary workaround.

Now that we have a working module, we’re free to customize the template and settings of our field(s) as we see fit.

Prefilling Liferay Forms

Imagine you’re the proud owner of a fine establishment called ‘The Pizza Place’ and you’re looking to improve your customer experience. Naturally, you’d like your customers to be able to order a pizza online. But what if we could go even further beyond and partially fill in their order for them? Now before we show off our prowess, completely fill in their form and choose their pizza for them, let’s start with something basic like their name.

The name-field module does just that. It uses the name of the logged in user and automatically fills it in the form. That being said, the user is still able to edit the name if needed. In order for us to accomplish this amazing feat, we’ll have to make some adjustments. First, if you take a look at the NameFieldRenderer, you will notice the method ‘populateOptionalContext(… , … , …)’ which we’re using to pass down the username to the template. Using the DDMFormFieldRenderingContext, we have access to the ThemeDisplay which in turn gives us access to the logged in User.

Override
protected void populateOptionalContext(Template template, DDMFormField ddmFormField, DDMFormFieldRenderingContext ddmFormFieldRenderingContext) {
        HttpServletRequest httpServletRequest = ddmFormFieldRenderingContext.getHttpServletRequest();
	ThemeDisplay themeDisplay = (ThemeDisplay) httpServletRequest.getAttribute(WebKeys.THEME_DISPLAY);
 
	User user = themeDisplay.getUser();
 
	template.put("username", user.getFullName());

Next, we’ll have to access the username in our template described in name-field.soy. Simply add the ‘username‘ to the list of parameters and pass it down to the value attribute of the input field.

Note:  the name attribute of the input field along with its value is used to store the value and should NOT be removed!

{namespace ddm}
 
/**
* Prints the form field.
*
* @param label
* @param name
* @param required
* @param showLabel
* @param tip
* @param username
*/
{template .NameField}
	<div class="form-group name-field" data-fieldname="{$name}">
		{if $showLabel}
			<label class="control-label">
				{$label}
 
				{if $required}
					<span class="icon-asterisk text-warning"></span>
				{/if}
			</label>
 
			{if $tip}
				<p class="liferay-ddm-form-field-tip">{$tip}</p>
			{/if}
		{/if}
		<br>
		<input class="field form-control" id="{$name}" name="{$name}" type="text" value="{$username}">
	</div>
{/template}

Easy enough, right?

Not so fast, you activated Liferay’s trap card!

For reasons that go beyond my current understanding, Liferay overwrites the template with our component described in name-field component. Seeing as we have no way of knowing what the username is on the client-side, our username provided by the server will be overwritten with whatever we define as its value in this component. In this case we did not define its value, so our username would be overwritten by an empty string. More information about this component can be found in Liferay’s documentation.

AUI.add(
	'name-field',
	function(A) {
		var NameField = A.Component.create(
			{
				ATTRS: {
					username:{}
				},
 
				EXTENDS: Liferay.DDM.Renderer.Field,
 
				NAME: 'name-field',
 
				prototype: {
					getTemplateContext: function() {
						var instance = this;
 
						return A.merge(
							NameField.superclass.getTemplateContext.apply(instance, arguments),
							{
								username: fetchUsername(instance)
							}
						);
					}
				}
 
			}
		);
 
		Liferay.namespace('DDM.Field').NameField = NameField;
	},
	'',
	{
		requires: ['liferay-ddm-form-renderer-field']
	}
);
 
function fetchUsername(instance) {
	return instance.get('container') == undefined ? '' : (instance.getInputNode() != null ? instance.getValue() : '');
}

However we can access the template’s value before it’s overwritten by our component through the instance object and thus pass it down to our own component. The function ‘fetchUsername(instance)’ accomplishes just that. Additional checks are needed however to make sure the template does not break when you’re configuring your form within the control panel. This does feel a bit hacky at the moment,  but so far I haven’t found another way around this little annoyance.

What if my data is stored somewhere else?

Let’s say you’d like to use data from an external provider, how would we go about this? As stated before, the template passed down by the server is overwritten by our custom component. Using this to our advantage we could fetch the data from the external provider at the client-side and pass it down to our component, as demonstrated in the email-field component.

Note: For the purpose of this demonstration I’ve provided an endpoint, in the custom-form-fields-endpoints, that (given a userId) fetches the user’s email address.

AUI.add(
	'email-field',
	function(A) {
		var EmailField = A.Component.create(
			{
				ATTRS: {
					email: {
						value: ''
					},
					prefill: {}
				},
 
				EXTENDS: Liferay.DDM.Renderer.Field,
 
				NAME: 'email-field',
 
				prototype: {
					getTemplateContext: function() {
						var instance = this;
 
						return A.merge(
							EmailField.superclass.getTemplateContext.apply(instance, arguments),
							{
								email: renderEmail(instance)
							}
						);
					}
				}
 
			}
		);
 
		Liferay.namespace('DDM.Field').EmailField = EmailField;
	},
	'',
	{
		requires: ['liferay-ddm-form-renderer-field']
	}
);
 
function renderEmail(instance) {
	return instance.get('prefill') ? fetchEmail() : '';
}
 
function fetchEmail() {
	var xmlHttp = new XMLHttpRequest();
	var url = "http://localhost:8080/o/custom-forms/forms/" + getLiferayUserId() + "/email";
	xmlHttp.open( "GET", url, false );
	xmlHttp.send( null );
 
	return xmlHttp.responseText
}
 
 
function getLiferayUserId() {
	return Liferay.ThemeDisplay.getUserId();
}

In this scenario we do not need to concern ourselves with the EmailFieldRenderer and we can simply pass down an empty string. That does serve a purpose: it makes sure that the field does not display ‘null’ before it’s overwritten by our component.

Note: If you prefer to make the call on the server-side, you can still pass its value down to the template and use the previous approach to fill in the field.

@Override
protected void populateOptionalContext(Template template, DDMFormField ddmFormField, DDMFormFieldRenderingContext ddmFormFieldRenderingContext) {
   template.put("email", "");
}

What if the integrity of the data of the external provider can’t be guaranteed anymore?

Let’s say for some reason the external provider can’t be trusted anymore. In that case you’d like stop autofilling this field. This can be accomplished with a single additional configurable setting. In the EmailFieldTypeSettings, we’ll have to add a basic setting ‘prefill’ to the DDMFormLayout and define it as a boolean.

@DDMForm
@DDMFormLayout(
	paginationMode = com.liferay.dynamic.data.mapping.model.DDMFormLayout.TABBED_MODE,
	value = {
		@DDMFormLayoutPage(
			title = "%basic",
			value = {
				@DDMFormLayoutRow(
					{
						@DDMFormLayoutColumn(
							size = 12,
							value = {
									"label", "required", "tip", "prefill"
								}
							)
						}
					)
				}
			),
			@DDMFormLayoutPage(
				title = "%properties",
				value = {
					@DDMFormLayoutRow(
						{
							@DDMFormLayoutColumn(
								size = 12,
								value = {
									"dataType", "name", "showLabel", "repeatable",
									"type", "validation", "visibilityExpression"
								}
							)
						}
					)
				}
			)
		}
)
public interface EmailFieldTypeSettings extends DefaultDDMFormFieldTypeSettings {
 
	@DDMFormField(label = "%prefill")
	boolean prefill();
}

This adds a checkbox to the field configuration.

Last but not least, we have to add an additional check to our email-field component. This component determines whether to fetch data from the external provider or not.

function renderEmail(instance) {
	return instance.get('prefill') ? fetchEmail() : '';
}

Takeaway + parting gift

And so my short adventure in the wonderful world of Liferay Forms has come to an end. Although it felt cumbersome at times, made my eyebrows frown more than once and made me question some fundamental life questions, it does feel like Liferay Forms has the potential to do what it was designed to do in a flexible and extendable way: steal gather people’s information and feedback.

As a reward for you having made it this far into the blog post, I’ve provided a repository which you are free to use. It contains the fields and endpoints discussed in this blog:

  • the autofilled name-field,
  • the autofilled email-field,
  • a custom Liferay endpoint.

Thanks for reading!

Koen Olaerts