Navigation plugin

  • Authors: Marc Palmer
22 votes
Dependency:
compile ":navigation:1.3.2"

 Source  Issues

Summary

Provides convention-based navigation data for your UI and tags for rendering the navigation menus.

Description

This plugin is DEPRECATED. This documentation is left here for users who have used it to date. If you are not already using this and looking for navigation menu handling, please consider using the Platform Core plugin's Navigation API. Same author, but 3-4 years' more experience later, and much better docs.

This plugin implements simple menu navigation in your application using convention - your controllers and actions are mapped into the navigation structure - with default rendering like this:

With it you can:

  • Render menus or tabs, with control over the rendering and CSS styling
  • Indicate which controllers and actions should appear in menus
  • Declare and render sub-items of a top-level item
  • Explicitly add non-convention based menu items for the "20% case"
  • Have as many navigation groups (each is a 2-level menu) as you like
The plugin makes navigation in simple apps a no-brainer - all you need to do is style it and set a property in controllers.

It also opens up other possibilities like layouts and GSP pages that are portable between applications, and plugins that automatically add navigation items to your UI (plugins like Weceem let you inject items into their navigation).

Installation

Just run:

grails install-plugin navigation

… or just add the dependency to your BuildConfig

Getting started

Once installed into your application, simply edit any controllers that you would like to appear in the navigation and add a "navigation" static property, like this:

class DashboardController {
	static navigation = true

def index = { } }

Then edit your layout and add a navigation tag:

<html>
<head><nav:resources/></head>
<body>
    <div id="menu">
		<nav:render/>
    </div>
    <g:layoutBody/>
</body>
</html>

Then view the page. As if by magic, you have a menu tab! Clicking on the tabs takes you to the default action of the controllers. The name of the tabs is automatically extracted from the controller names. The currently active item is highlighted automatically.

This is the simplest usage of the plugin. What you've actually done is register the controllers in the "all" navigation group, and rendered these.

Say for example you want to change the text for one of the tabs so that it differs from the controller name, choose an action that isn't the default, change the ordering of menu items, and put them into a different named group so you can have different sets of navigation:

class DashboardController {

static navigation = [ group:'tabs', order:10, title:'Your Inbox', action:'inbox' ]

def index = { }

def inbox = { [items:Inbox.findAllByUser(user)]} }

class SettingsController {

static navigation = [ group:'tabs', order:100, title:'Account', action:'billing' ]

def index = { }

def billing = { } }

You then change the GSP to:

<html>
<head><nav:resources/></head>
<body>
    <div id="menu">
		<nav:render group="tabs"/>
    </div>
    <g:layoutBody/>
</body>
</html>

Now with this you should see "Your Inbox" and "Account" as tabs, in that order, and they will invoke the actions specified.

At the same time you might have another AuthController with "login" and "view profile" actions, and you want to display these login functions in a different area of the page - no problem. Let's also change the markup used to render the links, and make the display of some conditional:

class AuthController {
	static navigation = [
		[group:'userOptions', action:'login', order: 0, isVisible: { session.user == null }],
		[action:'logout', order: 99, isVisible: { session.user != null }],
		[action:'profile', order: 1, isVisible: { session.user != null }]
	]

def login = { }

def logout = { }

def profile = { } }

And now if you change your layout GSP:

<html>
<head><nav:resources/></head>
<body>
	<div id="user"> // You might use CSS to, say, right-align this
		<nav:render group="userOptions"/>
	</div>
    <div id="menu">
		<nav:render group="tabs"/>
    </div>
    <g:layoutBody/>
</body>
</html>

There you have it. The user options should appear separate from the tabs.

A note about isVisible: the closure can access all services registered by your Grails application, as well as the session, request, params and flash (the last four as long as those were available by the context that calls 'render').

OK so next you realise that you need multiple sections in the Inbox - "New" and "Archive". No problem:

class DashboardController {

static navigation = [ group:'tabs', order:10, title:'Your Inbox', action:'newInbox', subItems: ['newInbox', 'archive'] ]

def index = { newInbox() }

def newInbox = { [items:Inbox.findAllByUserAndArchive(user, false)]}

def archive = { [items:Inbox.findAllByUserAndArchive(user, true)]} }

Then edit your GSP to render the sub-items:

