Across primer part 2: templates, web resources and menus

About the Across primer series

This is the second article in the Across primer series.  Previous articles:

What we'll do in this article

In this article we'll be focusing on some of the templating infrastructure that AcrossWebModule provides. We'll create a layout template for our pages, show a way to register static resources programmatically and show an easy way to set up a dynamic top navigation that can later be extended at runtime.

Source code

All source code can be found on our Bitbucket repository: https://bitbucket.org/beforeach/across-primer/branch/part-2

Most steps in this article correspond with a single commit, you can checkout out to any of those revisions and start building from there on.

Creating a drop-down menu item

$ git checkout de7282b

Currently the browse item in the top navigation is a simple button that navigates to the very first category.  Let's turn it into a drop-down listing all available categories.  The user can then directly select the category he wants and navigate to it.  

Creating drop-down markup

Change the homepage template accordingly:

classpath:views/th/catalog/homepage.thtml
<ul class="nav navbar-nav" th:inline="text">
	<li th:classappend="${selectedCategory == null} ? active"><a th:href="@{/}">[[#{nav.home}]]</a></li>
	<li class="dropdown" th:classappend="${selectedCategory != null} ? active">
		<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
			aria-expanded="false">[[#{nav.browse}]] <span class="caret"></span></a>
		<ul class="dropdown-menu">
			<li th:each="category: ${categories}"
				th:classappend="${selectedCategory == category} ? active">
					<a th:href="@{'/category/' + ${category.path}}">[[${category.name}]]</a>
			</li>
		</ul>
	</li>
</ul>

Add required javascript

For the Bootstrap drop-down to work, we need to add JQuery along with Bootstrap javascript.  At it at the bottom of the homepage template:

<th:block th:fragment="bottom-scripts">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</th:block>

We have wrapped this in a th:block because we need to include the same scripts in the categoryItems template.  A th:block will not render any output itself, only the content of it.  Update the categoryItems template accordingly before the closing body tag:

classpath:views/th/catalog/categoryItems.thtml
<th:block th:replace="th/catalog/homepage :: bottom-scripts" />

Refreshing the page should now give us a working drop-down:

However, if we were to create more pages, we would have to repeat this process everywhere, including the same bottom and head sections from the homepage.  To avoid this we can switch to a layout template for all pages.

Creating a layout template

$ git checkout 3914412

Creating the layout bean and template file

There are several libraries you could use for creating a layout.  For example there is the Thymeleaf layout dialect you could use to define your layouts from within Thymeleaf itself.  However in our case, we'll make use of the templating infrastructure that AcrossWebModule packs.

AcrossWebModule allows you to determine the layout to use on a controller level.  A layout template is configured by creating a LayoutTemplateProcessorAdapterBean that registers the unique name of the layout, and specifies the Thymeleaf template it should use.  Create a MainTemplate as follows:

across.demo.catalog.application.ui.MainTemplate
@Component
public class MainTemplate extends LayoutTemplateProcessorAdapterBean {
    public static final String NAME = "MainTemplate";

    public MainTemplate() {
        super(NAME, "th/catalog/layouts/main");
    }
}

Put the Thymeleaf template in the right location as well:

classpath:views/th/catalog/layouts/main.thtml
<html>
<head>
    <title th:replace="${childPage} :: title">page specific title</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
    <link rel="stylesheet" th:href="@{/across/resources/static/catalog/css/catalog.css}"/>
</head>
<body>

<div class="container">
    <nav class="navbar navbar-default">
        <div class="navbar-header">
            <div class="logo">&nbsp;</div>
            <ul class="nav navbar-nav" th:inline="text">
                <li th:classappend="${selectedCategory == null} ? active"><a th:href="@{/}">[[#{nav.home}]]</a></li>
                <li class="dropdown" th:classappend="${selectedCategory != null} ? active">
                    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
                       aria-expanded="false">[[#{nav.browse}]] <span class="caret"></span></a>
                    <ul class="dropdown-menu">
                        <li th:each="category: ${categories}"
                            th:classappend="${selectedCategory == category} ? active">
                            <a th:href="@{'/category/' + ${category.path}}">[[${category.name}]]</a>
                        </li>
                    </ul>
                </li>
            </ul>
        </div>
    </nav>
    <div th:replace="${childPage} :: content">
        here comes the body content
    </div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

</body>
</html>

Looking at the source code of main.thtml, you can see it looks very much like the homepage template.  The important part is the section <div th:replace="${childPage} :: content">.  This basically tells the layout to inject the fragment identified as content from the original template that was requested.  

The AcrossWebModule web templates work on an interceptor level. When requesting a particular view, the controller will be inspected to determine if a layout should be applied. If that is the case, the original view name will be stored in a childPage request attribute, and the layout template will be rendered instead. Using Thymeleaf include mechanism can then be used to render the right fragments from the original page.

Modifying homepage and categoryItems

In order for the original templates to work with the layout, we need to change them to include a content fragment.  We can also remove all markup (css includes etc) that is no longer required because it is now rendered by the layout template.  The latter is not strictly necessary however.   Thymeleaf strongly stresses a natural templating approach.  To have a better idea what that means and how you can harness the power of Thymeleaf to do so, we gladly refer you to the Thymeleaf user documentation.

In our case, let's alter the original templates like so:

classpath:views/th/catalog/homepage.thtml
<div class="container">
    <div th:fragment="content" class="jumbotron">
        <h1 th:text="#{welcome.message}">Hello world!</h1>
    </div>
</div>
classpath:views/th/catalog/categoryItems.thtml
<html>
<head>
    <title th:inline="text">Category: [[${selectedCategory.name}]]</title>
</head>
<body>
<div class="container">
    <div th:fragment="content" class="row">
		...
    </div>
</div>

</body>
</html>

Specifying the layout on the controllers

We have created the layout and made sure our templates can work with it, however we still need to specify which layout the controllers should use.  We can do this by adding the @Template annotation on either @Controller or @RequestMapping level.  Add the annotation to both HomepageController and CategoryItemsController.

@Controller
@Template(MainTemplate.NAME)
public class HomepageController {

Clear the template on the exception handler

There is a problem however.  Try calling the category detail with an invalid category path, eg /category/iDontExist.  You will notice that now we get a non-working page.  There is no body content and the top navigation is not complete.

The reason for this is that the MainTemplate is being applied because we specified it on the CategoryController.  Often however when there is an exception, there is a good change we cannot render the layout because some of the required model attributes might not have been set.  So in this case we do not want the layout to be used.

There's two easy ways to solve this:

  1. specify the @Template on the individual @RequestMapping methods instead of on the controller
  2. add a @ClearTemplate annotation on those mappings where we do not want the template to apply

In our case, let's try the latter:

Updated CategoryController
    @ClearTemplate
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String categoryNotFound(Exception e, Model model) {
        model.addAttribute("feedback", e.getMessage());
        return "th/catalog/404";
    }

Restarting the application and calling the error page (or if you have spring-boot-devtools, just recompiling should do the trick) should now yield the following result:

Now we get an unstyled error page because no layout has been applied, and we removed the css includes from the homepage head section.

If you still get styles, you probably did not remove the head section in the homepage template. That's okay as well (smile)

Creating a separate error template

$ git checkout e583c54

Instead of no template, let's create a separate template for errors instead.  Repeat the process above and create a LayoutTemplateProcessorAdapterBean and the layout file it references:

across.demo.catalog.application.ui.ErrorTemplate
@Component
public class ErrorTemplate extends LayoutTemplateProcessorAdapterBean {
    public static final String NAME = "ErrorTemplate";

    public ErrorTemplate() {
        super(NAME, "th/catalog/layouts/error");
    }
}
classpath:views/th/catalog/layouts/error.thtml
<html>
<head>
    <title>Error</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"/>
    <link rel="stylesheet" th:href="@{/across/resources/static/catalog/css/catalog.css}"/>
</head>
<body>

<div class="container">
    <div th:replace="${childPage} :: content">
        here comes the body content
    </div>
</div>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

</body>
</html>

Finally, replace the @ClearTemplate with a separate @Template on the exception handler:

Updated CategoryController
    @Template(ErrorTemplate.NAME)
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public String categoryNotFound(Exception e, Model model) {
        model.addAttribute("feedback", e.getMessage());
        return "th/catalog/404";
    }

If you reload you should see the styled error page again.

We now have more than one @Template annotation in our controller class.  However a @Template on request mapping level will always take precedence over the annotation on the controller level.

Registering the default template

$ git checkout 0bbc90a

It's not always very convenient to have to specify the @Template on every separate controller.  Often there is a common layout that is being applied and only some controllers use a different one.  One strategy would be to inherit from a base class or interface, but apart from tighter coupling, we would still have to remember doing that for every controller.

There is a different approach we can take.  All web templates are registered in the WebTemplateRegistry and whenever a request comes in, AcrossWebModule will check which template should be applied.  By default if there is none specified the original view name will be rendered.  However we can also specify the name of the default template that should be used.  In that case, if there is no @Template or @ClearTemplate specified for the requested handler, the template with the default name will be used.

So let's add an @Autowired method on the MainTemplate and have the template register itself as the default in the registry:

across.demo.catalog.application.ui.MainTemplate
@Component
public class MainTemplate extends LayoutTemplateProcessorAdapterBean {
    public static final String NAME = "MainTemplate";

    public MainTemplate() {
        super(NAME, "th/catalog/layouts/main");
    }

    @Autowired
    public void registerAsDefaultTemplate(WebTemplateRegistry webTemplateRegistry) {
        webTemplateRegistry.setDefaultTemplateName(NAME);
    }
}

We can now remove the @Template on the HomepageController and CategoryController.  Of course, the @Template(ErrorTemplate.NAME) on the exception handler should be kept in place!

Dynamically registering web resources

$ git checkout ce58755

While we now have two separate layouts with their own template files.  But we still need to add the static resources to each template separately.  We could ofcourse include fragments from each other, but let's show a different way.  Let's register these resources from the backend, allowing also controllers to extend resources dynamically.

Register web resources from the template bean

The LayoutTemplateProcessorAdapterBean contains a couple of empty adapter methods.  One of those is registerWebResources that takes a single WebResourceRegistry as a parameter.  The WebResourceRegistry is a map construct that allows you to add a (static) resources.  As a minimum you need to specify:

  • the type of resource - this determines the collection to which you want to add the resource
  • the resource data - usually a url or relative path, but this could also be a custom object that needs to be converted to JSON
  • the resource location - specifying if the data is an absolute url, relative path or something else

Start by extending the layout beans and register the resources accordingly:

across.demo.catalog.application.ui.MainTemplate
@Component
public class MainTemplate extends LayoutTemplateProcessorAdapterBean {
    public static final String NAME = "MainTemplate";

	...

    @Override
    protected void registerWebResources(WebResourceRegistry registry) {
        registry.add(WebResource.CSS, "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css", WebResource.EXTERNAL);
        registry.add(WebResource.CSS, "/static/catalog/css/catalog.css", WebResource.VIEWS);
        registry.add(WebResource.JAVASCRIPT_PAGE_END, "https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js", WebResource.EXTERNAL);
        registry.add(WebResource.JAVASCRIPT_PAGE_END, "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js", WebResource.EXTERNAL);
    }
}


across.demo.catalog.application.ui.ErrorTemplate
@Component
public class ErrorTemplate extends LayoutTemplateProcessorAdapterBean {
    public static final String NAME = "ErrorTemplate";

    public ErrorTemplate() {
        super(NAME, "th/catalog/layouts/error");
    }

    @Override
    protected void registerWebResources(WebResourceRegistry registry) {
        registry.add(WebResource.CSS, "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css", WebResource.EXTERNAL);
        registry.add(WebResource.CSS, "/static/catalog/css/catalog.css", WebResource.VIEWS);
    }
}

Once resources have been added to the WebResourceRegistry they still need to be rendered in your templates.  The registry is available as a request attribute called webResourceRegistry.  Modify both main.thtml and error.thtml as in the example below, rendering the web resources collections.

classpath:views/th/catalog/layouts/main.thtml
<html>
<head>
    <title th:replace="${childPage} :: title">page specific title</title>

    <link rel="stylesheet" type="text/css" th:each="css : ${webResourceRegistry.getResources('css')}" th:href="@{${css.data}}"/>
</head>
<body>

<div class="container">
	...
</div>

<script th:each="javascript : ${webResourceRegistry.getResources('javascript-page-end')}" th:src="@{${javascript.data}}" th:if="${javascript.location != 'inline'}"></script>

</body>
</html>

If you compare the original template versus the Java registration of resources, you will notice that we no longer specify the /across/resources prefix on our catalog.css in Java code.  That is because the location WebResource.VIEWS specifies that it is a static resource according to the Across conventions, which will be served under the static path of the AcrossWebModule.  This is handy as the static resources path is configurable on the AcrossWebModule.

The initial WebResourceRegistry is filled by our template bean in this example, but it can also be wired as an argument on a @RequestMapping or @ModelAttribute method.  This allows request specific methods to add static resources, without the template having to contain all possible options.  Multiple beans can add resources to the registry, independently of each other.  It is also very important that this independence is respected, as there you cannot rely on the order of resources specified previously by a different component.

Creating a WebResourcePackage

$ git checkout c55f97a

Even though we're now using a WebResourceRegistry we just replaced the resource duplication from the Thymeleaf templates to the template beans.  One way would be to introduce inheritance between the two template beans, but we can also use a WebResourcePackage.  A WebResourcePackage is a nothing but a bundle of web resources that is known under unique name.  Said bundle is added to a WebResourceRegistry by specifying its name, after which the seperate web resources it contains will be added to the registry.

For that to be possible, the WebResourcePackage must be registerd in the WebResourcePackageManager first.

Let's create a separate CatalogWebResources bean that holds the necessary static resources we need.  In development mode we will return non-minified versions of all resources.  This logic is now contained in our WebResourcePackage, neither template nor registry is aware of the different resources depending on the application environment.  Furthermore, we could unit test the behaviour if we so wanted.  Using WebResourcePackage instances is also the easiest way to define dependencies between resources.

across.demo.catalog.application.ui.CatalogWebResources
@Component
public class CatalogWebResources extends SimpleWebResourcePackage {
    public static final String NAME = "CatalogWebResources";

    @Autowired
    public CatalogWebResources(AcrossDevelopmentMode developmentMode) {
        String minified = developmentMode.isActive() ? "" : ".min";

        List<WebResource> webResources = new ArrayList<>();
        webResources.add(new WebResource(
                CSS,
                "bootstrap-css",
                "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap" + minified + ".css",
                EXTERNAL
        ));
        webResources.add(new WebResource(
                CSS,
                "catalog-css",
                "/static/catalog/css/catalog.css",
                VIEWS
        ));
        webResources.add(new WebResource(
                JAVASCRIPT_PAGE_END,
                "jquery",
                "https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery" + minified + ".js",
                EXTERNAL
        ));
        webResources.add(new WebResource(
                JAVASCRIPT_PAGE_END,
                "boostrap-js",
                "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap" + minified + ".js",
                EXTERNAL
        ));

        setWebResources(webResources);
    }

    @Autowired
    public void registerInPackageManager(WebResourcePackageManager packageManager) {
        packageManager.register(NAME, this);
    }
}

Once the WebResourcePackage is defined, we can simplify the template beans:

across.demo.catalog.application.ui.MainTemplate
@Component
public class MainTemplate extends LayoutTemplateProcessorAdapterBean {
    public static final String NAME = "MainTemplate";

	...

    @Override
    protected void registerWebResources(WebResourceRegistry registry) {
        registry.addPackage(CatalogWebResources.NAME);
    }
}


Using a Menu to build the navigation

$ git checkout f8d0da7

Every controller that uses a certain template needs to make sure that all required metadata for the template is present.  In our current implementation that means the list of categories is available as a categories model attribute.  We currently set it in both HomepageController and CategoryController.  Let's remove some duplication here.

As usual there's more than one way to go about this.  We could manually add the attribute in the template bean, or use a @ControllerAdvice to set a general model attribute.  AcrossWebModule gives us another construct however, specifically aimed at building tree-based navigation structures that have a single leaf selected.

We can have our template bean create a named Menu.  This will publish an event that any other bean can handle in order to add items to the menu.  Just as with building the WebResourceRegistry, adding menu items can be done by independent components, without any idea of the order of the calls.  In order to build a safe hierarchical structure, items are registered using a unique hierarchical path as we will see below.

Start by building a menu named navigationMenu in our MainTemplate.  Do this by implementing the buildMenus(MenuFactory) adapter method:

across.demo.catalog.application.ui.MainTemplate
@Component
public class MainTemplate extends LayoutTemplateProcessorAdapterBean {
    public static final String NAME = "MainTemplate";

	...


    @Override
    protected void buildMenus(MenuFactory menuFactory) {
        menuFactory.buildMenu("navigationMenu");
    }
}

This will publish a BuildMenuEvent with the name navigationMenu.  Any bean - from any possible AcrossModule - can listen for this event.  This means that every separate controller could register its own navigational items for example.  In our example, let's create a separate component called NavigationMenuBuilder that is responsible for building the top navigation menu. 

across.demo.catalog.application.ui.NavigationMenuBuilder
@Component
public class NavigationMenuBuilder {
    private final MessageSource messageSource;

    @Autowired
    public NavigationMenuBuilder(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    @Event
    public void buildMainMenu(@EventName("navigationMenu") BuildMenuEvent<Menu> menuEvent) {
        menuEvent.builder()
                .item("/home", message("nav.home"), "/").and()
                .group("/category", message("nav.browse"));

        registerCategories(menuEvent.builder());
    }

    private void registerCategories(PathBasedMenuBuilder builder) {
        Category.LIST.forEach(c -> builder.item("/category/" + c.getPath(), c.getName()));
    }

    private String message(String messageCode) {
        return messageSource.getMessage(messageCode, null, LocaleContextHolder.getLocale());
    }
}

Notice the @Event annotated method.  The annotation specified that the method is an event listener, the method parameter specified the type of event we're listening to, in this case BuildMenuEvent.  Because our navigation menu does not implement a specific type, the generic type of the event is just Menu.  Because I am only interested in handling the navigation menu event, I specify @EventName("navigationMenu") limiting my handler to events with both the type and the event name.

Instead of directly modifying the Menu, we use the builder() component.  The latter is much more flexible for independent extending of the menu, using a unique canonical path for menu items.  When specifying a menu item, we usually provide three arguments:

  • the canonical path
  • the titel of the menu item
  • the url - if omitted the path will be used as url property as well

In our example above, we specify one group item.  This is a special demarcation for an item that contains only childen but is not an item itself.  Usually that means the group has a label but is itself not clickable.  In our case the Browse button triggers the dropdown menu, but only its children navigate to a category.

Building a menu using the MenuFactory will also make it available as request attribute using the name specified. All that's left to do is to modify main.thtml to render the top navigation using the navigationMenu.

classpath:views/th/catalog/layouts/main.thtml
<html>
<head>
    <title th:replace="${childPage} :: title">page specific title</title>

    <link rel="stylesheet" type="text/css" th:each="css : ${webResourceRegistry.getResources('css')}"
          th:href="@{${css.data}}"/>
</head>
<body>

<div class="container">
    <nav class="navbar navbar-default">
        <div class="navbar-header">
            <div class="logo">&nbsp;</div>
            <ul class="nav navbar-nav" th:inline="text">
                <li th:each="item : ${navigationMenu.items}"
                    th:class="${item.group} ? dropdown"
                    th:classappend="${item.selected} ? active">
                    <a th:unless="${item.group}" th:href="@{${item.url}}">[[${item.title}]]</a>
                    <a th:if="${item.group}" href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
                       aria-haspopup="true" aria-expanded="false">[[${item.title}]] <span class="caret"></span></a>

                    <ul th:if="${item.group}" class="dropdown-menu">
                        <li th:each="subItem: ${item.items}"
                            th:classappend="${subItem.selected} ? active">
                            <a th:href="@{${subItem.url}}">[[${subItem.title}]]</a>
                        </li>
                    </ul>
                </li>
            </ul>
        </div>
    </nav>
    <div th:replace="${childPage} :: content">
        here comes the body content
    </div>
</div>


<script th:each="javascript : ${webResourceRegistry.getResources('javascript-page-end')}" th:src="@{${javascript.data}}"
        th:if="${javascript.location != 'inline'}"></script>

</body>
</html>

When refreshing the page, the top navigation should still work.  But there's a couple of things to notice:

The structure we used to render is hierarchical but the builder we used was not

When building the menu, the items we specified by canonical path are actually converted into a tree structure.  Based on the example above, the resulting menu is:

  • /home
  • /category
    • /category/tv
    • /category/radio

The correct menu item is selected even though we did not specify any item to select

Menu can have a single item selected.  Selecting an item will also make all its parents selected.  This means there is something as a top-down selected path you can take when navigating a menu.  This is very useful to render for example a breadcrumb navigation.

When using the MenuFactory to create a menu, a default RequestMenuSelector will be used to determine which item in the menu should be selected.  It does so by inspecting the current request and selecting the most specific item that matches the request url with its own url.  Unless you are using very specific and variable urls, this usually works out of the box.  If not there are plenty ways to customize how the request should be match, and if you need to use a different selector you can set through the BuildMenuEvent.

Ordering menu items

Last thing to notice is that our menu items are not ordered correctly.  That is unless they are explicitly ordered, they will be sorted alphabetically.  Having the categories sorted alphabetically is just fine, but the main menu items we'd like in a fix order, so let's specify one.

across.demo.catalog.application.ui.NavigationMenuBuilder
@Component
public class NavigationMenuBuilder {
    private final MessageSource messageSource;

    @Event
    public void buildMainMenu(@EventName("navigationMenu") BuildMenuEvent<Menu> menuEvent) {
        menuEvent.builder()
                .item("/home", message("nav.home"), "/").order(0).and()
                .group("/category", message("nav.browse")).order(1);

        registerCategories(menuEvent.builder());
    }

	...
}

It is now no longer necessary to have the categories model attribute and you can remove it from HomepageController.

Updated HomepageController
@Controller
public class HomepageController {

    @RequestMapping("/")
    public String showHomepage() {
        return "th/catalog/homepage";
    }
}

Changing the left navigation on CategoryController

We still can't remove the model attribute from CategoryController, as the left navigation uses the categories as well.  However, nothing stops us to use the same navigationMenu to generate the left nav.  We only need to access the sub-tree of category items:

classpath:views/th/catalog/categoryItems.thtml
<html>
<head>
    <title th:inline="text">Category: [[${selectedCategory.name}]]</title>
</head>
<body>
<div class="container">
    <div th:fragment="content" class="row">
        <div class="col-md-2">
            <ul class="nav nav-pills nav-stacked">
                <li th:each="categoryItem : ${navigationMenu.getItemWithPath('/category').items}"
                    th:classappend="${categoryItem.selected} ? active">
                    <a th:href="@{${categoryItem.url}}" href="#"
                       th:text="${categoryItem.title}">category name</a>
                </li>
            </ul>
        </div>
		...
    </div>
</div>

</body>
</html>

And that allows us to remove the model attribute from the CategoryController as well.

Updated CategoryController
@Controller
public class CategoryController {

    @RequestMapping("/category/{categoryPath}")
    public String listCategoryItems(@PathVariable("categoryPath") Category selectedCategory, Model model) {
        if (selectedCategory == null) {
            throw new IllegalArgumentException("Illegal category requested.");
        }

        model.addAttribute("selectedCategory", selectedCategory);

        return "th/catalog/categoryItems";
    }
}

Adding an extra menu item

As a final example, let's add a separate menu item that actually points to an external website.  All we have to do to make it work is add it to the menu builder, our template already supports it.

across.demo.catalog.application.ui.NavigationMenuBuilder
@Component
public class NavigationMenuBuilder {
    private final MessageSource messageSource;

    @Event
    public void buildMainMenu(@EventName("navigationMenu") BuildMenuEvent<Menu> menuEvent) {
        menuEvent.builder()
                .item("/home", message("nav.home"), "/").order(0).and()
                .group("/category", message("nav.browse")).order(1).and()
                .item("/search", message("nav.search"), "http://www.google.com").order(3);

        registerCategories(menuEvent.builder());
    }

	...
}

As stated, extending the menu can be done through any even handler, even handlers from other modules.  You will see plenty examples of this approach when using AdminWebModule or DebugWebModule, and in the final part of this primer we'll do exactly the same thing when creating multiple applications.

Wrapping up

That wraps up this part of the primer series.  We mainly zoomed in on some web templating features that AcrossWebModule brings to the game.  We showed how to create layouts and link backend code with Thymeleaf template files.  Additionally we've seen how constructs like WebResourceRegistry and Menu can help you to dynamically configure templates from Java code.

In the next part of the series we'll continue extending our example by switching to JPA entities and building a basic administration UI.