HTML & Integrant
Compose a UI system with Integrant and render Hiccup.
Separate data, views and lifecycle
A Hiccup value describes HTML as vectors: [:h1 "Hello"] is a heading, and [:a {:href "/"} "Home"] is a link with attributes. replicant.string/render renders that data into a response body. Rendering a page does not start a server.
Integrant constructs components from a map and explicit ig/ref dependencies. Fast-Twitch still sees ordinary handler functions and route vectors. You can use Hiccup without Integrant, and neither requires a new routing abstraction.
A complete Ring application graph
(ns application.html
(:require [integrant.core :as ig]
[replicant.string :as html]
[fast-twitch.routing :as routing]))
(defmethod ig/init-key ::settings [_ settings] settings)
(defmethod ig/init-key ::pages [_ {:keys [settings]}]
{:home (fn [_]
{:status 200 :headers {"content-type" "text/html; charset=utf-8"}
:body (str "<!doctype html>"
(html/render
[:html {:lang "en"}
[:head [:meta {:charset "utf-8"}]
[:title (:title settings)]]
[:body [:h1 (:title settings)]
[:a {:href "/hello/Ada"} "Greet Ada"]]]))})
:hello (fn [{:keys [path-params]}]
{:status 200 :headers {"content-type" "text/html; charset=utf-8"}
:body (html/render [:h1 (str "Hello " (:name path-params))])})})
(defmethod ig/init-key ::app [_ {:keys [pages]}]
(routing/routes
[{:pattern "/" :method :get :handler (:home pages)}
{:pattern "/hello/:name" :method :get :handler (:hello pages)}]
(fn [_] (routing/not-found "Missing"))))
(def config
{::settings {:title "My application"}
::pages {:settings (ig/ref ::settings)}
::app {:pages (ig/ref ::pages)}})
(def system (ig/init config))
(def fetch-handler
(routing/ft-handler (::app system) {:protocol "HTTP/1.1" :remote-addr "unknown"}))
This constructs the Fetch handler; start it through routing/start-server! or use the complete runnable project, which includes dependencies, compiler configuration, tasks, views and startup code.
Read the graph in dependency order
ig/init first initializes settings, then passes that value to pages, then passes those handlers to app. The settings ref becomes the initialized value, not a lookup that each request performs. The page functions close over that component's settings. Routes remain visible together in the application component.
A response body is the rendered string, not the Hiccup vector. Set an HTML content type explicitly. Put user text in Hiccup text nodes so the renderer handles text escaping; concatenating untrusted strings into raw HTML has different semantics. Rendering is not validation of a route parameter's business meaning.
Own resources explicitly
If a component opens a database, socket or watcher, give it an ig/halt-key! method that releases that resource. Call ig/halt! on the initialized system at shutdown. Components containing only pure handler functions do not need invented lifecycle state. A server listener has its own shutdown operation; halt ordering should stop dependants before releasing resources they use.
What to change next
Add a page function, add one visible route pointing at it, and request that route. Add a setting to ::settings and use it in the view. Add middleware around routing/routes in ::app if parsed form data is needed. Form project · Upload project.