Templates
Djula - HTML markup
Djula is a port of Python’s Django template engine to Common Lisp. It has excellent documentation.
Install it if you didn’t already:
(ql:quickload "djula")
The Caveman framework uses it by default, but otherwise it is not difficult to setup. We must declare where our templates live with something like:
(djula:add-template-directory (asdf:system-relative-pathname "myproject" "templates/"))
and then we can declare and compile the ones we use, for example::
(defparameter +base.html+ (djula:compile-template* "base.html"))
(defparameter +products.html+ (djula:compile-template* "products.html"))
A Djula template looks like this:
{% extends "base.html" %}
{% block title %} Products page {% endblock %}
{% block content %}
<ul>
{% for product in products %}
<li><a href="{{ product.id }}">{{ product.name }}</a></li>
{% endfor %}
</ul>
{% endblock %}
This template actually inherits a first one, base.html
, which can be:
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
<title>{% block title %} My Lisp app {% endblock %}</title>
</head>
<body>
<div class="container">
{% block content %} {% endblock %}
</div>
</body>
</html>
This base template defines two blocks: one for the page title, one for
the page content. A template that wants to inherit this base template
will use {% extends "base.html" %}
and replace each blocks with {% block content %} … {‰ endblock %}
.
At last, to render the template, call djula:render-template*
inside a route.
(easy-routes:defroute root ("/" :method :get) ()
(djula:render-template* +products.html+
nil
:products (products)
Djula is, along with its companion access library, one of the most downloaded libraries of Quicklisp.
Djula filters
Filters are only waiting for the developers to define their own, so we should have a work about them.
They allow to modify how a variable is displayed. Djula comes with a good set of built-in filters and they are well documented. They are not to be confused with tags.
They look like this: {{ var | lower }}
, where lower
is an
existing filter, which renders the text into lowercase.
Filters sometimes take arguments. For example: {{ var | add:2 }}
calls
the add
filter with arguments var
and 2.
Moreover, it is very easy to define custom filters. All we have to do
is to use the def-filter
macro, which takes the variable as first
argument, and which can take more optional arguments.
Its general form is:
(def-filter :myfilter-name (var arg) ;; arg is optional
(body))
and it is used like this: {{ var | myfilter-name }}
.
Here’s how the add
filter is defined:
(def-filter :add (it n)
(+ it (parse-integer n)))
Once you have written a custom filter, you can use it right away throughout the application.
Filters are very handy to move non-trivial formatting or logic from the templates to the backend.
Spinneret - lispy templates
Spinneret is a “lispy” HTML5 generator. It looks like this:
(with-page (:title "Home page")
(:header
(:h1 "Home page"))
(:section
("~A, here is *your* shopping list: " *user-name*)
(:ol (dolist (item *shopping-list*)
(:li (1+ (random 10)) item))))
(:footer ("Last login: ~A" *last-login*)))
I find Spinneret easier to use than the more famous cl-who, but I personnally prefer to use HTML templates.
Spinneret has nice features under it sleeves:
- it warns on invalid tags and attributes
- it can automatically number headers, given their depth
- it pretty prints html per default, with control over line breaks
- it understands embedded markdown
- it can tell where in the document a generator function is (see
get-html-tag
)