(Quick Reference)
6 Navigation API - Reference Documentation
Authors: Marc Palmer (marc@grailsrocks.com), Stéphane Maldini (smaldini@vmware.com)
Version: 1.0.RC3
6 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.
6.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. 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.
Breadcrumbs themselves represent a navigational superset of your app's primary navigation structure. They are not supported in this release of the API, because the work has not yet been done to declare breadcrumbs that represent non-navigational items i.e. nested content inside a multi-page document is not part of your regular site navigation.
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.
6.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.
6.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.
6.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.
6.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')
}
}
}
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 the following:
- 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 delegate that supplies request and application properties (see below) |
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 delegate that supplies request and application properties (see below) |
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.SpringSecurityUtils
def loggedIn = { ->
springSecurityService.principal instanceof String
}
def loggedOut = { ->
!(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
The closures receive a delegate which resolves the following standard Grails properties:
- grailsApplication
- pageScope
- session
- request
- controllerName
- actionName
- flash
- params
Any unresolved properties will resolve to the model (pageScope) and failing that, to the application's bean context, so you can resolve service beans etc by just accessing them by name.
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']}">
<nav:title item="${item}"/>
</p:callTag>
</li>
</nav:primary>
This supports custom rendering in the same way as the
menu tag.
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"/>
This supports custom rendering in the same way as the
menu tag.
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"/>
<%-- With custom item rendering --%>
<nav:menu scope="admin" id="nav" class="admin" custom="true">
<li>
<p:callTag tag="g:link" attrs="${linkArgs + [class:'nav button']}">
<nav:title item="${item}"/>
</p:callTag>
</li>
</nav:menu>
See the
menu tag reference for full details.
nav:title
This renders the i18n title of a specific navigation item passed to it; for use in custom menu rendering.
<nav:primary scope="admin" id="nav" class="admin" custom="true">
<li><nav:title item="${item}"/></li>
</nav:primary>
See the
menu tag reference for full details.
nav:set
You can call this tag from inside a controller or GSP if you need to define request-specific parameters.
You can "fudge" the current request's activation path or set the default scope to be used by navigation tags.
You may need to do this inside an error.gsp for example, or inside admin pages to reuse a generic theme that renders navigation using
nav:primary
.
<html>
<body>
<!-- pretend we are in messages/inbox even though we are in a GSP with no controller -->
<nav:set path="app/messages/inbox"/>
<nav:set scope="admin"/>
<!-- or set those together -->
<nav:set path="app/messages/inbox" scope="admin"/>
<!-- these will use whatever the current active path and scope are -->
<nav:primary/>
<nav:secondary/>
<p>Something went wrong!</p>
</body>
</html>
See the
set tag reference for full details.