Richard Bolkey – Blog

Icon

Thoughts of a plain old java developer.

Creating a news feed in Tapestry 5

News feeds are frequently encountered in today’s web interfaces. Using Ajax, news and social feeds are continually monitored with new items being retrieved for display. In Apache Tapestry 5, the lingua-franca of performing these types of Ajax updates is the Zone component. The Zone component marks (or contains) a region of your Tapestry template that you wish to replace on an Ajax event; however, with news feeds you typically want to add new markup to a page without replacing the existing markup.

Currently, Tapestry does not behave this way. But with the ease of a custom mixin (a way Tapestry can add or alter the behavior of a component), we can easily change a Zone to insert the new markup.  And with the addition of another simple mixin, we can continue to poll for new markup to insert!

This tutorial requires previous knowledge of mixins and ajax in Tapestry, as well as some familiarity with Prototype, a JavaScript framework used by Tapestry.

Inserting markup instead of replacing

First, we will alter the behavior of a zone to insert instead of replace markup.  This does not require much code. All you need is to add a mixin to the zone component that alters the baked in Tapestry behavior.  In this example, we will call this mixin the ZoneInserter.

/**
 * Modifies how a the content of a Tapestry Zone is updated.  Allows content to 
 * be inserted instead of just updating an element.
 */
@IncludeJavaScriptLibrary("ZoneInserter.js")
public class ZoneInserter {
 
    /**
     * Accepted insertion points are:
     * <ul>
     * <li>before (as element's previous sibling)</li>
     * <li>after (as element's next sibling)</li>
     * <li>top (as element's first child)</li>
     * <li>bottom (as element's last child) [<b>default</b>]</li>
     * </ul>
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL, value = "bottom")
    private String insertion;
 
    @InjectContainer
    private Zone zone;
 
    @Inject
    private ComponentResources resources;
 
    @Inject
    private RenderSupport renderSupport;
 
    void afterRender() {
 
        final String id = zone.getClientId();
 
        final JSONObject config = new JSONObject();
 
        if (resources.isBound("insertion")) config.put("insertion", insertion);
 
        renderSupport.addInit("zoneInserter", new JSONArray(id, config));
    }
}

This is a very simple mixin that passes the client id of the zone and a single configuration parameter (“insertion”) to a JavaScript initialization script. The mixin needs to run after the zone renders so that the zones client id is known. The necessary JavaScript included by the mixin is a little more interesting:

Tapestry.Initializer.zoneInserter = function(zoneId, options)
{
    // Need to access the zone after the zone manager is configured during Tapestry.onDomLoaded
    Event.observe(window, "load", function() {
 
        var zoneObject = Tapestry.findZoneManagerForZone(zoneId);
 
        if (!zoneObject) return;
 
        zoneObject.show = function(content)
        {
            var key = (options.insertion || 'bottom');
 
            var insertion = new Hash();
            insertion.set(key, content);
 
            this.updateElement.insert(insertion.toJSON().evalJSON());
 
            var func = this.element.visible() ? this.updateFunc : this.showFunc;
 
            func.call(this, this.element);
 
            this.element.fire(Tapestry.ZONE_UPDATED_EVENT);
        }
    });
};

As we have said, by default Tapestry replaces the content of a zone with new markup. This script alters that default behavior of a given zone. The key is in changing the default show method of the zone object in JavaScript. Tapestry’s default show method uses Prototype’s update method on the zone’s update element, and instead, we want to use the insert method here to insert content around that element (before, after, at the top of, or at the bottom of).

Now that we can insert new markup before a zone element instead of always replacing the element’s contents, we can turn to another mixin that will poll for new markup to insert!

Polling for new markup to insert

Here, we want to trigger a Tapestry event on a periodic interval in order to fetch new markup for our page. Again, we turn to mixins to solve this problem. We will call this mixin “PeriodicUpdate”. It’s still not a lot of code, but requires a few more lines to get working than the ZoneInserter .

/**
 * Enables a zone to be periodically refreshed with the response from the given event.
 */
@IncludeJavaScriptLibrary("PeriodicUpdate.js")
public class PeriodicUpdate {
 
    /**
     * The name of the event to call to update the zone.
     */
    @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL)
    private String event;
 
