Todo

A simple Todo application that demonstrates the capabilities of GoHT.

Source Code: github.com/stackus/goht-todos

GoHT Todo GoHT Todo

HTMX

This application also makes use of HTMX to provide a more interactive experience. HTMX is a library that allows you to create web applications with less JavaScript. It uses HTML attributes to define the behavior of the application.

The Todo examples use project-specific domain types plus HTMX, HyperScript, SortableJS, Chi, and small application helpers.

Page Layouts

Each page will be wrapped with the following main layout:

package shared

@haml Page(title string) {
	!!!
	%html.h-full{lang: "en"}
		%head
			%meta{charset: "UTF-8"}
			%title= title
			%link{rel: "icon", type: "image/svg+xml", href: "/dist/favicon.svg"}
			%meta{content: "width=device-width, initial-scale=1", name: "viewport"}
			%meta{content: "index, follow", name: "robots"}
			%meta{content: "7 days", name: "revisit-after"}
			%meta{content: "English", name: "language"}
			%script{src: "https://unpkg.com/htmx.org@1.9.10", integrity: "sha384-...", crossorigin: "anonymous"}
			%script{src: "https://unpkg.com/hyperscript.org@0.9.12"}
			%script{src: "https://unpkg.com/sortablejs@1.15.0"}
			%script{src: "/dist/app.js"}
			%link{rel: "stylesheet", href: "/dist/styles.css"}
		%body.h-full.bg-yellow-50.font-mono
			%section.max-w-lg.mx-auto.my-2
				%h1.text-8xl.font-black.text-center.m-0.pb-2 Todos
				=@children
}

Using =@children makes it straightforward to build nested layouts. Here is an example of one of the pages:

package pages

import (
	"github.com/stackus/goht-todos/domain"
	"github.com/stackus/goht-todos/templates/partials"
	"github.com/stackus/goht-todos/templates/shared"
)

@haml HomePage(todos []domain.Todo) {
	=@render shared.Page("Home")
		=@render partials.Search("")
		=@render partials.RenderTodos(todos)
		=@render partials.AddTodoForm()
}

Each page in the application makes use of the base template and will then include additional content which is rendered by the before mentioned =@children directive.

Taking a look at the partials.AddTodoForm template:

package partials

@haml AddTodoForm() {
	%form.inline{
		method: "POST",
		action: "/todos",
		hx-post: "/todos",
		hx-target: "#no-todos",
		hx-swap: "beforebegin",
	}
		%label.flex.items-center
			%span.text-lg.font-bold Add Todo
			%input.ml-2.grow{
				type: "text",
				name: "description",
				_: "on keyup if the event's key is 'Enter' set my value to '' trigger keyup"
			}
}

Being able to put the attributes across multiple lines makes it easier to read and understand the structure of the form. The HTMX attributes are also included without any additional effort. Likewise, we can also add the HyperScript code with _ as its name.

Handlers

A simple render() function is used to render the templates and catch any errors that may occur:

func render(ctx context.Context, template goht.Template, w io.Writer) error {
	err := template.Render(ctx, w)
	if err != nil {
		return errors.ErrInternal.Wrap(err, "failed to render template")
	}
	return nil
}

This is the handler that returns the content when a Todo is being modified:

func handleGet(todos domain.TodosStore) http.HandlerFunc {
	return errorHandler(func(w http.ResponseWriter, r *http.Request) error {
		id := chi.URLParam(r, "todoId")

		todo, err := todos.Get(r.Context(), id)
		if err != nil {
			return err
		}

		if hx.IsHtmx(r) {
			return render(r.Context(), partials.EditTodoForm(todo), w)
		}

		return render(r.Context(), pages.TodoPage(todo), w)
	})
}

For a handler without the shared application render() helper, call the generated template directly:

func handleShow(todos domain.TodosStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		id := chi.URLParam(r, "todoId")

		todo, err := todos.Get(r.Context(), id)
		if err != nil {
			http.Error(w, "todo not found", http.StatusNotFound)
			return
		}

		if err := pages.TodoPage(todo).Render(r.Context(), w); err != nil {
			http.Error(w, "failed to render todo", http.StatusInternalServerError)
			return
		}
	}
}

The snippets above show how GoHT templates, nested layouts, HTMX attributes, and Go HTTP handlers fit together in a small application.