Tutorial of creating soy portlet

In Liferay DXP it supports soy template with portlet. Through soy template we can take advantage of  Google Closure to build rich web application.
 
In next sections I am going to lead you in to a journey of Soy portlet.
 
Tools we use: 
    Liferay IDE 3.1. -- Super awsome official tool to help us to create project and run gradle task.
    Atom Editor. -- Professional UI development tool.(Or you can use whatever you are familiar tool)
    Git Bash -- Reversion control and windows - Linus EOL/EOF convert.
 
Knowledge:
    Familiar with Liferay OSGi portlet module development. If you are a stranger of this, please check this  blog to learn how to create a mvc portlet.
    Familiar with NodeJS development.
    Familiar with Git
 
 
Step 1 Portlet Creation with soy frame work
 
Let's create a mvc portlet module. In my case I call my project sample-soy.
The difference between soy portlet and mvc portlet class is soy portlet class extends SoyPortlet com.liferay.portal.portlet.bridge.soy.SoyPortlet which is a child class of MVCPortlet.
Modify the super class to be SoyPortlet
So you need to add the dependency to build.gradle correspondingly.
compileOnly group: "com.liferay", name: "com.liferay.portal.portlet.bridge.soy", version: "3.0.0"
 
Make your Soy portlet component annotation like this:
 
@Component(
    immediate = true,
    property = {
            "com.liferay.portlet.add-default-resource=true",
            "com.liferay.portlet.application-type=full-page-application",
            "com.liferay.portlet.application-type=widget",
            "com.liferay.portlet.css-class-wrapper=table-wrapper",
            "com.liferay.portlet.display-category=category.sample",
            "com.liferay.portlet.header-portlet-css=/SoySample.css",
            "com.liferay.portlet.layout-cacheable=true",
            "com.liferay.portlet.preferences-owned-by-group=true",
            "com.liferay.portlet.private-request-attributes=false",
            "com.liferay.portlet.private-session-attributes=false",
            "com.liferay.portlet.render-weight=50",
            "com.liferay.portlet.scopeable=true",
            "com.liferay.portlet.use-default-template=true",
            "javax.portlet.display-name=Soy Sample Portlet",
            "javax.portlet.expiration-cache=0",
            "javax.portlet.init-param.copy-request-parameters=true",
            "javax.portlet.init-param.template-path=/",
            "javax.portlet.init-param.view-template=SoySample",
            "javax.portlet.name=soy_sample_portlet",
            "javax.portlet.resource-bundle=content.Language",
            "javax.portlet.security-role-ref=guest,power-user,user",
            "javax.portlet.supports.mime-type=text/html"
    },
    service = Portlet.class
)
According to this annotation, we will have SoySample.soy as our defaule view. We will have SoySample.scss for our css style
 
Step 2 node config file
 
In the build tool, we utilize NodeJS to build and compile UI elements soy, metal, other js, css, and etc.
Create a package.json in project root folder with content:
{
    "dependencies": {
        "metal-component": "^2.4.5",
        "metal-soy": "^2.4.5"
    },
    "devDependencies": {
        "liferay-module-config-generator": "^1.1.10",
        "metal-cli": "^1.3.1"
    },
    "name": "soy-sample",
    "version": "1.0.0"
}
It's a good practice that make the version be consistent with version in bnd.bnd
 
 
Step 3 OSGi config
 
Add the following code to the bnd.bnd so that bnd tool knows package.json need to be in the jar file, and the OSGi bundle need soy capability.
 
Include-Resource: package.json
Require-Capability: soy;filter:="(type=metal)"
 
Step 4 Use Render method to define variables in soy template.
 
Add/Modify your render method in portlet class.
    @Override
    public void render(
            RenderRequest renderRequest, RenderResponse renderResponse)
        throws IOException, PortletException {

        template.put("msg", "Good Job!");

        super.render(renderRequest, renderResponse);

    }
 
We will have a parameter in our soy template called msg with the a string value " Good Job! "
 
 
Step 5 SoySample template
 
Create a file called SoySample.soy in this path src/main/resources/META-INF/resources/resources
With the following code:
{namespace SoySample}

