DEV Community

Colin Oakley
Colin Oakley

Posted on • Updated on

templating - from html to macros

Whenever I am pairing with other developers we always seem to hit a similar topic; abstraction.

There is ever a clear-cut answer to that question, as it requires the context of the code and the state of the application.

Overly abstracted code can make changing it difficult and hard to comprehend whereas no abstraction can lead to bloat and repetition.

Below is an example of a text input form GOV.UK Elements.

The examples below use nunjucks.

<div class="form-group">
    <label class="form-label" for="ni-number">
        National Insurance number
        <span class="form-hint">
            It's on your National Insurance card, benefit letter, payslip or P60.
      </span>
    </label>
    <input class="form-control" id="ni-number" type="text" name="ni-number">
</div>
Enter fullscreen mode Exit fullscreen mode

Form elements always seem a very clear example of where we should do an abstraction - they have set structure and usually are used multiple times on a site.

When we look at making a reusable form element, we do it with the composition of the makeup of our HTML in mind.

There are a couple ways we could break this out, the first would be so that each of the parts of the code would have its own macro - this would mean each part would be usable within other form elements.

This would look like this:

  • form-group wrapper
  • label
  • input

Using nunjucks we'd end up with something like this:

{% macro input(id, name, value='') %}
<input class="form-control" 
        id="{{ id }}" 
        name="{{ name }}"
        type="text"
        value="{{value}}">
{% endmacro %}

{% macro label(for, label, hint) %}
<label class="form-label" for="{{ for }}">
    {{ label }}
    <span class="form-hint">
        {{ hint }}
    </span>
</label>
{% endmacro %}

Enter fullscreen mode Exit fullscreen mode

This would allow us to compose our UI like this,

{% from "forms.html" import field, label %}

<div class="form-group">
    {{ label('ni-number', 
            'National Insurance number',
            'It\'s on your National Insurance card, bene...') }}

    {{ field('ni-number',
             'ni-number',
                niNumberValue) }}
</div>

Enter fullscreen mode Exit fullscreen mode

This allows label and field to be composed to produce other form elements, they could also be wrapped in another function to produce a form element, I'll come to this later. We could also look at moving the form-group div to a macro.

This could also be done as a single macro.

{% macro textInput(id, name, label, hint, value) %}
<div class="form-group">
    <label class="form-label" for="{{ id }}">
        {{ label }}
        <span class="form-hint">
            {{ hint }}
        </span>
    </label>
    <input class="form-control" 
        id="{{ id }}" 
        name="{{ name }}"
        type="text"
        value="{{value}}">
</div>
{% endmacro %}

Enter fullscreen mode Exit fullscreen mode

This encapsulates both the previous macros in one call, this makes it less composable but also has the full element.

The second method will quickly become bloated if we need to add on additional options for example styling, input type or additional HTML properties.

We could also make a hybrid of the two methods, though as we start to do multiple layers we start to make our code less readable and we need to be more mindful when we make changes.

{% macro textInput(id, name, label, hint, value) %}
<div class="form-group">
    {{ label(id, 
            label,
            hint) }}

    <input class="form-control" 
        id="{{ id }}" 
        name="{{ name }}"
        type="text"
        value="{{value}}">
</div>
{% endmacro %}

Enter fullscreen mode Exit fullscreen mode

This in hindsight feels like the right balance, textInput is encapsulated but the label can be re-used across different macros.

My final macro has the following inputs

Name Description
name sets the name of the input
id sets the id of the input, and the for of the label
label sets the text of the label
hint sets hint text within the label
value sets the value of the input
error sets the error message
maxlength sets the max length of the input
classes object used for styling elements

This has various states, including optional fields and error states. This after a couple of iterations seemed like the right level of abstraction in the application context, though I think there is more to do with moving the label to its own macro and being called within the element.

{% macro textInput(name, id, label, hint, value, error, maxlength, classes) %}
    {% if not id %}
        {% set id="input-" + name %}
    {% endif %}

    <div id="{{id}}-form" class="form-group{% if error %} form-group-error{% endif %}">
        <label for="{{ id }}">
            <span class="{% if classes.label %}{{ classes.label }}{% else %}form-label-bold{% endif %}">{{ label }}</span>
            {% if hint %}<span class="form-hint">{{hint}}</span>{% endif %}
            {% if error %}<span class="error-message">{{error}}</span> {% endif %}
        </label>
        <input
            class="form-control{% if classes.input %} {{ classes.input }}{% endif %}"
            id="{{ id }}"
            {% if maxlength %} maxlength="{{maxlength}}"{% endif %}
            name="{{name}}"
            type="text"
            value="{{value}}">
</div>
Enter fullscreen mode Exit fullscreen mode

Whenever you make a deliberate decision to try and make re-useable code there is always going to be a trade-off in functionality, readability, and maintenance.

Keeping things self-contained gives the ability to refactor it later with minimal change.

Feel free to add how you'd do this, or message me on twitter

Top comments (0)