(Quick Reference)
Grails Plugin Platform Core - Reference Documentation
Authors: Marc Palmer (marc@grailsrocks.com), Stéphane Maldini (stephane.maldini@gmail.com)
Version: 1.0.M2-SNAPSHOT
1 Overview
The Plugin Platform provides APIs and utilities that provide the glue required for advanced Grails plugin development and integration across multiple Grails versions, and turbo-charge the development of a new generation of plugins.
The founding principle is that these platform features should not be part of
Grails core because this would tie plugins that use the APIs to specific Grails versions.
This relative freedom from Grails versions means that plugins that use the platform should remain compatible with more Grails versions for longer, and that new features used by all plugins can be added outside of the Grails release cycle.
1.1 The APIs
The features include in this release include:
- Configuration API - Plugin-namespaced config, Config merging, validation and default values
- Security API - An abstraction of basic security features that most applications require, with implementations to be provided by plugins or your application
- UI Extensions - A set of tags and helper functions
Each feature is covered in more detail later in this documentation.
The platform is very self-referential - it uses own APIs - for example to provide event hooks for Grails application lifecycle, to declare dynamic methods on your controller and service artefacts, and to declare the configuration values that it uses.
All of these APIs are designed to be as simple as possible while providing some very high level features.
Use this platform to add tighter and more consistent integration to your own plugins.
1.2 Change log
1.0.M2-SNAPSHOT
- Refined and documented Navigation API
- There are no longer any "g" namespaced tags. All
g:
tags have move to p:
namespace
- Added "site.url" Config setting for siteLink tag to use instead of grails.serverURL if the two differ for your use case
- Refactored Injection, Conventions and Navigation implementations into public interface + implementation classes
1.0.M1
First public release with Config and Security APIs and some UI Extensions. Work-in-progress APIs for Events, Injection, Conventions and Navigation.
1.3 Known Issues
1.0.M2-SNAPSHOT
- Automatic convention controller navigation includes all actions, not just those with GET allowedMethod
1.0.M1
- Config API - false validation errors with platform-ui due to no x.y.'*' support yet
- Navigation API - controllers do not auto-register in Grails 1.3.x, no DSL artefact, no reloading
- Conventions API - API not for public use. Not fully implemented / TBD
- Events - API not for public use. Scopes not fully implemented / TBD
- Injection - injection may not re-apply dynamic methods and properties to reloaded or new artefacts, API is not for public use at all yet
2 Getting Started
To get started you need to install the platform-core plugin.
Add the plugin platform as a dependency to your application or plugin, either directly or transitively. To do so directly edit your
BuildConfig.groovy
to look something like this:
…
plugins {
…
compile ":platform-core:1.0.M1"
}
You can run your code now and browse to
http://localhost:8080/<appname>/platform/
, a special page available only in the development environment. From there you can explore some of the plugin platform features in the debug interface.
There is no default security implementation for the Security API, this will need to come from the security plugin you use, or your application. See
implementing a security bridge
3 UI Extensions
Several simple UI Extensions are included in platform-core.
The tags supplied make it trivial to render links to controllers and actions
using i18n messages, display messages to the end user, and render buttons and
labels in i18n friendly ways.
Linking tags
The
smartLink tag renders links for controllers and actions, automatically working out the text of the link using i18n.
<%-- Link to default action of BooksController --%>
<p:smartLink controller="books"/><%-- Link to list action of current controller --%>
<p:smartLink action="list"/>
These will use i18n messages located by convention of the form:
action.controllerName.actionName
Label tag
The label tag will render a <label> with the text optionally loaded from i18n:
<p:label text="field.user.name"/>
See
label for full details of the attributes, which include passing arguments to the i18n message.
Button tag
The button tag will render a text-based button using either a <button>, <input type="submit"> or <a> tag, with the text optionally loaded from i18n:
<p:button text="button.save"/>
See
button for full details of the attributes, which include setting the kind of button rendered and passing arguments to the i18n message.
Display message tag
The displayMessage tag works with the
displayMessage
and
displayFlashMessage
controller methods to make it easy to render messages to the user in a uniform way.
See
displayMessage for full details of the attributes and the
displayMessage and
displayFlashMessage controller methods.
The tag will render both request and flash messages, and wraps them in a div with CSS classes according to the type of message.
Branding tags
There are several simple but useful site branding tags included. Commonly to be used in site footers and emails templates.
- organization - Renders the name of the business, taken from Config var
plugin.platformCore.organization.name
- siteName - Renders the name of the site/product, taken from Config var
plugin.platformCore.site.name
- siteURL - Renders an absolute URL for the root of the site
- siteLink - Renders an absolute link to the site, with the site name as the link text
- year - Renders the current year, for use in copyright footers
3.2 Beans and utilities
There are a some UI utility classes and beans available:
- grailsUiHelper - Provides methods for setting and getting displayMessages
- TagLibUtils - Provides helper functions for manipulating attributes, CSS class name lists etc.
4 Configuration API
The Configuration API adds the following features:
- A way to declare the Config properties that a plugin supports
- Automatic namespacing of plugin Config values to avoid clashes
- Validation of Config values
- Merging of config from plugins into main Application config
- The ability for plugins to configure other plugins
- An injected "pluginConfig" variable in all artefacts containing the plugin's configuration
All of this adds up to more powerful integration and less frustration and confusion for developers.
4.1 Changing Application and Plugin Config Values
To change application or plugin configuration from within a plugin you need to declare the
doWithConfig hook in your plugin descriptor.
The code uses a simple DSL that is identical to normal Config except:
- The top level nodes are plugin names or "application" to determine what scope of config you are changing
- The closure is passed the existing config as its first and only argument
The application Config is loaded first. All the doWithConfig blocks are evaluated and the results merged in.
def doWithConfig = { config ->
platformUi {
ui.Bootstrap.button.cssClass = 'btn'
ui.Bootstrap.tab.cssClass = 'tab-pane'
ui.Bootstrap.field.cssClass = 'input'
} application {
// set something based on another config value that has already been
// by the application
grails.x.y = confg.p.q == 'something' ? true : false
}
}
The plugin name scope is the camel case beanNameConventionStyle. So the above is setting config values for the "grails-platform-ui" plugin.
Any values within a plugin scoped block as in the first block above, are merged into the main Config under
plugin.<pluginName>
. It is therefore impossible to change global Config values within a plugin config block.
Values inside the "application" block are merged into the root level of the application Config, so you can set any normal value.
4.2 Declaring Configuration Options
To make use of the plugin configuration features and make life easier for developers, your plugin must define the configuration options it accepts.
This allows the platform to warn users when they mistype a config name or supply and invalid value - and to formalize definition of default values rather than a plugin merging in default values.
Do declare the options your plugin supports, add the
doWithConfigOptions
closure to your plugin descriptor:
def doWithConfigOptions = {
'organization.name'(type: String, defaultValue: 'My Corp (set plugin.platformCore.organization.name)')
'site.name'(type: String, defaultValue: 'Our App (set plugin.platformCore.site.name)')
}
This block, from the platform core, defines two configuration values of type String, with a default value.
You can also supply a custom validator:
def doWithConfigOptions = {
'concurrentConnections'(type: Integer, defaultValue: 10,
validator: { v -> v < 500 ? null : 'concurrent.connections.too.big' }
}
Behaving just like constraint validators, your validator returns null for "ok" or an i18n message string for the error.
4.3 Accessing Plugin Config
Plugins that declare their configuration with
doWithConfigOptions can get access
to their "slice" of the Config using the
pluginConfig variable.
The
pluginConfig
variable is automatically injected into all artefacts of
your plugin, automatically namespace for your plugin using the
plugin.<pluginName>.
prefix.
So in a service you can trivially access this config inside a service or controller for example:
class MyPluginService {
def doSomething() {
if (pluginConfig.enabled) {
println "It worked!"
}
}
}
5 Security API
The Security API provides common security related functionality so that plugins and in some cases applications no longer need to be tied to a specific security implementation.
It is deliberately minimal to place few requirements on implementations.
It is not intended to be a complete security abstraction, but "just enough" for the needs of most applications and plugins that do not require advanced security manipulation - which will likely always require direct knowledge of the underlying provider.
This API provides the most basic security features to enable this
interoperability, using a bridging interface that security plugins must
implement to actually provide these services.
Security plugins must implement the "provider" bean - out of the box there is no security implementation.
Dynamic methods
These methods and properties are added to Services, Controllers, TagLibs and Domain Classes:
- securityIdentity - The string used to identify the current logged in user. Typically a user id, user name or email address. The nature of this value is dependent on your security implementation
- securityInfo - The object representing the current logged in user's information.
- userHasAnyRole - test if the current user has any of the roles
- userHasAllRoles - test if the current user has all of the roles
- userIsAllowed - test if the current user has a specified permission on an object
- withUser - run a section of code as another user
See the reference guide for Security Properties and Security Methods for further details on these.
All of these features and more can be accessed by any code in your application by using the
grailsSecurity bean.
Tags
Here's an example of some of the security tags available:
<s:identity/>
<s:info property="email"/>
<s:ifPermitted role="ROLE_ADMIN">
…
</s:ifPermitted>
<s:ifNotPermitted role="ROLE_ADMIN">
…
</s:ifNotPermitted>
<a href="${s.createLoginLink()}">Log in</a>
<a href="${s.createLogoutLink()}">Log out</a>
<a href="${s.createSignupLink()}">Sign up</a>
See the
Tags Security reference section for details.
grailsSecurity bean
The Security bean provides access to all the basic security functions. These are passed through to the security bridge implementation.
This includes methods for applications and plugins to use such as:
- String getUserIdentity()
- def getUserInfo()
- boolean userHasRole(role)
- boolean userIsAllowed(object, action)
- def ifUserHasRole(role, Closure code)
You simply auto wire this bean into your classes using the name "grailsSecurity"
For more details see
grailsSecurity.
5.1 Implementing a Security Bridge
To use the Security API, an application must have a Security Bridge bean that implements the
SecurityBridge interface,
Typically this bean will be provided by the security plugin you are using. However you can easily implement your own in your own plugin or application.
Simply implement the interface and register the bean as "grailsSecurityBridge" in your Spring context either in a plugin's
doWithSpring
or your application's
resources.groovy
:
grailsSecurityBridge(com.mycorp.security.MySecurityProvider) {
// wire in any other dependencies it has
grailsApplication = ref('grailsApplication')
}
The interface is defined here:
interface SecurityBridge { /**
* Implementations must return the name of their security provider
* @return A name such as "Spring Security"
*/
String getProviderName() /**
* Get user id string i.e. "marcpalmer" of the currently logged in user, from whatever
* underlying security API is in force
* @return the user name / identity String or null if nobody is logged in
*/
String getUserIdentity() /**
* Get user info object containing i.e. email address, other stuff defined by the security implementation
* @return the implementation's user object or null if nobody is logged in
*/
def getUserInfo() /**
* Return true if the current logged in user has the specified role
*/
boolean userHasRole(role) /**
* Can the current user access this object to perform the named action?
* @param object The object, typically domain but we don't care what
* @param action Some application-defined action string i.e. "view" or "edit"
*/
boolean userIsAllowed(object, action) /**
* Create a link to the specified security action
* @param action One of "login", "logout", "signup"
* @return Must return a Map of arguments to pass to g:link to create the link
*/
Map createLink(String action) /**
* Execute code masquerading as the specified user, for the duration of the Closure block
* @return Whatever the closure returns
*/
def withUser(identity, Closure code)
}
6 Events API
Pending
7 Injection API
Pending
8 Navigation API
The Navigation API provides a standard way to expose information about the menus available in your application and plugins.
Aside from application navigation, plugins can expose their controllers and actions so that application can reuse them in their own navigation structure. Applications can also add items to the navigation structure of plugins, to merge items into the UI of plugins.
8.1 Concepts
There are only three concepts to understand in the Navigation API - items, scopes and the activation path.
Out of the box, scopes are created for all your application and plugin controllers automatically by convention. Items are created in these scopes for every action on the controller.
You will typically move from this to using the navigation DSL artefact for more control over the navigation structure.
What is a navigation item?
An item is a place the user can reach in your navigation structure. Every item results in a menu item and link, and whether it is visible or enabled can be determined at runtime.
Items are always inside one scope.
Items can have child items.
Items must be resolvable from a controller/action pair, so the navigation API can always tell where the user is in the structure if the current controller/action is known and you have an item declared for them.
What is a Scope?
A scope is a name that identifies one or more navigation items. Top-level scopes are called root scopes and represent your main groupings of navigation items. For example you may have your application navigation for regular users and an admin root scope for backend administration.
Example of scope names:
app // typically your default app navigation root scope
app/messages // the "messages" item in the "app" root scope
admin/scaffolding/book // the "book" item under "scaffolding" item in the "admin" scope
plugin.cms/admin // the "admin" item supplied by the "CMS" plugin
plugin.socialFeed/feeds // the "feeds" item supplied by the "social-feed" plugin
Root scopes do not generate any menu links themselves, they are merely containers for your top level navigation items. They enable you to have multiple sets of navigation for different contexts.
The items that scopes refer to can be nested arbitrarily. It is however generally recommended that you use at most 2 levels of navigation, sometimes three if really necessary. This is purely because of the user experience issues with deep navigation.
Usually you should factor out deep navigation into separate root scopes. For example most applications would have the "app" scope, a "footer" scope for footer links like Terms of Use, Support etc., and a "user" scope for log in/out and so on.
What is an Activation Path?
An activation path is a string that represents the currently active navigation item. This may be a few levels down in your navigation structure and represents the breadcrumb trail the user would see to get to the location they are currently viewing.
The activation path is set on the current request and indicates which node is currently active. By default navigation API attempts to identify the correct activation path in your structure using the current controller and action, much like reverse URL mapping.
However you can explicitly set the activation path using a tag or some code, for cases where you need to "fudge" it - for example if your action performs some odd redirection, or the endpoint is simply a GSP view which cannot be reversed to a location in the structure.
8.2 Getting Started
The first thing you need to do is install the platform-core plugin if you haven't already.
If you then run your application and you have some existing controllers you'll find that if you add the nav:
primary tag to one of your sitemesh layouts or GSP pages you will see top-level navigation for each of your controllers.
8.3 Navigation by convention
To get you started quickly, all your controllers will be automatically registered in the "app" scope and each controller has sub-items for each of it actions.
All the tags default to the "app" scope if you don't supply a scope and the current controller/action are within that scope, so it just works out of the box for the simple cases. So add the following to your sitemesh layout or GSP:
<nav:primary/>
<nav:secondary/>
This will render one or two <ul> tags for the "app" scope based on the currently active controller/action pair.
By default all your controllers are automatically declared for you inside the "app" scope if they are not explicitly declared in a navigation DSL artefact and the navigationScope property is not set on them.
These controller scopes have a nested item for each action defined on the controller, including the default action (the same as the link for e controller scope itself).
Moving some controllers from the default app navigation scope
You often have some controllers that you don't want to appear in the main navigation of the application. You may want these to appear in an admin interface for example. To do this with convention based navigation you can just add a static navigationScope property to controllers.
class BookController {
static scaffold = Book static navigationScope = 'admin'
}
This allows you to push controllers into another scope. Note that plugin controllers are automatically namespaced into a scope under "plugin.<pluginName>", in a scope beneath this with the value of the navigationScope property.
You will not need to change your tags to render the admin navigation - if the controller/action the user is viewing resolves to an item inside the admin scope, the nav:primary tag will render the admin scope.
8.4 What is primary and secondary navigation?
The primary navigation is the top level application the user sees, and the secondary is the context-sensitive sub items of the currently active primary item.
Contemporary site styles typically separate out the primary and secondary navigation.
The primary and secondary tags are geared up for this and automatically lookup up the scope and activation path to work out what to render.
Normally you will only use these once in a page.
You can render any part of your navigation structure as a menu as many times as you like anywhere in your pages, using the
menu tag.
Rendering multiple navigation scopes on the same page
A typical contemporary application will have something like three separate menus used on most pages; main, user and footer.
The main menu would use
primary &
secondary tags.
You would then render the user and footer navigation using the menu tag, and passing the user and footer scopes:
<html>
<body>
<nav:primary/>
<nav:secondary/> <div id="user-nav">
<nav:menu scope="user"/>
</div> <g:layoutBody/> <div id="footer-nav">
<nav:menu scope="footer"/>
</div>
</body>
</html>
This results in a page where there are actually for navigation renderings, showing different scopes.
8.6 Using the Navigation DSL
To declare navigation items you use navigation DSL artefacts to determine the items in each scope. Scopes are named and can be nested to provide a hierarchy.
Navigation artefacts are groovy scripts end in the name "Navigation" in
grails-app/conf
.
Here's an example for the various ways to use the DSL to declare scopes and items:
Example contents of
grails-app/conf/AppNavigation.groovy
:
navigation = {
// Declare the "app" scope, used by default in tags
app { // A nav item pointing to HomeController, using the default action
home() // Items pointing to ContentController, using the specific action
about(controller:'content')
contact(controller:'content')
help(controller:'content') // Some user interface actions in second-level nav
// All in BooksController
books {
// "list" action in "books" controller
list()
// "create" action in "books" controller
create()
} // More convoluted stuff split across controllers/locations
support(controller:'content', action:'support') {
faq(url:'http://faqs.mysite.com') // point to CMS
makeRequest(controller:'supportRequest', action:'create')
}
} // Some back-end admin scaffolding stuff in a separate scope
admin {
// Use "list" action as default item, even if its not default action
// and create automatic sub-items for the other actions
books(controller:'bookAdmin', action:'list, create, search') // User admin, with default screen using "search" action
users(controller:'userAdmin', action:'search') {
// Declare action alias so "create" is active for both "create" and "update" actions
create(action:'create', actionAliases:'update')
}
}
}
You can also use overrides in the navigation DSL to move items from one scope to another for example to move items declared by a plugin into your primary application navigation.
This part is not implemented in 1.0.M2-SNAPSHOT - code sample to be written
Using tags such as the
primary and
secondary navigation tags you can render all the page elements you need.
The Navigation DSL Definition
The script must return a Closure in the
navigation
variable in the binding.
This closure represents the DSL and method invocations have a special meaning within the DSL.
The name used in method calls is used to construct the activation path of each item. So a call to "app" that has a call to "messages" which has a closure that calls "inbox" will create:
- A scope called "app"
- A top-level item in the "app" scope, called "messages", with activation path
app/message
# A nested item under "messages" called "inbox" with activation path
app/messages/inbox
Top level method invocations (root scopes)
The top-level method calls that pass a Closure define root scopes in the navigation structure.
The "app" scope is a prime example of this:
navigation = {
app {
home controller:'test', data:[icon:'house']
}
}
By default scopes defined by Navigation artefacts within plugins are automatically namespaced to prevent collisions with application namespaces.
Thus the scope "app" in a plugin called "SpringSecurityCore" would become the scope "plugin.springSecurityCore.app". If a plugin defines the scope with the
global:true
argument, this will not happen:
// Example of a plugin exposing a root scope without namespacing
navigation = {
app(global:true) {
contact controller:'test', data:[icon:'mail']
}
}
Nested method calls - defining navigation items
The DSL supports the following arguments when defining a navigation items.
Linking arguments
These are
controller
,
action
,
uri
,
url
and
view
. These are passed to
g:link
to create links. The "view" attribute is handled internally and removed and converted to "uri" for the purpose of calling g:link
These values are passed through to the navigation tags for link rendering just as you would expect when calling
g:link
.
There are some special behaviours however:
Argument | Usage |
---|
controller | Optional - it will be inherited from the parent node if the parent uses controller to create its link, or failing that it will use the name of the DSL method call |
action | Optional - it will fall back to the name of the method call if the controller is specified or inherited. If the controller was not specified either (and hence "uses up" the method call name), this will use the default action of the controller or "index" if none is set. The action value can be a List or comma-delimited string. If it is, the first element is the action used to generate the item's link, and any other actions listed will have sub-items created for them, in alphabetical order. |
actionAliases | Optional - list of actions that will also activate this navigation item. The link is always to the action defined for the item in the DSL, but if the current controller/action resolves to an action in this alias list, the navigation item will appear to be active. Used for those situations where you have multiple actions presenting the same user view i.e. create/save, edit/update |
Visibility and Status
You can control per request whether items are visible or enabled, or set this in the navigation structure statically.
The arguments:
Argument | Usage |
---|
visible | Determines whether the item is visible and can be a boolean or a Closure. If it is a Closure, it will receive a "grailsApplication" property that can be used to reach beans in the main context and will be called every time the item needs to be rendered. |
enabled | Determines if the item is enabled or not and can be a boolean or a Closure. If it is a Closure, it will receive a "grailsApplication" property that can be used to reach beans in the main context and will be called every time the item needs to be rendered. |
Typically you will want to hide items if the user is not permitted to see them. An example of doing this with Spring Security Core:
import org.codehaus.groovy.grails.plugins.springsecurity.SpringSecurityUtilsdef loggedIn = { ->
grailsApplication.mainContext.springSecurityService.principal instanceof String
}
def loggedOut = { ->
!(grailsApplication.mainContext.springSecurityService.principal instanceof String)
}
def isAdmin = { ->
SpringSecurityUtils.ifAllGranted('ROLE_ADMIN')
}navigation = {
app {
home controller:'test', data:[icon:'house']
…
} admin {
superUserStuff controller:'admin', visible: isAdmin
…
} user {
login controller:'auth', action:'login', visible: notLoggedIn
logout controller:'auth', action:'logout', visible: loggedIn
signup controller:'auth', action:'signup', visible: notLoggedIn
profile controller:'auth', action:'profile', visible: loggedIn
}
}
Note how the Closures are "def"'d in the script to make them reusable and reachable within the DSL
Title text
The title of an item is the text used to display the navigation item.
Two arguments are used for this:
Argument | Usage |
---|
title | Optional. Represents an i18n message code to use. It defaults to "nav." plus the the item's activation path with "/" converted to "." so path app/messages/inbox becomes the i18n code nav.app.messages.inbox |
titleText | Optional. represents literal text to use for the navigation item title if the i18n bundle does not resolve anything for the value of title |
For automatically created action navigation items, the titleText defaults to the "human friendly" form of the action name. i.e. "index" becomes "Index", "showItems" becomes "Show Items".
Application custom data
Each item can have arbitrary data associated with it - but note that this data is singleton and should not change at runtime.
Typically you would use this to associate some extra data such as an icon name, which you then use in custom menu rendering code.
Just put the values into the "data" Map:
navigation = {
app {
home controller:'test', action:'home', data:[icon:'house']
}
}
Ordering of items
Items are ordered naturally in the order they are declared in the DSL.
However you may wish to manually order items, for example so that plugins (or the application) can inject items into certain positions in your navigation.
Just pass the integer value in the
order
argument:
navigation = {
app {
home controller:'test', action:'home', order:-1000
about controller:'test', action:'about', order:100
contact controller:'test', action:'contact', order:500 data:[icon:'mail']
messages(controller:'test', data:[icon:'inbox'], order:10) {
inbox action:'inbox'
archive action:'archive'
trash action:'trash', order:99999999 // always last
}
}
There are a few Navigation tags available, all detailed in the reference section.
The most common tags you will use are explained here.
It is important to understand that all the tags work by default using the current scope and activation path as determined by the request - but you can override scope and path on all of these tags to render anything you like.
Navigation is rendered by default as an HTML
<ul>
tag with an
<li>
containing a single link for each of the items. Nested items are rendered as nested
<ul>
.
All navigation rendering tags support attributes for CSS class, id and custom rendering of items if required. These are still always rendered within
<ul>
.
nav:primary
Use this tag to render the primary user navigation of your site:
<nav:primary scope="admin" id="nav" class="admin"/><%-- With custom item rendering --%>
<nav:primary scope="admin" id="nav" class="admin" custom="true">
<li>
<p:callTag tag="g:link" attrs="${linkArgs + [class:'nav button']}">
<g:message code="${item.titleMessageCode}"/>
</p:callTag>
</li>
</nav:primary>
See the
primary tag reference for full details.
nav:secondary
Use this tag to render the second-level navigation based on the selected item within the current primary navigation. The scope resolved by
nav:primary
is stored in the request so that this tag knows which scope to use:
<nav:secondary id="secondary-nav" class="admin"/>
See the
secondary tag reference for full details.
nav:menu
The menu tag is used internally by the primary/secondary tags and can be called directly to render any part of the navigation structure, with any activation path.
<nav:menu id="main-nav"/><%-- Render the admin nav 3-deep, including all nested descendents whether active or not --%>
<nav:menu scope="admin" depth="3" forceChildren="true"/>
See the
menu tag reference for full details.
nav:breadcrumb
Breadcrumbs are often used to render a list of links to the "ancestors" of the current navigation location. This tag will render these as a
<ul>
list by traversing the "activation path" for the current request or that passed in as the "path" attribute.
<nav:breadcrumb/><%-- Custom rendering without ul wrapper --%>
<div class="breadcrumb">
<nav:breadcrumb custom="true">
<p:callTag tag="p:smartLink" attrs="${linkArgs}"/>
<g:if test="$">
»
</g:if>
</nav:breadcrumb>
</div>
See the
breadcrumb tag reference for full details.
nav:setActivePath
You can call this tag from inside a controller or GSP if you need to "fudge" the current request's activation path. This is only necessary if there is no controller/action available on the current request, or if you need to simulate navigation while ignoring the real current activation path.
You may need to do this inside an error.gsp for example.
<html>
<body>
<!-- pretend we are in messages/inbox even though we are in a GSP with no controller -->
<nav:setActivePath path="app/messages/inbox"/> <nav:primary/>
<nav:secondary/> <p>Something went wrong!</p>
</body>
</html>
See the
setActivePath tag reference for full details.
9 Convention API
Pending