Component-Led Design Patterns with Nunjucks & Grunt

Avatar of Morgan feeney
Morgan feeney on (Updated on )

Recently I was involved with the creation of an in-house system for building HTML prototypes intended to act as a reference point for an eCommerce CMS called Hybris. It started out simple, with PHP. As the team grew, so did the codebase, this eventually made things untidy, giving me sleepless nights.

We used PHP at first because:

  1. Includes. PHP include()s allow reusable chunks of code to be created. For example: headers, footers, and components.
  2. Familiarity. candidates were being recruited with prior knowledge of PHP & content management systems such as WordPress.
  3. Logic. Like if () {} else {}. People could take advantage of logical features, or, choose to write plain HTML, accommodating people of different skills.
  4. Simplicity. Simple enough to enable our journey.

Since the beginning, we strove for an effective componentized system. We looked for influence from systems such as Atomic Design and how templating is achieved by CMSs like WordPress and Magento.

Design Patterns with PHP

We devised a way to split up bits and pieces of code. We called them:

  • Templates
  • Components
  • Fragments

Templates were web pages based on a boilerplate PHP file. They looked like this:

<?php 
  require_once('../../_assets/classes/Prototype.class.php');
  $pageTitle = 'Boilerplate template | HTML';
  Prototype::getComponent('shared/head');
?>

<body>
  <?php Prototype::getComponent('shared/simple-header'); ?>
  <div id="main-content" class="container-outer">
    <div class="container-set-width">
      <div class="container-main">
          <!-- ADD YOUR STUFF HERE -->
      </div>
    </div>
  </div>
  <?php Prototype::getComponent('shared/footer'); ?>
  <?php Prototype::getComponent('shared/scripts'); ?>
</body>
</html>

Components were things like forms, carousels, and bigger chunks of reusable code. Ideally components were self contained and able to be placed anywhere without worry of CSS inheritance affecting the appearance unintentionally.

Fragments were anything smaller than a component, such as a product image or a single product including the price and product title.

What PHP Lacked

The main thing lacking from our PHP prototyping system was template inheritance. This led to the codebase growing undesirably. There were things lurking around which were no longer of any use and many duplicate files. It was also a nasty experience to search and find things.

PHP served its purpose at first, but eventually we superseded it with Nunjucks. Nunjucks enabled a different kind of system.

Reasons We Changed from PHP to Nunjucks

  1. Template inheritance. Reduces code duplication and allows us to maintain less code.
  2. Functionality. Nunjucks beat out other template languages that were tried (such as Handlebars) due to built-in features like includes, filters, and macros.
  3. Documentation. The documentation is very useful. It helps make Nunjucks easy to get your head around and provides working examples.
  4. Deliverables. Could be compiled as HTML using Grunt. We found doing this with PHP problematic.
  5. Logic. We could still accommodate people of different skillsets, as you can still write logical code or plain HTML.
  6. Simplicity.

Design Patterns with Nunjucks

  1. Layouts
  2. Templates
  3. Components
  4. Partials
  5. Macros

Comparing to the previous list using PHP, you’ll notice that layouts and macros are new here. Partials is another word for fragments.

Layouts are boilerplate HTML files, like templates, but this time there is a single file used as a base as opposed to a duplicate each time. How? Template inheritance. To quote the Nunjucks docs:

Template inheritance is a way to make it easy to reuse templates. When writing a template, you can define “blocks” that child templates can override. The inheritance chain can be as long as you like.

