Flash-Scoped Messages Helper

  • Tags : flash i18n
  • Latest : 0.9.6
  • Last Updated: 28 September 2013
  • Grails version : 1.0 > *
4 votes
Dependency :
compile ":flash-helper:0.9.6"

Documentation Source Issues

Summary

Simplifies and standardizes the process of adding/reading messages in the flash scope, particularly i18n messages that must be retrieved from the messages.properties files. It provides the following features: Automatically resolves i18n messages when message keys are stored in flash scope Optionally enforces the use of a limited number of flash keys (e.g. info, error, warning) Supports adding multiple messages to the same flash key Allows a Locale and default message argument to be provided when resolving i18n messages Provides a taglib that can be used to retrieve messages added to the flash

Installation

grails install-plugin flash-helper

Description

Overview

This plugin simplifies and standardizes the process of adding/reading messages in the flash scope, particularly i18n messages that must be retrieved from the messages*.properties files. It provides the following features:

  • Automatically resolves i18n messages when message keys are stored in flash scope
  • Optionally enforces the use of a limited set of flash keys (e.g. info, error, warning)
  • Supports adding multiple messages to the same flash key
  • Allows a Locale and default message argument to be provided when resolving i18n messages
  • Provides a taglib that can be used to retrieve messages added to the flash
  • Ensures that objects stored in flash scope (e.g. information messages) are only displayed once

The Problem

A common pattern for usage of the flash scope is to store messages using a limited set of keys, e.g. "info", "error", "warning". Messages placed into the flash under one of these keys may be displayed using a GSP template such as

<g:if test="${flash.info}">
    <div class="info">${flash.info}</div>
</g:if>

or if this key refers to a list of messages

<g:if test="${flash.info}">
    <g:each in="${flash.info}" var="msg">
        <div class="info">${msg}</div>
    </g:each>
</g:if>

If the message must be retrieved from the messages*.properties files, then the message key, message arguments, and default message may be stored in the flash scope using

flash.key = "message"
flash.args  = ["arg1", "arg2"]
flash.default = "default message"

The message itself may be retrieved in a GSP using the message tag

<g:message code="${flash.key}" args="${flash.args}" default="${flash.default}"/>

This approach has a number of shortcomings:

  • Storing/retrieving i18n messages should be simpler
  • Mistyping the key when storing/retrieving the message would (silently) cause a message display failure
  • No support for storing multiple messages under the same key

The Solution

Storing Flash Messages

This plugin adds an object flashHelper to every controller that may be used to:

Store multiple messages under the same key

flashHelper.info "Some Message"
flashHelper.info "Some Message2"

This will store 2 messages in the flash under the key "info"

Easy storage/retrieval of i18n messages

After a method call such as

flashHelper.info "key2"

A message will be stored in the flash scope under the key "info". The plugin automatically looks up the message with this key in the resource bundles, such that the resolved message is stored under the "info" key.

If a key is not found in the resource bundles, then it is assumed that the argument is a message (rather than a message key) and will be stored in the flash scope.

When retrieving parametrized messages from the resource bundles, additional Locale and default message arguments may be provided. If a default message is provided, the default message will be stored in flash if the key does not exist in the relevant messsages*.properties file. Refer to the "Complete Example" section below for details.

Easy storage/retrieval of i18n messages with arguments

Similarly, a i18n message with arguments may be stored in the flash scope with a method call such as

// Store a a single message that takes two arguments under 'info' key
flashHelper.info key1: ['arg1', 'arg2']

This will lookup a message with key "key1" in the resource bundles, replacing any {} placeholders with the arguments supplied and store the resolved messages themselves in the flash scope under the key "info".

If a key is not found in the resource bundles (and a default message is not provided), a NoSuchMessageException will be thrown.

When retrieving parametrized messages from the resource bundles, additional Locale and default message arguments may be provided. If a default message is provided, the default message will be stored in flash if the key does not exist in the relevant messsages*.properties file. Refer to the "Complete Example" section below for details.

Message Argument Resolution

Assume you have the following resources configured

not.found={0} not found
person.label=Person

and you want to store the message "Person not found" in the flash scope. You could do this by resolving the key person.label (e.g. using the messageSource Spring bean) and providing the result along with the key not.found to the flashHelper as described in the previous section. Fortunately, since version 0.6 this can be performed in a single step by providing true as the final argument, e.g.

