To create a Soy portlet, you’ll need these key components:
- A module that publishes a portlet component with the necessary properties
- Controller code to handle the request and response
- Soy templates to implement your view layer
Configuring the Web Module
First, familiarize yourself with a Soy portlet’s anatomy. You may recognize it, since a Soy portlet extends an MVC portlet:
my-soy-portlet
bnd.bnd
build.gradle
package.json
src/main/
java/path/to/portlet/
MySoyPortlet.java
action/
*MVCRenderCommand.java
resources/META-INF/resources/
content/
Language.properties
View.es.js
(MetalJS component)View.soy
(Soy template)
Now that you know the basic structure of a Soy portlet module, you can configure it. You can use the soy portlet Blade template to build your initial project if you wish. Otherwise, you can follow the instructions in this section to manually configure the module.
Specifying OSGi Metadata
Add the OSGi metadata to your module’s bnd.bnd
file. A sample BND
configuration is shown below:
Bundle-Name: Liferay Hello Soy Web
Bundle-SymbolicName: com.liferay.hello.soy.web
Bundle-Version: 1.0.3
Require-Capability:\
soy;\
filter:="(type=metal)"
Include-Resource: package.json
In addition to the standard metadata, notice the Require-Capability
property.
This specifies that this bundle requires modules that provide the capability
soy
with a type
of metal
to work. Also note the Include-Resource
property. You must include your package.json
file to load the Soy
Portlet’s JavaScript files.
Specifying JavaScript Dependencies
Specify the JavaScript module dependencies in your package.json
. At a minimum,
you should have the following dependencies and configuration parameters:
{
"dependencies": {
"metal-component": "^2.4.5",
"metal-soy": "^2.4.5"
},
"devDependencies": {
"liferay-module-config-generator": "^1.2.1",
"metal-cli": "^4.0.1"
},
"name": "my-portlet-name",
"version": "1.0.0"
}
This provides everything you need to create a Metal component based on Soy. Note
that the version
in your package.json
should match the Bundle-Version
in
your bnd.bnd
file.
Next you can specify your module’s build dependencies.
Specifying Build Dependencies
Add the dependencies shown below to your build.gradle
file:
dependencies {
provided group: "com.liferay", name: "com.liferay.portal.portlet.bridge.soy", version: "3.1.0"
provided group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0"
provided group: "com.liferay.portal", name: "com.liferay.util.java", version: "2.0.0"
provided group: "javax.portlet", name: "portlet-api", version: "2.0"
provided group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
provided group: "org.osgi", name: "org.osgi.service.component.annotations", version: "1.3.0"
}
Now that your module build is configured, you can learn how to create the Soy portlet component.
Creating a Soy Portlet Component
Create a Soy Portlet component that extends the SoyPortlet
class. This
requires an implementation of the javax.portlet.portlet
service to run.
Declare this using an @Component
annotation in the portlet class:
@Component(
immediate = true,
service = Portlet.class
)
public class MySoyPortlet extends SoyPortlet {
@Override
public void render(RenderRequest renderRequest, RenderResponse renderResponse) {
//do things here
}
}
Liferay DXP’s
SoyPortlet
class
extends Liferay DXP’s
MVCPortlet
class,
which is an extension itself of javax.portlet.Portlet
, so you’ve provided the
right implementation.
The component requires some properties as well. A sample configuration is shown below:
@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.display-category=category.sample",
"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=Hello Soy 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=View",
"javax.portlet.name=hello_soy_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
)
Some of these properties may seem familiar to you, as they are the same ones
used to develop an MVC portlet. You can find a list of all the Liferay-specific
attributes that are available for use as properties in your portlet components
in the liferay-portlet-app_7_0_0.dtd
.
The javax.portlet...
properties are elements of the portlet.xml descriptor
Liferay’s DTD files can be found here
Now that you’ve set your Soy portlet component’s foundation, you can write the controller code.
Writing Controller Code
Soy portlets extend MVC portlets, so they use the same Model-View-Controller framework to operate. Your controller receives requests from the front-end, and it receives data from the back-end. It’s responsible for sending that data to the right front-end view so it can be displayed to the user, and it’s responsible for taking data the user entered in the front-end and passing it to the right back-end service. For this reason, it needs a way to process requests from the front-end and respond to them appropriately, and it needs a way to determine the appropriate front-end view to pass data back to the user.
Render Logic
The render logic is where all the magic happens. After all, what’s the use of a
portlet if you can’t see it? Note the init-param
properties you set in your
Component class:
"javax.portlet.init-param.template-path=/",
"javax.portlet.init-param.view-template=View",
This directs the default rendering to View (View.soy
). The template-path
property tells the framework the location of your Soy templates. The /
above
means that the Soy files are located in your project’s root resources
folder.
That’s why it’s important to follow Liferay DXP’s standard folder structure,
outlined above. Here’s the path of a hypothetical web module’s resource folder:
docs.liferaysoy.web/src/main/resources/META-INF/resources
In this case, the View.soy
file is found at:
docs.liferaysoy.web/src/main/resources/META-INF/resources/View.soy
That’s the default view of the application. When the init
method is called,
the initialization parameters you specify are read and used to direct rendering
to the default template. Throughout this framework, you can render a different
view (Soy template) by setting the mvcRenderCommandName
parameter of the
javax.portlet.PortletURL
to the Soy template. The example below uses a portlet
URL called navigationURL
to render the view View
:
navigationURL.setParameter("mvcRenderCommandName", "View");
Each view, excluding the default template view, must have an implementation
of the
MVCRenderCommand
class.
The *MVCRenderCommand
implementation must declare itself as a component with
the MVCRenderCommand
service, and it must specify the portlet’s name and MVC
command name using the javax.portlet.name
and mvc.command.name
properties
respectively. Below is an example MVCRenderCommand
implementation for a
Navigation
Soy template:
@Component(
immediate = true,
property = {
"javax.portlet.name=hello_soy_portlet",
"mvc.command.name=Navigation"
},
service = MVCRenderCommand.class
)
public class HelloSoyNavigationExampleMVCRenderCommand
implements MVCRenderCommand {
@Override
public String render(
RenderRequest renderRequest, RenderResponse renderResponse) {
Template template = (Template)renderRequest.getAttribute(
WebKeys.TEMPLATE);
PortletURL navigationURL = renderResponse.createRenderURL();
navigationURL.setParameter("mvcRenderCommandName", "View");
template.put("navigationURL", navigationURL.toString());
return "Navigation";
}
}
The render logic provides the view layer with information to display the data
properly to the user. In this case the MVC command name is set to Navigation
(the Soy template with namespace Navigation
). The MVC render command name for
the PortletURL
navigationURL
is set to View
(the Soy template with
namespace Navigation
), using the mvcRenderCommandName
attribute. The
navigationURL
parameter is passed to the Navigation
Soy template as the
variable navigationURL
, using the template.put()
method. Finally, the
*MVCRenderCommand
class returns the MVC render command name as a String
.
Note that Soy portlet parameters are scoped to the portlet class they’re written
in. For instance, you can have a navigationURL
parameter in two different
classes, each with a different value. Below is an example HelloSoyPortlet
class that also defines a navigationURL
parameter:
public class HelloSoyPortlet extends SoyPortlet {
@Override
public void render(
RenderRequest renderRequest, RenderResponse renderResponse)
throws IOException, PortletException {
PortletURL navigationURL = renderResponse.createRenderURL();
navigationURL.setParameter("mvcRenderCommandName", "Navigation");
template.put("navigationURL", navigationURL.toString());
template.put("releaseInfo", ReleaseInfo.getReleaseInfo());
super.render(renderRequest, renderResponse);
}
}
The navigationURL
points to the Navigation
Soy template this time. The
navigationURL
and releaseInfo
parameters are passed to the View
Soy
template. Since this logic should be executed before the default render
method, the method concludes by calling super.render
.
Now that you understand the render logic, you can learn how the view layer works.
Configuring the View Layer
Your portlet also requires a view layer, and for that you’ll use Soy templates, which is the whole point of developing a Soy portlet, isn’t it? This section briefly covers how to get your view layer working, from including other Soy templates, to creating a MetalJS component for rendering your views.
Soy templates are defined in a file with the extension .soy
. The filename is
arbitrary. The Soy template’s name is specified at the top of the template using
the namespace
declaration. For example, this template has the namespace View
:
{namespace View}
It can be accessed in another Soy template by calling the render
method on the
namespace as shown below:
{call View.render data="all"}{/call}
Below is an example View
Soy template that includes Header
and Footer
Soy
templates:
{namespace View}
/**
* Prints the portlet main view.
*/
{template .render}
<div id="{$id}">
{call Header.render data="all"}{/call}
<p>{msg desc=""}here-is-a-message{/msg}</p>
{call Footer.render data="all"}{/call}
</div>
{/template}
Each view has a corresponding *es.js
file (usually with the same name) that
imports the Soy templates the view requires and registers the view as a MetalJS
component. This file is also used for any additional JavaScript logic your view
may have. For example, here is a View.es.js
component for a View.soy
template:
import Component from 'metal-component/src/Component';
import Footer from './Footer.es';
import Header from './Header.es';
import Soy from 'metal-soy/src/Soy';
import templates from './View.soy';
/**
* View Component
*/
class View extends Component {}
// Register component
Soy.register(View, templates);
export default View;
Now that you understand how to configure a Soy template view, you can learn how to use portlet parameters in your Soy templates next.
Using Portlet Template Parameters in the Soy Template
As mentioned above, the template.put()
method exposes portlet parameters to
the Soy templates. Once a parameter is exposed, you can access it in the Soy
template by defining it at the top with the {@param name: type}
declaration.
For instance, the hello-soy-web
portlet’s View
Soy template defines the
navigationURL
parameter with the code below:
{@param navigationURL: string}
It is then used to navigate between portlet views:
<a href="{$navigationURL}">{msg desc=""}
click-here-to-navigate-to-another-view
{/msg}</a>
Some Java theme object variables are available as well. For example, to access
the
ThemeDisplay
object
in a Soy template, use the following syntax:
{$themeDisplay}
You can also access the Locale
object by using {$locale}
. Here is the full
View.soy
template for the com.liferay.hello.soy.web
portlet, which
demonstrates the features covered in this section:
{namespace View}
/**
* Prints the portlet main view.
*/
{template .render}
{@param id: string}
{@param layouts: list<[
friendlyURL: string,
nameCurrentValue: string
]>}
{@param navigationURL: string}
<div id="{$id}">
{call Header.render data="all"}{/call}
<p>{msg desc=""}here-you-will-find-how-easy-it-is-to-do-things-like{/msg}</p>
<h3>{msg desc=""}listing-pages{/msg}</h3>
<div class="list-group">
<div class="list-group-heading">{msg desc=""}navigate-to{/msg}</div>
{foreach $layout in $layouts}
<a class="list-group-item" href="{$layout.friendlyURL}">{$layout.nameCurrentValue}</a>
{/foreach}
</div>
<h3>{msg desc=""}navigating-between-views{/msg}</h3>
<a href="{$navigationURL}">{msg desc=""}click-here-to-navigate-to-another-view{/msg}</a>
{call Footer.render data="all"}{/call}
</div>
{/template}
Now you know how to create a Soy Portlet!