Building widgets

This introduction gives you the basics on building widgets — views for displaying content on customizable pages.

People use widgets on the widgetized pages: a community home page, space overview pages, and project overview pages. Widgets provide views of content in the community or on the web, or simply display a message. You can write your own widgets to give views of other content, or even act as enhanced versions of the included widgets.

Widget basics

Your widget can be very basic — such as a single class. At the same time, widgets can also include other files to support your design goals.

A basic widget could include:

  • A plugin.xml file for configuration, as with other plugins.
  • A Java class for logic (required). Your basic widget (with just a Java class) could return the HTML to render for the user. The widget class needs to implement the Widget interface, but you may likely find it easier to do this by extending the BaseWidget class.
  • FTL files for presenting data (optional). You can use an FTL file to shape the data you present to the user, handle widget resizing, and so on.
  • A properties file to provide strings used for widget property names and descriptions (optional).
  • i18n keys (optional) to internationalize your widget's UI text.

Creating plugin.xml for configuration

Every plugin has plugin.xml. This file tells the application what's in the plugin — what features you're extending — and where to find code and other supporting files your plugin needs. Not necessarily all the required files are described in plugin.xml, as you can see in the next section.

Here's an example. Here, the <widget> element gives the name of your plugin class.

<plugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://www.jivesoftware.com/schemas/clearspace/2_5/plugin.xsd">
    <name>Hello World Widget</name>
    <description>Simple example of a widget that says hello.</description>
    <author>Jive Software</author>
    <version>1.0.0</version>
    <minServerVersion>2.5.0</minServerVersion>
 
    <!-- Defines a custom widget for this plugin -->
    <widget class="com.jivesoftware.clearspace.plugin.example.widget.HelloWorldWidget" />
</plugin>

The <plugin> element's children include information about where the plugin is coming from, its version (in case you revise it for upgrade later), and so on. The <minServerVersion> element specifies the minimum application version that the plugin will run on (it won't load on earlier versions). There's a corresponding maxServerVersion element also. The code here tells the application that the plugin includes a widget, and which widget class to load.

Creating Java classes for widget logic

You create a Java class that provides logic for the widget. Your widget class implements the com.jivesoftware.community.widget.Widget interface, usually by extending the com.jivesoftware.community.widget.BaseWidget class.

As you implement the class, you do the following:

  • Implement three methods that called to display widget essentials.
  • Add support for new properties with accessors and the @PropertyNames annotation.
  • Call helper methods as needed to process an FTL file, support localization, and so on.

Required methods

When you extend the BaseWidget class, you need to implement three methods from the Widget interface that aren't implemented in BaseWidget:

  • String getTitle(WidgetContext): This corresponds to your widget's default title. Implement this to return the title that should appear at the top of your widget.
  • String getDescription(WidgetContext): Implement this to return the description that should appear when the user hovers over your widget in the "customize" mode list of widgets.
  • String render(WidgetContext, ContainerSize): Implement this to return the HTML your widget will display when it's published.

You can see that each of these methods takes a WidgetContext instance as a parameter. The WidgetContext class includes methods through which you can get information about the context in which the widget instance is executing — its containing community or space, its current user, request and response objects, and so on. In some cases, you can be passing the instance to other methods.

Widget types

You use the @WidgetTypeMarker annotation to specify which of the display contexts your widget is allowed in. Deciding which contexts to allow is an important part of designing your widget. For example, you might decide that a widget that takes a very "personal preference" set of property values (such as the Tag widget, which displays content associated with a particular tag) isn't useful in high-level contexts such as the instance or space home pages (where the broader set of people viewing might want views on a large variety of tags).

To indicate which pages your widget supports use on, annotate your widget class declaration with the @WidgetTypeMarker annotation. This annotation supports values from the WidgetType enumeration. Here's an example that includes those values:

@WidgetTypeMarker({WidgetType.HOMEPAGE, WidgetType.PERSONALIZEDHOMEPAGE,
    WidgetType.COMMUNITY, WidgetType.PROJECT})
@PropertyNames("myProperty")
public class MyWidget extends BaseWidget {
    // Implementation code omitted.
}

Exposing widget design-time properties

Each widget exposes properties that the page designer can use to customize how and what the widget displays. For the two default properties — title and description — you need only implement getters to return the values that should be displayed. For properties you add, you do the following:

  • Implement accessors through which the application can get and set property values.
  • Add a class-level @PropertyNames annotation that lists each of the property names.
  • Include a .properties file that lists the values that should be used in the widget's UI where the user sets properties (there's a .properties file example below.)

In the following example, the title ("Hello World Widget") and description ("Displays a 'Hello World' message.") are properties implemented through the required accessors getTitle and getDescription.

Another property, greetUser, is implemented in the widget class through its own annotation and accessors; the radio buttons here are automatically provided because the accessors get and set a boolean value. The greetUser property also provides title and description values through a properties file included in the widget.



Here's the code:

// To hold the value of a custom property.
private boolean greetUser = false;
/**
 * Called to get the description that should be displayed for
 * the widget when the user hovers over it in the "customize" mode list of
 * widgets.
 *
 * @param widgetContext
 *            Context in which the widget instance is executing.
 */