/**
 * Show portlet message in the view.
 * @param id
 * @param msg
 */
{template .render}
     <div id="{$id}">
        <div class="form-group">
            <div class="input-group">
                <span class="input-group-addon" id="inputGroupAddon01">Message:</span>
                <input aria-describedby="inputGroupAddon01" class="form-control" value="{$msg}" type="text">
            </div>
        </div>

        <div class="form-group">
            <div class="input-group">
                <input aria-describedby="inputGroupAddon02" class="form-control" placeholder="Recipient's username, ex. neil.jin ;)" type="text">
                <span class="input-group-addon" id="inputGroupAddon02">@liferay.com</span>
            </div>
        </div>

        <div class="form-group">
            <div class="input-group">
                <span class="input-group-addon">$</span>
                <input aria-label="Amount" class="form-control" type="text">
                <span class="input-group-addon">.00</span>
            </div>
        </div>

        <div class="form-group">
            <button class="btn btn-default" type="button">Donate</button>
        </div>
     </div>
{/template}
 
Closure require the template's declaration in java-doc like comment. So the comment about the {templlate .render} is required. All the param has to be used in the template.
And {template .render} has to be the beginning or the line without and space characters.
 
Everytime you change the soy file, it's better to run build soy gradle task to generate soy.js
 
In my template I utilized  Lexicon to build my UX.
 
In the same folder, create a file called SoySample.scss, just leave it blank.
 
Run buildSoy gradle command.
 
Step 6 test and check.
 
Now you can build your module and deploy the jar to your Liferay server and test.
If everything is correct you are suppose to see this view from your portlet.
 
When you build, the task tries to download necessary node_modules, it might take a while(about 100m). 
Sometimes it get stuck on npmInstall, you can stop the process and build again, it will work.
 
Step 7 javascript actions
 
I am going to add javascript to my soy template.
Create a file called SoySample.es.js with the same folder of SoySample.soy with the following code:
 
import Component from 'metal-component/src/Component';
import core from 'metal/src/core';
import dom from 'metal-dom/src/dom';
import Soy from 'metal-soy/src/Soy';
import templates from './SoySample.soy';

class SoySample extends Component {

}

//Register component
Soy.register(SoySample, templates);

export default SoySample;
Our js framework is done.
 
Step 8 add javascript method
 
We are going to create a donate method to send out a message to our js concole.
Add the following method to the class SoySample
 
   /**
     * donate to neil
     *
     * @param  {MouseEvent} event
     */
    donate(event) {
        console.log("donate successful");
     }
 
Step 9, use method
 
We will make this method be executed when user click donate button, so add data-onclick="donate" attribute to donate button.
It will look like this:
 
<button class="btn btn-default" data-onclick="donate" type="button">Donate</button>
 
After you have done everything, you can start build and deploy your project. When you click on donate button, your browser console should show  a message "donate successful"
 
 
Troubleshoot:
Because of an issue of soy compiler, the compiler can't identify the end of line and end of file character.
We have to manually convert CRLF to LF. On windows you may meet js error "TypeError: fn is not a function" in the browser.
 
To solve this we are going to utilize git.
You can create a file in your project's git repo named   .gitattributes, with the following content:
*.pom text eol=lf
*.soy text eol=lf
ivy.xml text eol=lf
After you have added your work to the index and commit, the CRLF should be automatically converted to LF.
 
You can also take advantage of IDE 3.1 with AnyEdit eclipse plugin. (Thanks @Greg for the info)
 
You can also utilize Atom Editor with line-ending-selector
 
Hope you enjoy.
The attachment is a compiled soy portlet jar, you can downlad and deploy to your portal for a test.
 
Blogs
[...] Previously we have talked about how to create a soy portlet, how to use a 3rd party js lib(ChartJS) in SOY portlet, and how to use Liferay Service Builder to create a remote service(web service) in... [...] Read More
[...] The purpose of this tutorial is to let you know how to involve a 3rd part js lib in metal soy portlet and how to use them in ES6 script. In Liferay DXP we provide abundant OOTB js lib to help... [...] Read More