<html>
<head><nav:resources/></head>
<body>
	<div id="user"> // You might use CSS to, say, right-align this
		<nav:render group="userOptions"/>
	</div>
    <div id="menu">
		<nav:render group="tabs"/><br/>
		<nav:renderSubItems group="tabs"/>
    </div>
    <g:layoutBody/>
</body>
</html>

Reload the page and bingo. Selecting dashboard should show 2 sub-items - "New Inbox" and "Archive". Also notice how there is different styling for the currently selected item - it knows which controller and action you are on and uses this to highlight them correctly.

There are more options here - specifically most of the properties that apply to top-level items can also be specified on subItems:

class DashboardController {

static navigation = [ group:'tabs', order:10, title:'Your Inbox', action:'newInbox', subItems: [ [action:'newInbox', order:1, title:"Inbox"], [action:'archive', order:10, title:'Old items'] ] ]

def index = { newInbox() }

def newInbox = { [items:Inbox.findAllByUserAndArchive(user, false)]}

def archive = { [items:Inbox.findAllByUserAndArchive(user, true)]} }

Just reload the page to see the changes.

Now the observant among you will have noticed that specifying the text of your nav elements is bad for i18n and is strictly for prototyping only!

So you will be glad to know that the nav:renderXXX tags all use message bundles to resolve the menu labels, using the title. So title "inbox" for an item in group "tabs" would look for a message bundle message such as:

navigation.tabs.inbox=Your Inbox

Sub-items are resolved using the title of the parent in lower case:

navigation.tabs.inbox=Your Inbox
navigation.tabs.inbox.inbox=Your Inbox
navigation.tabs.inbox.archive=Your Inbox

You may notice here the use of "archive" instead of the title "Old items" used in the last example. Its generally the case that if you want to use message bundles that you should not use spaces or punctuation in the titles you give.

So you can specify English titles first during prototyping then change all the title attributes (or leave them to default to action names) to be simple identifiers and define all your strings in the i18n bundles.

To customize the styling, you can either simply override or replace the CSS provided by the plugin. The best plan is probably to copy all the CSS out of plugins/navigation-x.y/web-app/css/navigation.css and put it into your own CSS file. To do this you will need to change your nav:resources tag to:

<nav:resources override="true"/>

You can also use the CSS :before and :after selectors to amend the rendering without any changes to the HTML.

However, for many situations you will need the "nuclear option": forget <nav:renderXXX> altogether and use <nav:eachItem> or <nav:eachSubItem> to directly render any markup you like yourself.

You can also manually register menu items programatically by calling into the NavigationService, and also through Config.groovy. You may need to do this if you have controllers supplied by plugins that you wish to include in navigation or links to specific content in a generic controller, for example.

Adding items programatically:

// first get the navigationService instance… how depends on where you are
navigationService.registerItem('tabs', [controller:'reports', action:'users', title:'Reports'])
navigationService.registerItem('tabs', [controller:'content', action:'view', id:'welcome', title:'Welcome'])
navigationService.update()

Alternatively, adding items in Config.groovy:

navigation.user = [controller:'content',title:'Log in',action:'view',id:'login']
navigation.dashboard = [
    [controller:'content',title:'Help',action:'view',id:'help'],
    [controller:'content',title:'Beta info',action:'view',id:'beta']
]

How it works out what items are "active"

Normally you want to show the user which section of the navigation they are currently in. The rendering passes in an "active" flag for each item that is considered active.

As of 1.2 this is calculated in a new way, using the current controller and action and request parameters to create a path e.g. "book/list". This is reverse-mapped to the controller and action that is declared with this path. Any nav item or sub item that matches the start or all of this path is marked active. Exact matches for query parameters are also supported, but it will fall back to a definition that does not have any parameters specified if a match is not found for e.g. "book/list/[max:5]"

You can use this mechanism to provide different path mappings, other than controller/action, and to specify navigation items that do not exist as controllers - perhaps standalone GSPs. Just add the "path" attribute to the navigation declaration.

You can then tell the tags what is your current "activePath" if the plugin will not be able to work it out from the controller/action (which may be null if rendering a standalone GSP):

class DashboardController {

static navigation = [ group:'tabs', order:10, title:'Your Inbox', action:'newInbox', path:'inbox', subItems: [[action:'newInbox', path:'inbox/new'], [action:'archive', path:'archive']] ]

def index = { newInbox() }

def newInbox = { [items:Inbox.findAllByUserAndArchive(user, false)]}

def archive = { [items:Inbox.findAllByUserAndArchive(user, true)]} }

