Farewell to templates

Don't know about you but I've got tired of Nunjucks swallowing errors... And Nunjuck is the best JS templater out there.

So I will try to replace backend templates with virtual-dom which eventually will be turned to HTML (string) via vdom-to-html. I also will use hyperscript-helpers to improve readability.

You may think I'm crazy because templates are for backend and VDOM is only for frontend, but really... Let's try this and measure performance, compare readability. This should be fun enough. As a bonus, it's a part of the road to isomorphic app which all we are seeking for.

Static blocks

Ok. Let's get started. Static blocks are the easiest.

Nunjucks (Django, Jinja...)

<footer id="footer">
  <div class="container">
    <div class="row">
      <div class="col-sm-6">
        <ul class="nav nav-list nav-footer">
          <li><a href="/blog">Blog</a></li>
          <li><a href="/about">About</a></li>
          <li><a href="/links">Links</a></li>
        </ul>
      </div>
      <div class="col-sm-6">
        <p class="copyright"><a href="/">paqmind.com: 2016</a></p>
      </div>
    </div>
  </div>
</footer>

HyperScript

let HH = require("hyperscript-helpers")
let h = require("virtual-dom/h")

let {a, div, footer, li, p, ul} = HH(h)

export default function renderFooter()  {
  return footer("#footer", [
    div(".container", [
      div(".row", [
        div(".col-sm-6", [
          ul(".nav nav-list nav-footer", [
            li(a({href: "/blog"}, "Blog")),
            li(a({href: "/about"}, "About")),
            li(a({href: "/links"}, "Links")),
          ]),
        ]),
        div(".col-sm-6", [
          p("#copyright", [a({href: "/"}, "paqmind.com"), ": 2016"]),
        ]),
      ]),
    ]),
  ])
}

Well, that was quite easy. I would say that both snippets are equally readable. There are two main differences, obviously. First template is not vulnerable to coding errors. It may contain syntax errors (to some degree) and still being parsable. Second template is JS so it's not so forgiving. Instead it gets all the power of Turing complete language. We may apply functions, comment single lines with //, apply linters etc.

If your templates are created by designer I wouldn't recommend second one. However, if we're in that boat where programmer is responsible for such things... I would have thought for a moment.

But let's see what form the other goodies of Nunjucks will take.

Dynamic blocks

Next thing we want is variable rendering.

Nunjucks (Django, Jinja...)

<div>{{ sitename | default("...")) }}</div>

HyperScript

function renderDynamic(ctx={}) {
  return div([ctx.sitename || "..."])
}

A piece of cake for JS. By the way, I never really bought an argument that template language has to be specially limited. There is a business logic and a presentation logic. And the second one can be more complex than the first one in many cases. Guess what's the natural place for such logic? Right, it's a view layer, represented by templates or render functions in our case.

No struggling with half-baked filters. No controversial semantics to learn. No care about environment injections. No deals with caching layer. Sounds pretty good so far...

One thing worth mentioning is that due to js div(".myClass") shortcut syntax we were required to wrap node content into array (js [ctx.sitename || "..."]).

Inheritance

Now to the more complex stuff. How could one reimplement the coolest block + extends combo invented by Django team?

Nunjucks (Jinja, Django...)

<html>
<head>
  {% block head %}
    <title>{{ seotitle }}</title>
  {% endblock %}
</head>
<body>
  {% block scripts %}
    <script src="base.js"></script>
  {% endblock %}
</body>
</html>
{% extends "base.html" %}

{% block scripts %}
  {{ super() }}

  <script src="home.js"></script>
{% endblock %}

HyperScript

let {chain, identity} = require("ramda")

let unnest = chain(identity) // chain is a flatMap alias

function renderBase(ctx={}, cbs={}) {
  return html([
    head(unnest([
      (cbs.head || identity)([
        title(ctx.seotitle),
      ])
    ])),
    body(unnest([
      (cbs.scripts || identity)([
        script({src: "base.js"}),
      ])
    ])),
  ])
}

Inheritance itself is replaced by "parent" function application from "child" function. In this particular case home.html extends base.html which means renderHome will call renderBase.

As renderHome needs access to default value renderHome provide we implement this through a callback. We add a cbs, a dictionary of callbacks where every callback will be called with appropriate result (which is Nunjucks js {{ super() }} equivalent).

let {append} = require("ramda")

function renderHome(ctx={}, cbs={}) {
  return renderBody({}, {
    scripts: super_ => append(script({src: "home.js"}, super_))
  })
}

// can be simplified to
function renderHome(ctx={}, cbs={}) {
  return renderBody({}, {
    scripts: append(script({src: "home.js"}))
  })
}

