-
Notifications
You must be signed in to change notification settings - Fork 18
Inline partial definitions #68
Description
The most underrated features of Go's templating system are the define and block directives.
define enables template authors to define new named templates within an existing template file, which is nice for small snippets that are related to the main template but you don't want to put them in their own file. When you render the main template, any inline defines are skipped. Then it can be invoked by name like any other template.
block is like a define + an invocation of the newly defined template in the main template. This is super handy, and allows authors to write up a whole html document in a single file while slicing out sections of it into small reusable snippets.
The combination of these two features pairs surprisingly well with htmx.
You can see an example of this use case in xrss, a toy rss reader I built with go templates and htmx.
1. Here's a somewhat large snippet of the project to demonstrate the idea of defining partials inline. I can write the entire page as if it were a single html document, and surround any given section for reuse later.
The nice thing about this is that everything stays in context and isn't unnecessarily broken up across dozens of files.
I'm also doing some other funny things like extracting url params, launching sql queries etc directly in the template, which is... lets say "unconventional". But don't get distracted, I'm talking about partials here. 😆
{{- block "items-panel" .}}
{{- $feed_id := int (.Params.ByName "feed_id") }}
<div id="items-panel" class="md:flex-1 shrink-0 snap-start w-full h-full min-w-0 overflow-y-auto" style="scrollbar-gutter: stable;">
<div class="sticky top-0 z-20 flex w-full gap-2 p-2 pb-2 bg-white">
<span
class="md:hidden h-full p-3 text-lg text-gray-600 bg-gray-100 rounded-lg scale-x-[-1]"
onclick="document.getElementById('feeds-panel').scrollIntoView({behavior:'smooth'})"> ⇌ </span>
<span class="p-3 text-lg text-center text-gray-600 bg-gray-100 rounded-lg">
{{.QueryVal `SELECT COALESCE((SELECT title FROM v_feed WHERE id=$1), 'All Feeds')` $feed_id}}
</span>
{{- if ne $feed_id 0}}
{{- if not .Config.read_only}}
<a href="" hx-post="/feeds/{{$feed_id}}/refresh" hx-target="#items-panel" hx-swap="outerHTML scroll:top"
class="p-3 text-lg text-center text-gray-600 bg-gray-100 rounded-lg">⟳</a>
{{- end}}
{{- end}}
</div>
<ul id="items" class="p-2">
{{- block "items" .}}
{{- $feed_id := int (.Params.ByName "feed_id") }}
{{- $offset := int (.Req.URL.Query.Get "offset") }}
{{- $count := .QueryVal `SELECT COUNT(*) FROM v_item WHERE feed_id=$1 OR $1=0` $feed_id}}
{{- range .QueryRows `SELECT feed_id, id, feed_title, title, description, image, author, published FROM v_item WHERE feed_id=$1 OR $1=0 ORDER BY published DESC LIMIT 10 OFFSET $2` $feed_id $offset }}
{{- block "item" .}}
<li class="odd:bg-gray-100 max-h-7.5em h-[7.5em] flex flex-row gap-2 p-2 cursor-pointer transition" hx-get="/feeds/{{.feed_id}}/items/{{.id}}/expanded" hx-swap="outerHTML transition:true">
<div class="flex-none w-[6em] h-[6em] m-1 bg-cover rounded" style="background-image: url('{{.image}}');"></div>
<div class="items-baseline flow-root overflow-hidden">
<div class="xl:flex-row flex flex-col">
<div class="flex-initial pr-2 text-lg font-semibold truncate">{{.title | sanitizeHtml "strict" }}</div>
<div class="flex flex-row items-baseline flex-none">
<div class="flex-none pl-1 pr-1 text-sm text-gray-800">{{.feed_title | sanitizeHtml "strict" }}</div>·
<div class="flex-none pl-1 pr-1 text-sm text-gray-800">{{.author | sanitizeHtml "strict" }}</div>·
<div class="group/tip relative flex-none pl-1 text-sm text-gray-800">
<time datetime="{{.published}}">{{.published | humanize "time:2006-01-02T15:04:05Z07:00"}}</time>
<span class="group-hover/tip:visible left-full top-full mr-[-999px] -translate-x-full absolute invisible p-1 text-gray-200 bg-gray-800 rounded shadow-lg">
{{- .published | toDate "2006-01-02T15:04:05Z07:00" | date "Monday, 02 Jan 2006 15:04:05 MST" -}}
</span>
</div>
</div>
</div>
<div class="line-clamp-2 xl:line-clamp-3 text-gray-500">{{.description | abbrev 4000 | sanitizeHtml "strict"}}</div>
</div>
</li>
{{- end}}
{{- end}}
{{- $next_offset := add $offset 10}}
{{- if lt $next_offset $count}}
<li hx-trigger="intersect" hx-swap="outerHTML"
hx-get="{{if $feed_id }}/feeds/{{$feed_id}}{{end}}/items?offset={{ $next_offset }}">(loading...)</li>
{{- end}}
{{- end}}
</ul>
</div>
{{- end}}2. Then later I reuse the partials in other routes defined inline:
Go doesn't combine templates and routing, so this {{define "<url pattern>"}} is my own addition that exploits the fact that the names of inline definitions can be arbitrary strings; on load I search all the defined templates for those with names that are a valid url path pattern and add it to the router. (We can talk about this later, one thing at a time. 😉)
This is also nice because it means I can have all the html for a given page and dozens of little http path handlers needed to make htmx work all defined in ONE file with only a few lines each and zero boilerplate.
{{- define "GET /feeds/:feed_id/"}}
{{- if (.Req.Header.Get "HX-Request")}}
{{- template "items-panel" .}}
{{- else}}
{{- template "/index.html" .}}
{{- end}}
{{- end}}
{{- define "GET /feeds/:feed_id/items"}}
{{- template "items" .}}
{{- template "perf" .}}
{{- end}}
{{- define "GET /feeds/:feed_id/items/:item_id/"}}
{{- $item := .QueryRow `SELECT feed_id, id, feed_title, title, description, image, author, published FROM v_item WHERE feed_id=$1 AND id=$2` (.Params.ByName "feed_id") (.Params.ByName "item_id") }}
{{- template "item" $item}}
{{- end}}Would you consider adding something akin to go templates' define and block in zmpl?