Some data-centric applications require the creation of large data-entry forms. Examples abound in healthcare, transportation, pharmaceutical, or any other heavily regulated industry. For these applications, you need an easy way to section off your forms into easily navigable groups.
Since Liferay DXP 7.0, the Form Navigator framework enables you to add new
sections and section categories dynamically to existing form navigation. The
framework includes a well-described API and a powerful
liferay-ui
tag
called
form-navigator
.
It’s easy to use and facilitates organizing large forms into sections of input
and categories.
Figure 1: The Form Navigator framework lets you add your app's configuration forms to existing form navigators, like the one used in Portal Settings.
Form Navigators can be used in two ways: customizing a Form Navigator that
already exists in the portal, and creating your own Form Navigator for your
application. This tutorial demonstrates customizing an existing form. It
references source code from an example portlet called the Form Nav Extension
portlet. On GitHub, you can find its complete project called
form-nav-extension-portlet.
You can also download the Form Nav Extension portlet’s bundle
form-nav-extension-portlet-1.0.jar
. To download it, go to its GitHub
page
and click the View Raw link.
Before extending a Form Navigator, you should understand the parts of the Form Navigator Framework and what they do.
Understanding the Parts of the Form Navigator Framework
Form Navigator implementations contain the following parts:
- A JSP that contains a form: The form must specify a
form-navigator
tag. All the form’s input sections tie into the Form Navigator. - Sections: A section (or entry) is a JSP that specifies inputs and a Java class that models the section and ties it into the Form Navigator.
- Categories: A category aggregates one or more sections. A Java class models it and ties it into the Form Navigator.
- IDs: A Form Navigator and its categories should be publicly identified. That is, they should all have IDs that can be looked up in a public API (e.g., Javadoc). A developer extending a Form Navigator must otherwise have access to the Form Navigator’s source code in order to find the IDs.
Liferay’s Form Navigator implementations meet all these requirements. They’re
implemented similarly and their IDs are published in the Javadoc for the class
FormNavigatorConstants
.
Now that you know the parts of a Form Navigator, it’ll be easier for you to extend one.
Extending a Form Navigator
Here’s an overview of the steps to extend a Form Navigator:
- Step 1: Implement a component portlet project to accommodate form navigation.
- Step 2: Create a JSP for each new section of inputs.
- Step 3: Identify the Form Navigator and category (if any) you’re extending.
- Step 4: Create new categories.
- Step 5: Create new sections.
Let’s set up a component portlet project to support form navigation.
Step 1: Implement a component portlet project to accommodate form navigation
First, your component portlet must be implemented as an OSGi bundle. You
can develop it in any environment that supports creating a bundle. Please refer
to the Liferay DXP 7.0 tutorial section Tooling
to learn about development environments. The Form Nav Extension portlet
was created with BLADE based on the blade.portlet.jsp
template. This is also
covered in the tutorial section linked above.
A Form Navigator extension bundle’s metadata must do the following things:
- Specify the bundle’s symbolic name for your servlet context to target
- Include your project’s classes, JSPs, and resource bundles (for localization)
- Include the
JspAnalyzerPlugin
to generate generate metadata for the JSPs’ dependencies - Specify a web context path for the Form Navigator classes to associate with the JSPs
You should use a bnd.bnd
file to specify this metadata. Your Bnd file should
include definitions and directives similar to those specified in the Form Nav
Extension portlet’s bnd.bnd
file:
Bundle-Name: Form Nav Extension Portlet
Bundle-SymbolicName: com.liferay.docs.formnavextensionportlet
Bundle-Version: 1.0.0
Include-Resource:\
classes,\
META-INF/resources=src/main/resources/META-INF/resources
-jsp: *.jsp,*.jspf
-plugin.jsp: com.liferay.ant.bnd.jsp.JspAnalyzerPlugin
Web-ContextPath: /formnavextensionportlet
The Bundle-Name
value is arbitrary, but should be recognizable and unique. The
Bundle-SymbolicName
must be unique–the project’s package path makes for a
good symbolic name. For the Include-Resource
, make sure to include your
project’s classes and the root path of its JSPs. The directive below
includes all the project’s .jsp
and .jspf
files:
-jsp: *.jsp,*.jspf
And the following directive to specify a plugin to include all the JSP dependencies:
-plugin.jsp: com.liferay.ant.bnd.jsp.JspAnalyzerPlugin
Lastly, the Web-ContextPath
specifies the root of the portlet’s web context.
As you progress through this tutorial, you’ll refer to the metadata in your portlet’s classes. Before diving into the Java classes, howerver, let’s create JSPs for your sections’ inputs.
Step 2: Create a JSP for each new section of inputs
The existing Form Navigator has a form, and each of its sections extend the form
with sets of input. Your section will add its own inputs. You should create your
section’s JSP under the META-INF/resources
property folder you defined in
your bnd.bnd
file’s Include-Resource
directive. The example Bnd file
specified the folder like this:
META-INF/resources=src/main/resources/META-INF/resources
Under the folder, you can add a JSP for each section of inputs you want to add to the Form Navigator. Feel free to organize these with subfolders.
The Form Nav Extension portlet’s JSP file /portal_settings/my_app.jsp
provides
a checkbox input to enable/disable My App’s feature in the portal:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://liferay.com/tld/aui" prefix="aui" %>
<%@ taglib uri="http://liferay.com/tld/ui" prefix="liferay-ui" %>
<%@ page import="com.liferay.docs.formnavextensionportlet.MyAppWebKeys" %>
<%@ page import="com.liferay.portal.kernel.util.GetterUtil" %>
<%@ page import="com.liferay.portal.kernel.util.ResourceBundleUtil" %>
<%@ page import="java.util.ResourceBundle" %>
<%
boolean companyMyAppFeatureEnabled = GetterUtil.getBoolean(request.getAttribute(MyAppWebKeys.COMPANY_MY_APP_FEATURE_ENABLED));
ResourceBundle resourceBundle = ResourceBundleUtil.getBundle("content.Language", request.getLocale(), getClass());
%>
<h3><liferay-ui:message key='<%= resourceBundle.getString("my-app-features") %>' /><h3>
<aui:input checked="<%= companyMyAppFeatureEnabled %>" label='<%= resourceBundle.getString("enable-my-app-feature") %>' name="settings--myAppFeatureEnabled--" type="checkbox" value="<%= companyMyAppFeatureEnabled %>" />
The input’s name is settings--myAppFeatureEnabled--
. So that the Form Navigator
detects the inputs automatically, make sure to start each of your input’s names
with settings--
and end them with --
. Add all the inputs you need in each of
your sets of inputs.
After creating section JSPs, you must find out the IDs of the existing Form Navigator and categories you’re adding sections to. You refer to these IDs in the category and section classes you’ll create to represent your Form Navigator extensions.
Step 3: Identify the form navigator and category you’re extending
Liferay’s class
FormNavigatorConstants
specifies constant values for all the portlet Form Navigators and categories.
The identifiers follow the naming conventions below, where you substitue FOO
for the navigator’s or category’s name:
- Form navigator:
FORM_NAVIGATOR_ID_FOO
- Category:
CATEGORY_KEY_FOO
Note the names of the constants that match the Form Navigator and categories you’re extending–you’ll refer to them in your Java classes in the next steps.
Step 4: Create new categories
To add a new category, create a class that implements the
FormNavigatorCategory
interface. The class needs a @Component
annotation to register it as a
service in Liferay’s module framework. Here are the things your category class’s
component annotation should do:
- Declare that the component is of service type
FormNavigatorCategory.class
- Request immediate loading
- Optionally, specify the category’s entry order relative to the Form Navigator’s other categories. The higher the category’s order integer relative to the order of the other categories, the higher the category is listed in the form navigation.
Here’s an example component annotation for a category:
@Component(
immediate = true,
property = {"form.navigator.category.order:Integer=20"},
service = FormNavigatorCategory.class
)
Next, you implement the
FormNavigatorCategory
methods:
getFormNavigatorId
: Return the Form Navigator’s constant you noted previously fromFormNavigatorConstants
getKey
: Return an identifier for your category. You can optionally create a public class likeFormNavigatorConstants
, to publish your project’s identifiers.getLabel(Locale)
: Return the localized category label. You can create aLanguage.properties
file in your project’ssrc/main/resources/content
folder and specify a key/value pair for the category label. You can then add the localized value of the property in language properties files for other locales.
For example, here’s a Form Navigator category implementation for the Social Portal Setting category:
package com.liferay.portal.settings.web.internal.servlet.taglib.ui;
import com.liferay.portal.kernel.language.LanguageUtil;
import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorCategory;
import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorConstants;
import java.util.Locale;
import org.osgi.service.component.annotations.Component;
/**
* @author Sergio González
* @author Philip Jones
*/
@Component(
immediate = true, property = {"form.navigator.category.order:Integer=20"},
service = FormNavigatorCategory.class
)
public class CompanySettingsSocialFormNavigatorCategory
implements FormNavigatorCategory {
@Override
public String getFormNavigatorId() {
return FormNavigatorConstants.FORM_NAVIGATOR_ID_COMPANY_SETTINGS;
}
@Override
public String getKey() {
return FormNavigatorConstants.CATEGORY_KEY_COMPANY_SETTINGS_SOCIAL;
}
@Override
public String getLabel(Locale locale) {
return LanguageUtil.get(locale, "social");
}
}
After you’ve implemented new Form Navigator categories, you can add new Form Navigator sections.
Step 5: Create new sections
To add a new section (entry) that uses a JSP, create a class that extends
the abstract base class
BaseJSPFormNavigatorEntry
and implements the
FormNavigatorEntry
interface. The BaseJSPFormNavigatorEntry
base class integrates the section’s
JSP with the Form Navigator. In both these parts of your class declaration, you must
specify the Form Navigator’s model bean class as the generic type on which they
operate. For example, if your Form Navigator’s model bean class is User
, your
decarlation would be like this:
public class MyEntry extends BaseJSPFormNavigatorEntry<User>
implements FormNavigatorEntry<User>
There are a couple different ways to determine the model bean class.
If you can access the Form Navigator’s JSP source code, inspect the
form-navigator
element’s formModelBean
attribute value. The model bean class is the class
type of the object passed in as the form-navigator
’s formModelBean
attribute.
You can also deduce the model bean class from the name of the ID’s constant in
FormNavigatorConstants
.
The word(s) right after FORM_NAVIGATOR_ID_
in the constant’s name hints at
the class type. For example, if the navigator’s ID is
FORM_NAVIGATOR_ID_USERS_SETTINGS
, then User
is the model bean class; if the
ID is FORM_NAVIGATOR_ID_ORGANIZATIONS
, then Organization
is the class; etc.
Note: this is only a hint, and there are exceptions. For example, if the ID is
is FORM_NAVIGATOR_ID_SITES
, then Group
is the class.
For example, here’s a class declaration of a FormNavigatorEntry
, from the Form
Nav Extension portlet:
@Component(
immediate = true,
property = {
"form.navigator.entry.order:Integer=71"
},
service = FormNavigatorEntry.class
)
public class MyAppCompanySettingsFormNavigatorEntry
extends BaseJSPFormNavigatorEntry<Company>
implements FormNavigatorEntry<Company> {
// ...
}
It implements an entry on the Company
model bean for the Portal Settings Form
Navigator: the navigator identifed by the constant
FormNavigatorConstants.FORM_NAVIGATOR_ID_COMPANY_SETTINGS
.
The class also includes the @Component
annotation, which is the next thing an
entry must specify. An entry’s @Component
annotation registers the entry as a
service in Liferay’s module framework.
Here’s what a Form Navigation entry’s component annotation should do:
- Declare that the component is of service type
FormNavigatorEntry.class
- Request immediate loading
- Optionally, specify a
form.navigator.entry.order
property for the entry relative to the other entries in the category. The higher the entry’s order integer relative to the order of the category’s other entries, the higher the entry is listed in the category. For example,@Component(property = {"form.navigator.entry.order:Integer=71"}, service = FormNavigatorEntry.class)
Except for your entry’s order (optional), your entry’s @Component
annotation
should look similar to the previous example’s annotation. Next, you’ll implement
the entry class’s methods.
The
FormNavigatorEntry
implementation must implement the following methods:
-
getFormNavigatorId
: Return the Form Navigator’s constant you noted previously fromFormNavigatorConstants
-
getCategoryKey
: Return the Form Navigator category constant you noted previously fromFormNavigatorConstants
-
getKey
: Return an identifier for your entry. You can optionally create a public class likeFormNavigatorConstants
, to publish your project’s identifiers. -
getLabel(Locale)
: Return the entry’s localized label. You can create aLanguage.properties
file in your project’ssrc/main/resources/content
folder and specify a key/value pair for the entry label. -
getJspPath
: Return the path to the entry’s JSP, starting from the path you specified previously for yourbnd.bnd
file’sMETA-INF/resources
property. -
include(HttpServletRequest, HttpServletResponse)
: Sets the request and response attributes for displaying the entry’s HTML. You can retrieve the form’s current settings and pass them to the request. You can optionally use a template (e.g., FreeMarker or Velocity) to render the form page, as long as you completely overrideBaseJSPFormNavigatorEntry
’sinclude
method. The Form Nav Extension portlet’s entry class’sinclude
method passes to the request the current settings that were saved as portlet preferences. It doesn’t use a template language and instead callsBaseJSPFormNavigatorEntry
’sinclude
method.@Override public void include(HttpServletRequest request, HttpServletResponse response) throws IOException { ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(WebKeys.THEME_DISPLAY); PortletPreferences companyPortletPreferences = PrefsPropsUtil.getPreferences(themeDisplay.getCompanyId(), true); boolean companyMyAppFeatureEnabled = PrefsParamUtil.getBoolean( companyPortletPreferences, request, "myAppFeatureEnabled", true); request.setAttribute( MyAppWebKeys.COMPANY_MY_APP_FEATURE_ENABLED, companyMyAppFeatureEnabled); super.include(request, response); }
-
setServletContext(ServletContext)
: In this method, you set the parent entry class’s servlet context. Then, using a@Reference
annotation, you unbind the servlet context from its current target and target it to your app’s OSGi bundle. First, add the@Reference
annotation. Next, unbind the servlet context by specifyingunbind = "-"
. Finally, to target the servlet context to your app’s OSGi bundle, specify as the target value the bundle’s symbolic name–it’s the value you specified forBundle-SymbolicName
in yourbnd.bnd
file.For example, here’s the
setServletContext(ServletContext)
method from the Form Nav Extension portlet’s entry class:@Override @Reference( target = "(osgi.web.symbolicname=com.liferay.docs.formnavextensionportlet)", unbind = "-" ) public void setServletContext(ServletContext servletContext) { super.setServletContext(servletContext); }
The above method calls its parent’s setServletContext(ServletContext)
method.
But look at what its @Reference
annotation does. It unbinds the servlet
context from its current binding and instead targets it to its own bundle–it
targets the bundle symbolically named
com.liferay.docs.formnavextensionportlet
. That is the exact symbolic name
defined by Bundle-SymbolicName: com.liferay.docs.formnavextensionportlet
in the Form Nav Extension portlet’s
bnd.bnd
file.
You’ve learned what’s required to create a section class. You declared your
class to extend the BaseJSPFormNavigatorEntry
class and implement the
FormNavigatorEntry
interface, both with respect to your Form Navigator’s form
model bean class. Using annotations, you registered your entry class as an OSGi
service. Then you implemented all the entry methods to relate your entry to a
Form Navigator, category, and JSP, populate your entry’s request object, and
target the servlet context to your bundle.
If you’re curious about what a working entry implementation looks like, check out the example entry class next.
Example Form Navigator Entry Class
Inspecting an example implementation can help you work out details in your
implementation. Here’s the Form Nav Extension portlet’s entry class
MyAppCompanySettingsFormNavigatorEntry
:
package com.liferay.docs.formnavextensionportlet;
import java.io.IOException;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.portlet.PortletPreferences;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.liferay.portal.kernel.servlet.taglib.ui.BaseJSPFormNavigatorEntry;
import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorConstants;
import com.liferay.portal.kernel.servlet.taglib.ui.FormNavigatorEntry;
import com.liferay.portal.kernel.util.PrefsParamUtil;
import com.liferay.portal.kernel.util.PrefsPropsUtil;
import com.liferay.portal.kernel.util.ResourceBundleUtil;
import com.liferay.portal.kernel.util.WebKeys;
import com.liferay.portal.model.Company;
import com.liferay.portal.theme.ThemeDisplay;
@Component(
immediate = true,
property = {
"form.navigator.entry.order:Integer=71"
},
service = FormNavigatorEntry.class
)
public class MyAppCompanySettingsFormNavigatorEntry
extends BaseJSPFormNavigatorEntry<Company>
implements FormNavigatorEntry<Company> {
@Override
public String getCategoryKey() {
return FormNavigatorConstants.CATEGORY_KEY_COMPANY_SETTINGS_MISCELLANEOUS;
}
@Override
public String getFormNavigatorId() {
return FormNavigatorConstants.FORM_NAVIGATOR_ID_COMPANY_SETTINGS;
}
@Override
protected String getJspPath() {
return "/portal_settings/my_app.jsp";
}
@Override
public String getKey() {
return "my-app";
}
@Override
public String getLabel(Locale locale) {
ResourceBundle resourceBundle = ResourceBundleUtil.getBundle(
"content.Language", locale, getClass());
return resourceBundle.getString("my-app");
}
@Override
public void include(HttpServletRequest request, HttpServletResponse response)
throws IOException {
ThemeDisplay themeDisplay =
(ThemeDisplay)request.getAttribute(WebKeys.THEME_DISPLAY);
PortletPreferences companyPortletPreferences =
PrefsPropsUtil.getPreferences(themeDisplay.getCompanyId(), true);
boolean companyMyAppFeatureEnabled =
PrefsParamUtil.getBoolean(
companyPortletPreferences, request, "myAppFeatureEnabled",
true);
request.setAttribute(
MyAppWebKeys.COMPANY_MY_APP_FEATURE_ENABLED,
companyMyAppFeatureEnabled);
super.include(request, response);
}
@Override
@Reference(
target = "(osgi.web.symbolicname=com.liferay.docs.formnavextensionportlet)",
unbind = "-"
)
public void setServletContext(ServletContext servletContext) {
super.setServletContext(servletContext);
}
}
The above class is declared an OSGi component that provides a
FormNavigatorEntry.class
service. Since the entry adds a JSP to Portal
Settings, the class extends BaseJSPFormNavigatorEntry
and implements
FormNavigatorEntry
on the Company
model bean class. The class specifies that
the entry belongs to the Miscellaneous portal settings category, by returning
navigator key FormNavigatorConstants.FORM_NAVIGATOR_ID_COMPANY_SETTINGS
from
method getFormNavigatorId
and category key
FormNavigatorConstants.CATEGORY_KEY_COMPANY_SETTINGS_MISCELLANEOUS
from method
getCategoryKey
. The entry’s method getKey
returns my-app as its own key
and method getJspPath
maps the class to the entry’s JSP by returning its JSP
file path /portal_settings/my_app.jsp
.
The entry’s include
method retrieves a boolean portlet preference variable
myAppFeatureEnabled
that specifies whether My App’s feature is enabled for the
portal. It then sets the preference’s value as an attribute on the request. The
Form Nav Portlet’s language keys for the entry’s name, input screen title, and
input label are defined in its src/main/content/Language.properties
file. The
method getLabel(Locale)
uses language key my-app
to return the entry’s
localized label. In summary, the MyAppCompanySettingsFormNavigatorEntry
class
meets all of the Form Navigation framework’s section entry requirements.
There you have it! You learned all the parts of the Form Navigator framework and
worked through all the steps to implement new categories and sections. To recap,
you prepared your project’s bnd.bnd
file to support form navigator extension,
created JSPs for your new form sections, identified the targeted form navigator
and categories, created new categories, and created new entry implementations.
You did it all! You now know what it takes to extend Liferay Form Navigators.