    /**
     * The context for the triggered event. The clientId of the containing zone is always 
     * added as the final context item.
     */
    @Parameter("defaultContext")
    private Object[] context;
 
    /**
     * How long, in seconds, to wait between the end of one request and the beginning of the next.
     */
    @Parameter(defaultPrefix = BindingConstants.LITERAL)
    private int period;
 
    @InjectContainer
    private Zone zone;
 
    @Inject
    private ComponentResources resources;
 
    @Inject
    private RenderSupport renderSupport;
 
    public Object[] getDefaultContext() {
        return new Object[0];
    }
 
    void afterRender() {
 
        final String id = zone.getClientId();
 
        final List<Object> context = Lists.newArrayList(Arrays.asList(this.context));
        context.add(zone.getClientId());
 
        final Link link = resources.createEventLink(event, context.toArray(new Object[context.size()]));
 
        final JSONObject config = new JSONObject();
 
        if (resources.isBound("period")) config.put("period", period);
 
        // Let subclasses do more.
        configure(config);
 
        renderSupport.addInit("periodicupdater", new JSONArray(id, link.toAbsoluteURI(), config));
    }
}

This mixin builds a link to a Tapestry event, and passes that link along with the client ID of a zone and the period of time between calling the link as parameters to a JavaScript initialization script, which we’ll look at now.

Tapestry.PeriodicUpdater = Class.create({
    initialize: function(element, url, options) {
        this.options = options;
 
        this.period = (this.options.period || 2);
 
        this.element = element;
        this.url = url;
 
        this.start();
    },
 
    start: function() {
        this.onUpdate = this.updateComplete.bind(this);
        this.timer = this.onTimerEvent.bind(this).delay(this.period);
    },
 
    stop: function() {
        this.onUpdate = undefined;
        clearTimeout(this.timer);
    },
 
    updateComplete: function() {
        this.timer = this.onTimerEvent.bind(this).delay(this.period);
    },
 
    onTimerEvent: function() {
        var zoneObject = Tapestry.findZoneManagerForZone(this.element);
 
        if (!zoneObject) return;
 
        zoneObject.updateFromURL(this.url);
 
        (this.onUpdate || Prototype.emptyFunction).apply(this, arguments);
    }
});
 
Tapestry.Initializer.periodicupdater = function(elementId, url, options)
{
    $T(elementId).periodicupdater = new Tapestry.PeriodicUpdater(elementId, url, options);
};

This script creates a new Class in prototype that encapsulates the events needed to poll for updates, and adds it to the zone element. The class provides actions to start, stop, and perform the polling (timer), as well as an event for when an update has run (onUpdate). The polling occurs by the onUpdate event calling the timer method on a delay, after which the timer performs the update and triggers back to onUpdate.

Tying things together

We can tie these mixins together in a Tapestry template on one zone component like so:

...
<t:zone mixins="ZoneInserter, PeriodicUpdate" insertion="top" period="10" event="updates"/>
...

This line in a Tapestry template will create a zone that will have any new markup returned by a custom updates event inserted at the top of the zone element every 10 seconds. Pretty nice!

Conclusion

By using these two mixins on a Zone component in Tapestry, you can build a news feed. the ZoneInserter mixin to insert new markup in a page, and the PeriodicUpdate mixin to monitor for new markup to insert. I hope you enjoyed this example!

Category: Programming

Tagged: ,

5 Responses

  1. Jim says:

    Hi have you tried this with T5.2.0? I was trying it with


    public Object onUpdates(){
    return request.isXHR() ? new java.util.Date().toGMTString() : null; // force refresh of whole
    }

    to test, which I though would just append / prepend the date to the zone but was getting a problem:


    Ajax failure: Status 500 for //index.layout.latestnewszone:updates/latestNewsZone: Unable to resolve '14 Oct 2010 16:51:32 GMT' to a known page name.

    Any idea of whether this leans towards a T5.2.0 incompatability or is an erro in my usage? Thanks.

    • Jim says:

      Nevermind – I was being a bit silly here – returned a Block from my onUpdates method that references the new content as a property as all is well – thanks for outlining the approach – I can see this being very useful.

  2. Josh Canfield says:

    Hey Richard,

    Have you submitted this to be included in Tapestry core?

    Josh

Leave a Reply

 

Pages

Follow Me on Twitter!

Categories