Here’s an example Nunjucks layout file:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>{{ page_title }}</title>
    <meta name="description" content="{{ page_description }}">
    <link rel="stylesheet" href="{{ base_path }}css/style.css">
    <meta name="viewport" content="width=device-width, initial scale=1">
  </head>
  <body class="{{ body_classes }}">
  <article>
    <nav id="main-nav" class="container-fluid navbar navbar-dark bg-inverse {{ main_nav_classes }}">
      <div class="container">
        {% block nav %}
          {% include "components/nav.njk" %}
        {% endblock %}
      </div>
    </nav>
    <header id="main-header" class="container-fluid {{ main_header_classes }}">
      <div class="container">
        {% block header %}
          {% include "components/header.njk" %}
        {% endblock %}
      </div>
    </header>
    <section id="main-section" class="container-fluid {{ main_section_classes }}">
      <div class="container">
        {% block main %}
        <!-- This block (and any other block) can be replaced using template inheritance, or leave the default content in -->
          <h2>This `<h2>` tag appears by default, yet can be replaced</h2>
          <p class="lead">
            This `<p>` tag is also a default but can be replaced.
          </p>
        {% endblock %}
      </div>
    </section>
    <footer id="main-footer" class="container-fluid m-t-3 {{ main_footer_classes }}">
      <div class="container">
        {% block footer %}
          {% include "components/footer.njk" %}
        {% endblock %}
      </div>
    </footer>
  </article>
  {% block footer_scripts %}
    {% include "partials/footer-scripts.njk" %}
  {% endblock %}
  </body>
</html>

Nunjucks and Grunt Suit the Project

Our project requirements are specific. They demand certain things. For example, one of the business requirements is that we hand-over HTML files to off-shore developers for implementation to some Java template files.

While we were still using PHP, we used a couple of Grunt plugins: grunt-php-2-html & grunt-prettify to automate this process. However, the structure of the project could often lead to issues with variable scope. This lead to things like individual component HTML being compiled with PHP errors embedded. Not ideal! You’d then open the file and manually remove the error, and then it’s no longer automated, or you’d end up working out why your variable scope is all screwed up instead of getting on with your work. It’s a process which shouldn’t require much thought once implemented.

Fortunately, using grunt-nunjucks-2-html means all files are compiled to HTML from the start. In order to see the front end at all, you need to view a `.html` file. We essentially use Nunjucks as a static site generator. The `.njk` files get ignored and are only used for compiling HTML files such as components for handing over or complete web pages for viewing. The language and output suit the needs of the project much better than the old PHP system.

Using Nunjucks to Ensure Code Consistency

There’s another aspect to our project that doesn’t have anything to do with Nunjucks, but it has everything to do with design patterns.

We use the Bootstrap framework (v3.3.4 to be exact). When using a framework like Bootstrap in a team there can be issues with code quality. There shouldn’t be, but sometimes conventions aren’t always followed, right? Bootstrap is a great resource for particular use cases, but not every use case is the same. The docs are excellent, there are plenty of examples, but sometimes you can run into trouble when you work with others and the framework is used differently by different people.

If you want to avoid this, you can use another great feature of Nunjucks: the Macro. To quote Nunjucks:

Macros allows you to define reusable chunks of content. It is similar to a function in a programming language.

Here’s a Nunjucks macro example:

{% macro field(name, value='', type='text') %}
  <div class="field">
    <input type="{{ type }}" name="{{ name }}" value="{{ value | escape }}" />
  </div>
{% endmacro %}

Now field is available to be called like a normal function:

{{ field('user') }}
{{ field('pass', type='password') }}

We can take some of the reusable parts of Bootstrap and package them up into reusable, configurable chunks of code called, for the sake of this article, Macros.

Here’s an example of a Bootstrap card Macro:

{% macro cardMacro (image, title, text, btnText="Button") %}
  <div class="card">
    <img class="card-img-top img-fluid" src="{{ image }}" alt="{{ title }}">
    <div class="card-block">
      <h4 class="card-title">{{ title }}</h4>
      <p class="card-text">{{ text }}</p>
      <a href="#" class="btn btn-primary">{{ btnText }}</a>
    </div>
  </div>
{% endmacro %}

The code is exactly the same each time it’s used. It’s the equivalent of a Sass/Less mixin.