flashHelper.info not.found: 'person.label', true

When this boolean is provided, an attempt is made to resolve all message arguments. If an argument cannot be resolved, the literal argument will be used instead, so you may provide a mixture of resolvable and unresolvable arguments (for messages that have more than a single placeholder).

Named Arguments API

When invoking the flashHelper through the default API with a large number of arguments, the readability of the code suffers. An example of this is when you supply a message code, arguments, a locale, a default message, and enable message argument resolution:

flashHelper.info 'code': ['arg1', 'arg2'], Locale.FRENCH, "default message", true

To improve the code readability, an alternative API may be used that supports named arguments

flashHelper.info(
        msgs: ['code': ['arg1', 'arg2']],
        locale: Locale.FRENCH, 
        default: "default message",
        resolveArgs: true,
        codeMustResolve: true)

This codeMustResolve argument specifies what should happen if a message code is not found in the resource bundle (and no default message is provided). If this argument is true and exception is thrown, if false the code itself will be used as the message.

(In the case of the standard API this behavior cannot be specified, instead the following rule is used: an exception is always thrown if message arguments are provided, and the key itself is always used if they are not)

Message Key Standardization

If you add a property such as:

// Value of this property may be either a string or list of strings
flashHelper.keys = ['message', 'error']

to Config.groovy the flashHelper object will throw a FlashKeyException if you try to use the flashHelper to store a message in the flash scope using any other key. If this property is not present, there is no restriction on the keys that may be used.

Regardless of whether or not this property is configured, any key may be used when bypassing flashHelper and accessing the flash object directly.

Reading Flash Messages

The flashHelper stores all messages added to a particular flash key within a list. If a message is added under the "info" key:

flashHelper.info "my message"

An attempt to display this message with the following GSP code:

${flash.info}

would actually render:

[my message]

As this is unlikely to be the desired result, the following GSP code should be used instead:

${flash.info.join("<br/>")}

If a message is stored under the "info" key, this will render

my message

If multiple message are stored under the "info" key, this will render

my message<br/>
my message2<br/>
my message3

Of course the <br/> passed to the join() method above, may be replaced by any other separator string. As an alternative to using the join() method to display messages added with the flashHelper one may instead use the tag library described in the following section.

Tag Library

The plugin provides 2 tags under the flashMsg namespace

flashMsg:msg

<flashMsg:msg key="info" sep=" "/>
This produces the same result as using the join method (described above), i.e. it uses a separator string to join all messages stored under a particular flash key.
  • The key attribute is mandatory and indicates the flash key under which the message(s) is stored
  • The sep attribute is optional and indicates the separator string to use when joining the messages (if only a single message is stored under the key, this attribute is unused). If this argument is not provided the tag will use the value of the flashHelper.separator property in Config.groovy If neither the sep attribute nor the configuration property exist, a default separator <br/> will be used.
  • The keyNotFound attribute is described in the "Invalid Key Handling" section below

flashMsg:msgBody

Renders the tag body for each message stored under the provided flash key. For example, if the message foo is stored under the flash key myKey the following invocation of this tag

<flashMsg:msgBody key="myKey">
  I said ${it}<br/>
</flashMsg:msgBody>

will render

I said foo<br/>

If the messages foo and bar are stored under this key, the tag above will render

I said foo<br/>I said bar<br/>

In addition to the key attribute, this tag also supports the keyNotFound attribute described in the following section.

Invalid Key Handling

If the key value provided to either tag does not exist, the default behavior is to continue without warning or error. This default behavior can be changed either per-invocation, or application-wide. To customize the default behavior on a per-invocation basis, set the value of the keyNotFound attribute to one of the following:
  • warn - log a warning if an invalid key is provided
  • error - thow a FlashKeyException if an invalid key is provided
  • ignore - do nothing if an invalid key is provided (the default)
To customize the default behavior on an application-wide basis, add the following property to Config.groovy
// Set the property below to "error", "warn", or "ignore"
flashHelper.keyNotFound = 'error'

The tag-level setting of this property overrides the application-wide setting if the two are in conflict.

Flash Object Removal