This will work as normal with the tags, but it will allow you to have a GSP that for example passes activePath='archive' to the navigation tag and renders the nav as if you are in the archive action.

Commercial Support

Commercial support is available for this and other Grailsrocks plugins.

Tag reference

nav:resources

Pulls in the CSS needed for default rendering. Failure to include this in a page that uses nav:render tags will result in an exception

Attributes

  • override - true to suppress inclusion of CSS, eg you are providing it in your own CSS
nav:eachItem

Iterate over each item in a navigation group

Attributes

  • group - the name of the group. Defaults to the all '*' group if not supplied
  • activePath - Optional. The current "location" inside your navigation structure. Use this to force the navigation to assume you are in a certain place in the hierarchy, or for when the current controller/action do not map directly to the menu items that should be shown as "active". Example: 'main/admin'
  • var - the name of the variable to contain the item when the body is invoked. Optional
nav:eachSubItem

Iterate over each sub-item of an item in a navigation group

Attributes

  • group - the name of the group. Defaults to the all '*' group if not supplied
  • var - the name of the variable to contain the item when the body is invoked. Optional
  • title - the title of the parent element to locate. Optional - defaults to current controller's subitems
  • controller - the name of the controller identifying the parent of the subitems. Optional - defaults to current controller's subitems
  • activePath - Optional. The current "location" inside your navigation structure. Use this to force the navigation to assume you are in a certain place in the hierarchy, or for when the current controller/action do not map directly to the menu items that should be shown as "active". Example: 'main/admin'
nav:ifHasItems

Renders the body of the tag only if the specified group has some items

Attributes:

  • group - the name of the group. Defaults to the all '*' group if not supplied
nav:ifHasNoItems

Renders the body of the tag only if the specified group has no items

Attributes

  • group - the name of the group. Defaults to the all '*' group if not supplied
nav:render

Renders all the navigation items within the specified group with default styling, which is a <ul> list with class "navigation" and optional id. Each item item has css classes set on it as appropriate; navigation_first, navigation_last, navigation_active. The default styling requires the <nav:resources/> tag to be present in the <head> section.

Active items are determined by the current action and controller combination.

Attributes

  • id - id of the <ul> tag holding the menu items. Optional - defaults to "navigation_<nameofgroup>"
  • group - the name of the navigation group. Defaults to the all '*' group if not supplied
  • subitems - set to true to automatically render all subitems in nested <ul> elements
  • activePath - Optional. The current "location" inside your navigation structure. Use this to force the navigation to assume you are in a certain place in the hierarchy, or for when the current controller/action do not map directly to the menu items that should be shown as "active". Example: 'main/admin'
nav:renderSubItems

Renders all the navigation sub-items within the specified group and below the selected item. The default styling, which is a <ul> list with class "subnavigation" and optional id. Each item item has css classes set on it as appropriate; subnavigation_first, navigation_last, navigation_active. The default styling requires the <nav:resources/> tag to be present in the <head> section.

Active items are determined by the current action and controller combination.

Attributes

  • id - id of the <ul> tag holding the menu items. Optional - defaults to "navigation_<nameofgroup>"
  • group - the name of the navigation group. Defaults to the all '*' group if not supplied
  • var - the name of the variable to contain the item when the body is invoked. Optional
  • title - the title of the parent element to locate. Defaults to the name of the current controller if none supplied. Optional
  • activePath - Optional. The current "location" inside your navigation structure. Use this to force the navigation to assume you are in a certain place in the hierarchy, or for when the current controller/action do not map directly to the menu items that should be shown as "active". Example: 'main/admin'

Service reference

navigationService

This service manages the navigation data.

registerItem(GrailsControllerClass controllerClass)

Used internally to register a controller with menu info taken from convention properties.

registerItem(String group, params)

Can be called to explicitly add an item, using the same properties as if defined in a controller. SubItems, if any, must be explicitly populated with all relevant property values.

hide(String controllerName)

Called to indicate that any registered navigation information for the specified controller should always be ignored. Use to suppress navigation data registered by a 3rd party plugin.

reset()

Called on reload to reset the data back to defaults - that is all data registered in Config.groovy and from previous calls to registerItem. Note that changes to your code that calls registerItem will likely result in duplication and a restart will be needed.

updated()

Must be called after any manual registrations are complete (apart from in Config.groovy)