One benefit of using Macros like this is that the code will always be consistent no matter who uses it. It’s a quick win for standards adherence. Another is that you’ll only have to update the macro in a single place if the code needs updating later.

Componentization of HTML for use with CSS frameworks.

Want to learn more? If you’ve made it this far, then perhaps you have a similar requirement and would like to find out more about how you can make use of Nunjucks and Grunt.

I’ve setup a repo called: Bootstrap Patterns with all the code used in this article, plus a heap more to get you started. I’ve added what you will need to pull down the required packages. As we’re using Grunt, you’ll need to have Node running on your machine. You should have basic knowledge of using a task runner and NPM.

What’s in the repo?

Working examples of how Nunjucks, Grunt & a CSS framework (Bootstrap in this case), can be combined. It demonstrates how the same reusable design patterns can be combined to achieve multiple variations.

Let’s look at each of the design pattern levels

1) Layouts

The file `layouts/layout.njk` is comprised of a reusable page structure. I’ve anticipated all the usual things I need to create web pages using Bootstrap. There are variables for things like meta information, e.g.

<title>{{ page_title }}</title>

Variable classes such as:

<body class="{{ body_classes }}">

Generic Bootstrap classes required for layout such as .container and .container-fluid added to elements such as nav, header, section, footer, along with Nunjucks variables, blocks, and includes; to enable total flexibility:

<header id="main-header" class="container-fluid {{ main_header_classes }}">
  <div class="container">
    {% block header %}
      {% include "components/header.njk" %}
    {% endblock %}
  </div>
</header>

2) Components

In the file: `components/nav.njk` the following Nunjucks features are used in order to create a reusable pattern:

  • Import
  • For
  • Filter
  • Block
  • Macro
{% import "macros/macro-search.njk" as macroSearch %}

<div class="nav navbar-nav">
  {% for item in navItems %}
    <a class="nav-item nav-link" href="{{ item.menu_item | lower | replace(" ", "") }}.html">{{ item.menu_item }}</a>
  {% endfor %}
  {% block navRight %}
    {{ macroSearch.search() }}
  {% endblock %}
</div>

This file demonstrates how several features of Nunjucks can be combined. I created a macro: `macros/macro-search.njk` which we import into `components/nav.njk`, as we want to add a search form in the nav.

{% import "macros/macro-search.njk" as macroSearch %}

We loop through some JSON data objects called navItems which are used to build the navigation, then use a filter to make the URL lowercase, and strip unwanted whitespace so the link is valid.

{% for item in navItems %}
  <a class="nav-item nav-link" href="{{ item.menu_item | lower | replace(" ", "") }}.html">{{ item.menu_item }}</a>
{% endfor %}

3) Macros

We call the search form macro in block tags just in case we don’t want the search form to appear in this component at some point later.

{% block navRight %}
  {{ macroSearch.search() }}
{% endblock %}

4) Templates

In templates we extend layouts like so:

{% extends "layouts/layout.njk" %}

This allows us to inherit the same layout many times without re-writing the code in more files.

…The nav component is then called into a block tag in the layout file:

<nav id="main-nav" class="container-fluid navbar navbar-dark bg-inverse {{ main_nav_classes }}">
  <div class="container">
    {% block nav %}
      {% include "components/nav.njk" %}
    {% endblock %}
  </div>
</nav>

I wrap nav in a block tag by default. This default can either be left, or changed via a template.

5) Partials

The only use I made of partials in this example was for some script tags in the footer. But as the name suggests, it’ a partial piece of code… it could be anything small.

{% block footer_scripts %}
  {% include "partials/footer-scripts.njk" %}
{% endblock %}

Finally

The examples I provide in this article are just scratching the surface of what can be achieved. There are more examples in the repo, so please do take a look, and feel free to let me know your thoughts.

I want to give a large shout out to the guys on the UI team at N.Brown Group PLC where these ideas came about, it’s been a blast!