Search

Dark theme | Light theme

January 19, 2016

Ratpacked: Customising Renderers With Decorators

When we use the Context.render method Ratpack's rendering mechanism kicks in. The type of the argument we pass to the render method is used to look up the correct renderer. The renderer implements the Renderer interface and provides the real output. We can add functionality that can work with the object of the Renderer implementation before the actual output is created. We do this by adding a class or object to the registry that implements the RenderableDecorator interface. The interface has a method decorate that accepts the Context and object that needs to be rendered. The code is invoked after the Context.render method, but before the Renderer.render method. This is especially useful when we use template renderers with a view model and with a RenderableDecorator implementation we can augment the view model with some general attributes.

Suppose we have a Ratpack application that uses the Groovy text template engine provided by the TextTemplateModule. The module adds a Renderer for TextTemplate objects. Let's write a RenderableDecorator implementation for the TextTemplate, where we add an extra attribute createdOn to the view model:

// File: src/main/groovy/com/mrhaki/ratpack/CreatedOnRendererDecorator.groovy
package com.mrhaki.ratpack

import ratpack.exec.Promise
import ratpack.groovy.template.TextTemplate
import ratpack.handling.Context
import ratpack.render.RenderableDecorator

import java.time.Clock
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

/**
 * Add extra attribute to view model for all TextTemplate renderers.
 */
class CreatedOnRendererDecorator implements RenderableDecorator<TextTemplate> {

    /**
     * Apply this decorator for TextTemplate renderers.
     * 
     * @return TextTemplate class.
     */
    @Override
    Class<TextTemplate> getType() {
        return TextTemplate
    }

    /**
     * Add an extra attribute createdOn to the view model with the current
     * date and time.
     * 
     * @param context Context to get Clock instance for this Ratpack application from.
     * @param template Template with view model to extend.
     * @return Promise with new TextTemplate instance with the extended view model.
     */
    @Override
    Promise<TextTemplate> decorate(final Context context, final TextTemplate template) {
        final footerModel = [createdOn: createdOn(context)]

        return Promise.value(
                new TextTemplate(
                        template.model + footerModel, 
                        template.id, 
                        template.type))
    }

    /**
     * Create formatted date/time String based on
     * the Clock available on the Ratpack registry.
     * 
     * @param context Context to get Clock instance from.
     * @return Formatted date/time String.
     */
    private String createdOn(final Context context) {
        final Clock clock = context.get(Clock)
        final LocalDateTime now = LocalDateTime.now(clock)
        final DateTimeFormatter formatter =
                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return formatter.format(now)
    }
    
}

To use this decorator we must add it to the registry in our Ratpack application. We also use the RenderableDecorator.of method to create a decorator for our application directory in our Ratpack Groovy DSL:

// File: src/ratpack/Ratpack.groovy
import com.mrhaki.ratpack.CreatedOnRendererDecorator
import ratpack.groovy.template.TextTemplate
import ratpack.groovy.template.TextTemplateModule
import ratpack.render.RenderableDecorator
import ratpack.util.RatpackVersion

import static ratpack.groovy.Groovy.groovyTemplate
import static ratpack.groovy.Groovy.ratpack

ratpack {
    bindings {
        // Use Groovy's simple text template engine.
        module TextTemplateModule
        
        // Use class that implements the
        // RenderableDecorator interface.
        bind CreatedOnRendererDecorator
        
        // Create RenderableDecorator instance using the 
        // RenderableDecorator.of method.
        // Here we add the model attribute createdWith.
        bindInstance RenderableDecorator.of(TextTemplate) { context, template ->
            new TextTemplate(
                    template.model + [createdWith: "Ratpack ${RatpackVersion.version}"],
                    template.id,
                    template.type)
        }
    }

    handlers {
        get {
            // Set model attributes used on the template.
            final model = 
                    [title: 'Ratpack Application',
                     welcomeMessage: 'Welcome to Ratpack']
            
            render groovyTemplate(model, 'index.html')
        }

        files { dir "public" }
    }
}

To complete the example we create the following index.html file in the directory src/ratpack/templates:

<!doctype html>
<html class="no-js" lang="">
<head>
    <meta charset="utf-8">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <title>${model.title}</title>
</head>
<body>

<header>
    <h1>${model.welcomeMessage}</h1>
</header>

<section>
    <p>Some sample text for the template.</p>
</section>

<footer>
    <p>Page is created with ${model.createdWith} at ${model.createdOn}.</p>
</footer>

</body>
</html>

Written with Ratpack 1.1.1.