Last updated by 3 years ago

Page: Navigation plugin, Version:15

Navigation Plugin

Author: Marc Palmer (http://www.anyware.co.uk)
CSS by: Andreas Arledal

Overview

This plugin implements simple menu navigation in your application using convention, with default rendering like this:

With it you can:

  • Render menus or tabs, with control over the rendering and CSS styling
  • Indicate which controllers 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"
But doing navigation is easy. Why is this a good thing?

Because it 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 elements to your UI.

Installation

grails install-plugin navigation

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.

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.

You can also 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 controller name or the title of the parent in lower case (if suplied as an argument to renderSubItems tag):

navigation.tabs.inbox=Your Inbox
subnavigation.tabs.inbox.inbox=New Inbox
subnavigation.tabs.inbox.archive=Archive

Note that the 'root bundle' for sub-items is 'subnavigation'.

You may also 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 before and after attributes to amend the HTML that is rendered.

Thirdly, you can forget nav:renderXXX altogether and use <nav:eachItem> or <nav:eachSubItem> directly.

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.updated()

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']
]

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
  • 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
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
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

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)