public String getDescription(WidgetContext widgetContext) {
    return "Displays a 'Hello World' message.";
}
/**
 * Called to get the widget's default title. The user will be
 * able to change this. If they do, their new title will be set with a call
 * to the final method BaseWidget.setCustomTitle.
 *
 * @param widgetContext
 *            Context in which the widget instance is executing.
 */
public String getTitle(WidgetContext widgetContext) {
    return "Hello World Widget";
}
/**
 * Called to get the value for the greetUser property.
 *
 * @return true if the user should be greeted; false to greet the world.
 */
public boolean getGreetUser() {
    return greetUser;
}
 
/**
 * Called to set the value for the greetUser property.
 *
 * @param greetUser
 *            true to greet the user; false to greet the world.
 */
public void setGreetUser(boolean greetUser) {
    this.greetUser = greetUser;
}

Setting FreeMarker properties

If your FTL file uses property variables (for FreeMarker properties, not widget properties), you can add values to the FreeMarker context from within your widget class. When your template is applied, the values you set will be used in generating the output.

Note that the application doesn't add the widget class itself to the FreeMarker context in the way it does with action plugins. This means that a ${x} property in the FreeMarker template won't result automatically in a call to a getX() method in your widget class.

Instead, you should override the BaseWidget.loadProperties(WidgetContext, ContainerSize) method to add in any variables for FreeMarker properties. Your implementation should call the superclass's implementation to retrieve the properties Map, then set additional properties by putting them into the map. That is, you'd add the x variable to the FreeMarker context using your loadProperties method implementation.

Here's an example:

public class MyWidget extends BaseWidget {
    // Declare a property variable and setter for injection by Spring.
    private CommunityManager communityManager;
    public void setCommunityManager(CommunityManager cm) {
        this.communityManager = cm;
    }
    protected Map<String, Object> loadProperties(WidgetContext widgetContext,
        ContainerSize size)
    {
        Map<String, Object> properties = super.loadProperties(widgetContext, size);
        // Implementation code omitted.
        properties.put("rootCommunityID", communityManager.getRootCommunity().getID());
        return properties;
    }
}

Helper methods

BaseWidget provides helper methods you might find useful. Here are a few:

  • String applyFreemarkerTemplate(WidgetContext, Widget.ContainerSize, String): Call this to process an FTL file (whose path is given as the String parameter) and get the HTML result.
  • String getLocalizedString(String, WidgetContext): Call this to retrieve the string corresponding to the key i18n key you pass as the first parameter. For more information, see the section of this topic on using i18n keys.
  • Map loadProperties(WidgetContext, Widget.ContainerSize): Override this to add property values to the FreeMarker context. For more information, see the widget class example below.

Implementing context-specific behavior

Because you can create a widget that exists in multiple parts of the application (the home page, a space, a project), you'll sometimes want to change the behavior of your widget based on where the widget is being rendered. You can determine the render context of your widget by checking the type of the WidgetContext class that you're given in the loadProperties method. Here's some example code that shows how you can determine what context the widget is in:

public 
class MyWidget extends BaseWidget {
    protected Map<String, Object> 
loadProperties(WidgetContext widgetContext, ContainerSize size) {
        Map<String, Object> properties = super.loadProperties(widgetContext, size);
        if (widgetContext.getWidgetType() == WidgetType.COMMUNITY) {
            CommunityWidgetContext cwc = (CommunityWidgetContext)widgetContext;
            // Do something specific for the community
        }
        else if (widgetContext.getWidgetType() == WidgetType.HOMEPAGE) {
            HomepageWidgetContext hwc = (HomepageWidgetContext)widgetContext;
            // Do something specific for the homepage
        }
        else if (widgetContext.getWidgetType() == WidgetType.PERSONALIZEDHOMEPAGE) {
            PersonalizedHomepageWidgetContext phwc =
                (PersonalizedHomepageWidgetContext)widgetContext;
            // Do something specific for the personalized homepage 
        }
        else if (widgetContext.getWidgetType() == WidgetType.PROJECT) {
            ProjectWidgetContext wwc = (ProjectWidgetContext)widgetContext;
            // Do something specific for the project
        }
 
        properties.put("rootCommunityID", communityManager.getRootCommunity().getID());
        return properties;
    }
}

Widget class example

Here's the full code of a widget class.