The renderHome refactoring example is a self-speaking advertisement for currying.
Curried functions are boilerplate killers.

You may think it's a tiny bit complicated and I kinda agree but things like flatMap, identity etc. become a second nature with practice. It took a much longer time for me to describe all this, that to write a working version of code above with some manual tests.

Good to know that Ramda already provides the same unnest implementation so you don't need to declare that in your own code. After we get Proxy, we may go further and replace ctx={} and x || identity checks with a code like this:

// ES2016?
function renderBase(ctx={}, cbs=DefaultDict(identity)) {
  return html([
    head(unnest([
      cbs.head([
        title(ctx.seotitle),
      ])
    ])),
    body(unnest([
      cbs.scripts([
        script({src: "base.js"}),
      ])
    ])),
  ])
}

Here DefaultDict is a function returning a special dict where all values fallback to provided value (identity in our case) if not set. But for now Proxy is a draft.

Layout

One thing to add. As code you get from a base template is a normal data you can apply all magic of PL to it. How often you wanted to remove some unnecessary script (coming from a super call) from one exact page, keeping it in a parent template? The only way to do it in Nunjucks was manual HTML parsing. HTML parsing with regular expressions is brittle so I bet it was easier to remove that script from a parent template nevertheless and repeat it in every template except that one. It was as boring to write as it was to implement it.

Now you can just filter super_.children and remove that script. You just to be aware of VirtualDOM VNode structure and I assure you it's quite easy.

Macros

Macros can be replaced with functions. They generally require less variables than layouts so such function will probably benefit from curryable signatures. For example you'll probably want to define like renderAlert(category, message) rather than renderAlert(ctx). So you just put that functions into a different folder to emphasize their different "personality".

Let's compare the gain we're getting here.

Nunjucks (Jinja, Django...)

{% macro renderAlert(category, message) %}
  <div class="alert alert-block alert-{{ category }} fade in">
    <button class="close" data-dismiss="alert" type="button">&times;</button>
    <p>
      {{ message | safe }}
    </p>
  </div>
{% endmacro %}
<div class="page-alerts">
  {% if alerts.error %}
    {% for message in alerts.error %}
      {{ renderAlert("error", message) }}
    {% endfor %}
  {% endif %}

  {% if alerts.success %}
    {% for message in alerts.success %}
      {{ renderAlert("success", message) }}
    {% endfor %}
  {% endif %}

  {% if alerts.warning %}
    {% for message in alerts.warning %}
      {{ renderAlert("warning", message) }}
    {% endfor %}
  {% endif %}

  {% if alerts.info %}
    {% for message in alerts.info %}
      {{ renderAlert("info", message) }}
    {% endfor %}
  {% endif %}
</div>

HyperScript

let {decode, encode} = require("ent")

let renderAlert = curry((category, message) => {
  return div(".alert.alert-block.fade.in", {className: `alert-${category}`}, [
    button(".close", {"data-dismiss": "alert", type: "button"}, decode("&times;")),
    p(message | safe),
  ])
})
let renderAlerts = (alerts) => {
  return div(".page-alerts", [
    alerts.error   && map(renderAlert("error"),   alerts.error),
    alerts.success && map(renderAlert("success"), alerts.success),
    alerts.warning && map(renderAlert("warning"), alerts.warning),
    alerts.info    && map(renderAlert("info"),    alerts.info),
  ])
}

Whew! HyperScript clearly wins this round. The code is way shorter and cleaner. Another thing to notice here: we need to manually decode HTML entities in HyperScript.

Wrapping macro call style

{% call foo %}
  bar
{% endcall %}

can be substituted by foo({content: "bar"}) call. Nothing special. That's probably all I could that about macros.

Rendering

Wanna see a code of final rendering? It's offensively simple. For example, for Koa it may look like:

let toHTML = require("vdom-to-html")

app.context.render = function (pathToComponent, ctx={}) {
  let context = merge(ctx, {
    constants: constants,
    i18n: Globalize,
    session: this.session,
    alerts: this.alerts,
  })
  let vdom = require("shared/components/" + pathToComponent)(context)
  return toHTML(vdom)
}

app.context.renderBody = function (pathToComponent, ctx={}) {
  this.type = "html"
  this.body = this.render(pathToComponent, ctx)
  return this.body
}
router.get("feedback", "/feedback",
  function* (next) {
    // ...

    this.renderBody("detail/page.feedback", {form, formErrors})
    return yield* next
  }
)

Conclusion

So whether you like the final result or not, I hope it was interesting for you. Manual HTML to HyperScript translation quickly becomes a bottleneck. That's why I propose you to use a webservice. Elm already has a similar tool which gave us additional inspiration.