Both tags provide an optional remove attribute which defaults to false. If set to true, an object is removed from flash scope after it is displayed with either tag. This is useful when an object is added to the flash scope followed by an invocation of the controller's render method (rather than redirect) In the normal course of events, the object would remain in flash scope for the next request, which could cause it to be displayed twice.

Setting the remove attribute to true ensures that an object in flash scope is only displayed once even when the controller does not perform a redirect

Examples

Controller Code

These examples are taken from FlashHelperTestControllerTests The test classes are an excellent source of information about how to use the flashHelper object and the tag library.

// Store literal message under "info" key
flashHelper.info "this is a message"

// Store message under "info" that must be retrieved from resource bundles flashHelper.info 'key1'

// Store message under "info" that must be retrieved from messages_fr.properties flashHelper.info 'key1', Locale.FRENCH

// Store message under "info" that must be retrieved from resource bundles. // Provide a default message in case the key is not found flashHelper.info 'key1', 'default'

// Store message under "info" that must be retrieved from messages_fr.properties // Provide a default message in case key is not found. // The order of the default message and Locale arguments may be transposed flashHelper.info 'key1', 'default', Locale.FRENCH

// Store parametrized messages under "info" that must be retrieved from resource bundles flashHelper.info key4: ['arg1', 'arg2']

// Store parametrized messages under "info" that must be retrieved from resource bundles // Provide default messages in case keys are not found flashHelper.info key4: ['arg1', 'arg2'], "default"

// Store parametrized messages under "info" that must be retrieved from messages_fr.properties flashHelper.info key4: ['arg1', 'arg2'], Locale.FRENCH

// Store parametrized messages under "info" that must be retrieved from messages_fr.properties // Provide default messages in case keys are not found flashHelper.info key4: ['arg1', 'arg2'], Locale.FRENCH, "default"

// Argument resolution - lookup 'resolvableArg' in the resource bundle and use the result as an // argument for the message with key 'key4' flashHelper.info key4: 'resolvableArg', true

// Use the named arguments API flashHelper.info(msgs: "literal message") flashHelper.info(msgs: ['key': 'arg'], locale: Locale.FRENCH) flashHelper.info(msgs: ['key': ['arg1', 'arg2']], locale: Locale.FRENCH)

Minor Features

Clear Flash

flashHelper.clear()

will remove all messages in the flash including any added by accessing the flash object directly.

Method Chaining

All the methods of flashHelper return this which enables calls to flashHelper to be chained together, e.g.

flashHelper.info("msg1").warn("msg3").error("foo")

Feedback

Suggestions for improvements or bugs may be sent to the e-mail address in the plugin descriptor file. Patches are welcome, but please use the unit/integration tests provided to verify that the patch hasn't broken anything.

Version History

  • 0.9.6 - Moved source code to GitHub, performed some refactoring and added more tests
  • 0.9.5 - Upgraded to Grails 2.2.0. No previous version of this plugin will work for Grails >= 2.2.0
  • 0.9.4 - Use LocaleContextHolder.locale for default locale
  • 0.9.3 - Upgraded to Grails 2.1.0 and latest version of release plugin
  • 0.9 - Upgraded to Grails 2.0.4 and changed taglib namespace from flash to flashMsg because the former collided with the flash object when you try to invoke the tag using the namespace.tagName() syntax.
  • 0.8 - Removed ability to store multiple messages under the same key in a single method call. The value of this feature isn't worth the complexity it adds to the API / implementation / documentation
  • 0.7.6 - removed redundant code - no functional changes
  • 0.7.5 - Changes to the named arguments API
  • 0.7 - Added named arguments API
  • 0.6.5 - Refactoring and performance improvements - no functional changes
  • 0.6 - Added argument resolution
  • 0.5.1 - Changed package names - no functional changes
  • 0.5 - Added remove attribute to tags
  • 0.4.1 - Upgraded to Grails 1.3.6
  • 0.4 - Added keyNotFound attribute to tags
  • 0.3 - Added flash:msgBody tag
  • 0.2.1 - Upgraded to Grails 1.2.2 and fixed problem with controller dependency version
  • 0.2 - Fixed bugs and added tag library, support for default messages, method chaining, and clear()
  • 0.1 - Initial release