The localized rich text editor

General Blogs August 4, 2013 By Adam Victor Brandizzi Staff

I'm working in an awesome component to be shipped from Liferay 6.2 onwards. Some of the features needed to implement it are rather reusable so they were implemented in the Liferay Porta itself. One of them is the new ability of the <liferay-ui:input-localized> tag of inserting a WYSIWYG editor in the page instead of an one-line input or a textarea.

How to use it

To use it, just set the type parameter of the <liferay-ui:input-localized> tag to editor, as below (source):

<portlet:renderURL var="savePostURL" />

<form action="<%= savePostURL %>" method="post">
    <liferay-ui:input-localized name="localizedEditor" xml="" type="editor" />
    <input type="submit" value="Submit post">

(Do not forget to put it inside a <form> element; it is mandatory to the <liferay-ui:input-localized> tag regardless of the type.)

Your portlet will be presented as below:

It works as any localized input: click on the flag corresponding to the language you want to translate to. The content of the editor will be cleared and you can enter your translation. If you click in a flag of a language which already has some content, the content will be shown in the editor.

To retrieve the value one can just use the usual steps of dealing with localized inputs. For example, if we want to present the translations inserted, we can use the LocalizationUtil class to retrieve the map with the submitted values. With the map, we can create e.g. a list of values, as in the lines below (source):

Map<Locale, String> translations = LocalizationUtil.getLocalizationMap(
    renderRequest, "localizedEditor");

for (Locale locale : translations.keySet()) {
    String translation = translations.get(locale);
        if (Validator.isNotNull(translation)) {
                    In <%= locale.getDisplayLanguage() %>:
                        <%= translation %>

The result could be something like this:

A new model hint

The new feature, however, shows all of its awesomeness when dealing with AlloyUI Forms and the Service Builder because we can change the model hint  of a field to use a localized rich-text editor as its input. To see it at use, let us create a new Service Builder entity named Post (source):

    <entity name="Post">
        <column name="postId" type="long" primary="true" />

        <column name="title" type="String" localized="true" />
        <column name="content" type="String" localized="true" />

Once you generate the service files, you can edit the model hint of the content column at the file docroot/WEB-INF/src/META-INF/portlet-model-hint.xml to use the new EDITOR hint collection. Replace this line...

<field name="content" type="String" localized="true" />

by these lines (source - diff):

        <field name="content" type="String" localized="true">
    <hint-collection name="EDITOR" />

Snippets for testing

For testing purposes, we can create a little form to edit it:

long postId = ParamUtil.getLong(renderRequest, "postId");
Post post = null;
if (postId > 0) {
    post = PostLocalServiceUtil.getPost(postId);

<portlet:actionURL name="savePost" var="savePostURL" />

<aui:form action="<%= savePostURL %>" method="post">
        <aui:model-context model="<%= Post.class %>" bean="<%= post %>" />    

        <aui:input name="postId" type="hidden" />

        <aui:input name="title" />
	        <aui:input name="content" />

        <aui:button name="submit" type="submit" />

We also create a portlet to process the action request (source)...

public class PostPortlet extends MVCPortlet {

    public void savePost(ActionRequest request, ActionResponse response) 
    			    throws PortalException, SystemException {
        		long postId = ParamUtil.getLong(request, "postId");

        		Map title =
                				LocalizationUtil.getLocalizationMap(request, "title");
        Map content =
				                LocalizationUtil.getLocalizationMap(request, "content");

        		Post post = (postId > 0) 
                				? PostLocalServiceUtil.getPost(postId) 
                				: new PostImpl();


		        if (postId > 0) {
        else {
} well as some code to list the saved values (source):

List<Post> posts = PostLocalServiceUtil.getPosts(0, QueryUtil.ALL_POS);

for (Post curPost : posts) {
    <portlet:renderURL var="postURL">
                <portlet:param name="postId" value="<%= String.valueOf(curPost.getPostId()) %>"/>
                <a href="<%= postURL %>"><%= curPost.getTitle(LocaleUtil.getDefault()) %></a>

Now, the input for the content will not be a line input or a textarea solely - it will be a localized rich text editor! After some input, for example, our portlet can look like this:

Summing up

This feature is not available on the bundles yet - it is coming in Liferay 6.2. Nonetheless, one can anticipate its usefulness. We hope it will make your lives easier! In fact, all model hints are pretty useful while not really widely known. Also, note that this feature allows one to define the input of a model field as a non-localized rich-text editor if one wants to.

As usual, the full source code is available at my repository of examples as the simple-localized-editor-portlet plugin.

The Liferay.Store JavaScript component

General Blogs July 25, 2013 By Adam Victor Brandizzi Staff

Since AlloyUI became a base component for Liferay, various amazing tools became available for the Liferay developer.

One of these are the JavaScript components such as Liferay.Store. This component allows the developer to store user-related data quickly on the server. To see how it works, let's consider a portlet which stores how the user prefers her coffee: black, sweet or with milk. We can just create a select input with the options (source):

How do you like your coffee? 

<select id="<portlet:namespace/>coffeePref" name="<portlet:namespace/>coffeePref"> 
    <option value="black">Black</option> 
    <option value="sweet">Sweet</option> 
    <option value="milk">With milk</option> 

As one would expect, the choice will be lost when the page is reloaded. Here enters  Liferay.Store: it asynchronously saves any information on the server, relating it to the authenticated user. In this case, when the select input changes we will store the new value. First, we open a new <aui:scipt> element, which should declare the use of the liferay-store module:

<aui:script use="liferay-store">


In it, we retrieve the input using the YUI API and bind a function to its change event:

<aui:script use="liferay-store">'#<portlet:namespace/>coffeePref').on(
        function(event) { }

Now comes the beef: the function retrieves the current value of the select input and stores it using the Liferay.Store component:

<aui:script use="liferay-store">'#<portlet:namespace/>coffeePref').on(
        function(event) {
            var instance = this;

            Liferay.Store('<portlet:namespace/>coffee-preference', instance.val());

In this case, we store the value of the input in a key composed by the portlet namespase followed by the coffee-preference suffix.

One can retrieve this value from JavaScript but it is a common pattern to fetch it on the server side, since it avoids unnecessary requests and slowdowns. This is what we'll do; for that, we use the get() method from the com.liferay.portal.util.SessionClicks class:


String coffeePreference = SessionClicks.get(request, renderResponse.getNamespace() + "coffee-preference", "");

Now, we can use the retrieved value to verify which option was selected - e.g. this way (source):
<select id="<portlet:namespace/>coffeePref" name="<portlet:namespace/>coffeePref">
    <option value="black" <%= "black".equals(coffeePreference) ? "selected" : "" %>>Black</option>
        <option value="sweet" <%= "sweet".equals(coffeePreference) ? "selected" : "" %>>Sweet</option>
    <option value="milk"  <%= "milk".equals(coffeePreference) ? "selected" : "" %>>With milk</option>
This is a rather simple process to save small, user-bound bits of data asynchronously and can save a lot of time. The full code can be found at my examples repository as the coffee-portlet plugin.
Showing 2 results.