package com.jivesoftware.clearspace.plugin.example.widget;
import com.jivesoftware.community.annotations.PropertyNames;
import com.jivesoftware.community.widget.*;
import java.util.Map;
// The WidgetTypeMarker annotation tells which kinds of pages this
// widget can be used on. This widget will show up in the widget list
// on the home page (the one seen by everyone) and a space overview
// page. Other supported values include WidgetType.PERSONALIZEDHOMEPAGE and WidgetType.PROJECT.
@WidgetTypeMarker({WidgetType.HOMEPAGE, WidgetType.COMMUNITY})
@PropertyNames("greetUser")
public class HelloWorldWidget extends BaseWidget {
    // FreeMarker template for rendering preview and published widget.
    private static final String FREEMARKER_FILE = "/plugins/example/resources/hello-world.ftl";
    // To hold the value of a custom property.
    private boolean greetUser = false;
    /**
     * Called to get the description that should be displayed for
     * the widget when the user hovers over it in the "customize" mode list of
     * widgets.
     *
     * @param widgetContext
     *            Context in which the widget instance is executing.
     */
    public String getDescription(WidgetContext widgetContext) {
        return "Displays a 'Hello World' message.";
    }
    /**
     * Called to get the widget's default title. The user will be
     * able to change this. If they do, their new title will be set with a call
     * to the final method BaseWidget.setCustomTitle.
     *
     * @param widgetContext
     *            Context in which the widget instance is executing.
     */
    public String getTitle(WidgetContext widgetContext) {
        return "Hello World Widget";
    }
    /**
     * Called to get the value for the greetUser property.
     *
     * @return true if the user should be greeted; false to greet the world.
     */
    public boolean getGreetUser() {
        return greetUser;
    }
    /**
     * Called to set the value for the greetUser property.
     *
     * @param greetUser
     *            true to greet the user; false to greet the world.
     */
    public void setGreetUser(boolean greetUser) {
        this.greetUser = greetUser;
    }
    /**
     * Called to get the HTML used to display the widget when it's
     * previewed or published.
     *
     * @param widgetContext
     *            Context in which the widget instance is executing.
     * @param containerSize
     *            An enum constant representing the size of the widget
     *            instance's current container: LARGE or SMALL.
     */
    public String render(WidgetContext widgetContext,
            ContainerSize containerSize) {
        // Process the included FTL file to render the HTML for display.
        return applyFreemarkerTemplate(widgetContext, containerSize,
                FREEMARKER_FILE);
    }
    /**
     * Called to get properties for use in your FTL file. These
     * will be added to the FreeMarker context.
     *
     * @param widgetContext
     *            Context in which the widget instance is executing.
     * @param containerSize
     *            An enum constant representing the size of the widget
     *            instance's current container: LARGE or SMALL.
     * @return A map of the properties and their values.
     */
    protected Map<String, Object> loadProperties(WidgetContext widgetContext,
            ContainerSize containerSize) {
        // First load existing properties.
        Map<String, Object> properties = super.loadProperties(widgetContext,
                containerSize);
        // Get the name of the community this instance is in, then add it as a
        // property.
        String communityName = ((CommunityWidgetContext)widgetContext).getCommunity().getName();
        String userName = widgetContext.getUser().getName();
        properties.put("communityName", communityName);
        properties.put("userName", userName);
        properties.put("greetUser", greetUser);
        return properties;
    }
}

Writing Properties files

You need to create a .properties file that provides the strings used in the design-time user interface.

In the widget JAR, this file is located at <jar_root>/classes/beans/HelloWorldWidget.properties.

# Resource bundle for Hello World Widget
# Author of widget
author=Me
# Version of the widget
version=1.0
# Properties - Display name and description
greetUser.displayName=Greet the User
greetUser.shortDescription=Greets the user by name; otherwise, greets the World.

Note that the greetUser part of the keys matches the name of the property as given in the @PropertyNames annotation.

Writing FTL files

You use a FreeMarker template (FTL) file to shape the data you're presenting in your published widget.

The FTL file must be on the application classpath; that can be accomplished by including it in your widget JAR file, then loading it at the relative location using the applyFreemarkerTemplate helper method. For more information, see the widget class code example.

Handling widget resizing

When someone clicks the Customize link to begin designing a layout, the design space includes a list of installed widgets and an area for arranging widgets. This area can be divided into multiple columns; the user chooses which configuration they want. In a multi-column layout, when someone drags your widget between large and small columns, the widget is resized from a large size to a small size.

Here's a view with the same widget on both sides of a two-column layout:



Each widget is responsible for its display characteristics, including adjusting within its frame when its size changes. In your FTL file, you can handle each of these cases by providing a large rendering and a small one. You do that by testing for the value of the enum com.jivesoftware.community.widget.Widget.ContainerSize; its two values are LARGE and SMALL.

<style type="text/css">
    .jive-widget .jive-widget-body p.gobig {
        font: 16px verdana, helvetica, sans-serif;
    }
    .jive-widget .jive-widget-body p.getsmall {
        font: 10px verdana, helvetica, sans-serif;
    }
</style>
<#-- Use the widget's greetUser property to define the greeting style. -->
<#if greetUser>
        <#assign greeting = "Hello " + userName + "!">
<#else>
        <#assign greeting = "Hello World!">
</#if>
<#-- Render for display in a small area. -->
<#if containerSize == enums['com.jivesoftware.community.widget.Widget$ContainerSize'].SMALL>
    <p class="getsmall">${greeting} Welcome to the ${communityName} community!</p>
<#-- Render for display in a large area. -->
<#else>
    <p class="gobig">${greeting} Welcome to the ${communityName} community!</p>
</#if>