This is great... but... not really

I hear you. This is not very interactive is it? We will get there, hold up



Also... remember

this is a throw away application and our goal is to learn htmx... we may cut a corner or two...
















No tailwind?

normally i would be more willing to go the distance here... but i want this to be as much focus on HTMX as possible, not on anything else



  • you can literally use react with htmx
  • you can use web components, lit components, whatever
  • you can even raw dawg some js and css in there if needed















Lets build a small application

a simple "contacts" application

The application has a form that takes in name and email and "saves" it

  • first lets create a form that takes a name and email
  • every time we hit "save" it will add the name and email to the server (just in memory) and display the updated list of names and emails


Why No sqlite? don't you love turso???


* every time we refresh our server, we lose our data

First The HTML then we will do the server part

  • build a form with name and email and submit button
  • build a display list of name and emails (contacts)















Full Programmed HTML Engineering

views/index.html

<html>
    <head>
        <title>Our First HTML Site!</title>
        <script src="https://unpkg.com/htmx.org/dist/htmx.min.js"></script>
    </head>
    <body>
        {{ template "form" . }}
        <hr />
        {{ template "display" . }}
    </body>
</html>

{{ block "form" . }}
<form hx-post="/contacts">
    <label for="name">Name</label>
    <input name="name" placeholder="Name">

    <label for="email">Email</label>
    <input type="email" name="email" placeholder="Email">

    <button type="submit">Submit</button>
</form>
{{ end }}

{{ block "display" . }}
    {{ range .Contacts }}
        <div>
            Name: <span>{{ .Name }}</span>
            Email: <span>{{ .Email }}</span>
        </div>
    {{ end }}
{{ end }}















Now lets upgrade the server

  • index.html now requires an object with { .Contacts } on it
  • we need an endpoint /contacts that takes POST requests















All the code

package main

import (
    "html/template"
    "io"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

type Template struct {
    tmpl *template.Template
}

func newTemplate() *Template {
    return &Template{
        tmpl: template.Must(template.ParseGlob("views/*.html")),
    }
}

func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
    return t.tmpl.ExecuteTemplate(w, name, data)
}

type Contact struct {
    Name  string
    Email string
}

type Data struct {
    Contacts []Contact
}

func NewData() *Data {
    return &Data{
        Contacts: []Contact{},
    }
}

func NewContact(name, email string) Contact {
    return Contact{
        Name: name,
        Email: email,
    }
}

func main() {

    e := echo.New()

    data := NewData()

    e.Renderer = newTemplate()
    e.Use(middleware.Logger())

    e.GET("/", func(c echo.Context) error {
        return c.Render(200, "index.html", data)
    })

    e.POST("/contacts", func(c echo.Context) error {
        name := c.FormValue("name")
        email := c.FormValue("email")

        data.Contacts = append(data.Contacts, NewContact(name, email))

        return c.Render(200, "index.html", data)
    })

    e.Logger.Fatal(e.Start(":42069"))
}















What went wrong?

Name And Email Display Issues
















Lets fix this

this is the same problem as before. we are returning the whole html fragment but replacing the form with that value (the body value)
















But is this good?

What are the problems with this approach?
















Lets explore the space a bit

There is more to offer with htmx... lets give it a go

  • what if we only responded with contacts?















What's wrong with this approach?

  • there are two big problems here...















what about errors?

One option is to use response headers: Response Headers



Another option: invert how we are doing this and use out of band updates



  • lets first make the response be an error on duplicate email and display errors in form, 400 status code for bad request















Why are we not rendering?

  1. we have hit default behavior of htmx. Default Behavior of 4xx and 5xx
  2. is 400 the right status code?


What we need to do

  1. lets change from 400 -> 422, respond with some extra headers
  2. lets add this bit of JS to our index
<script>
document.addEventListener("DOMContentLoaded", (event) => {
    document.body.addEventListener('htmx:beforeSwap', function(evt) {
        if (evt.detail.xhr.status === 422) {
            // allow 422 responses to swap as we are using this as a signal that
            // a form was submitted with bad data and want to rerender with the
            // errors
            //
            // set isError to false to avoid error logging in console
            evt.detail.shouldSwap = true;
            evt.detail.isError = false;
        }
    });
})
</script>















Ok, we can render errors, what about success?

Lets tackle this with out of band updates
















Where is my form?

great question...
















Lets look at the HTML

  • is this correct?
  • simple fix...















Full Code

cmd/main.go

package main

import (
    "html/template"
    "io"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

type Template struct {
    tmpl *template.Template
}

func newTemplate() *Template {
    return &Template{
        tmpl: template.Must(template.ParseGlob("views/*.html")),
    }
}

func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
    return t.tmpl.ExecuteTemplate(w, name, data)
}

type Contact struct {
    Name  string
    Email string
}

type Data struct {
    Contacts []Contact
}

func NewData() *Data {
    return &Data{
        Contacts: []Contact{
            {
                Name:  "John Doe",
                Email: "john.doe@gmail.com",
            },
            {
                Name:  "Jane Doe",
                Email: "jain.doe@gmail.com",
            },
        },
    }
}

type FormData struct {
    Errors map[string]string
    Values map[string]string
}

func NewFormData() FormData {
    return FormData{
        Errors: map[string]string{},
        Values: map[string]string{},
    }
}

type PageData struct {
    Data Data
    Form FormData
}

func NewContact(name, email string) Contact {
    return Contact{
        Name:  name,
        Email: email,
    }
}

func NewPageData(data Data, form FormData) PageData {
    return PageData{
        Data: data,
        Form: form,
    }
}

func contactExists(contacts []Contact, email string) bool {
    for _, c := range contacts {
        if c.Email == email {
            return true
        }
    }
    return false
}

func main() {

    e := echo.New()

    data := NewData()

    e.Renderer = newTemplate()
    e.Use(middleware.Logger())

    e.GET("/", func(c echo.Context) error {
        return c.Render(200, "index.html", NewPageData(*data, NewFormData()))
    })

    e.POST("/contacts", func(c echo.Context) error {
        name := c.FormValue("name")
        email := c.FormValue("email")

        if contactExists(data.Contacts, email) {
            formData := FormData{
                Errors: map[string]string{
                    "email": "Email already exists",
                },
                Values: map[string]string{
                    "name":  name,
                    "email": email,
                },
            }

            return c.Render(422, "contact-form", formData)
        }

        contact := NewContact(name, email)
        data.Contacts = append(data.Contacts, contact)

        formData := NewFormData()
        err := c.Render(200, "contact-form", formData)

        if err != nil {
            return err
        }

        return c.Render(200, "oob-contact", contact)
    })

    e.Logger.Fatal(e.Start(":42069"))
}

views/index.html

<html>
    <head>
        <title>Our First HTML Site!</title>
        <script src="https://unpkg.com/htmx.org/dist/htmx.js"></script>
    </head>
    <body>
        {{ template "contact-form" .Form }}
        <hr />
        {{ template "display" .Data }}

<script type="text/javascript">
        document.addEventListener("DOMContentLoaded", (event) => {
document.body.addEventListener('htmx:beforeSwap', function(evt) {
    if (evt.detail.xhr.status === 422) {
            console.log("setting status to paint");
        // allow 422 responses to swap as we are using this as a signal that
        // a form was submitted with bad data and want to rerender with the
        // errors
        //
        // set isError to false to avoid error logging in console
        evt.detail.shouldSwap = true;
        evt.detail.isError = false;
    }
});
        });
</script>
    </body>
</html>

views/contacts.html

{{ block "contact-form" . }}
<form id="contact-form" hx-post="/contacts" hx-swap="outerHTML">
    <label for="name">Name</label>
    <input name="name"
        {{ if .Values }}
            {{ if .Values.name }}
                value="{{ .Values.name }}"
            {{ end }}
        {{ end }}
        placeholder="Name">

        {{ if (.Errors) }}
            {{ if (.Errors.name) }}
                <div class="error">{{ .Errors.name }}</div>
            {{ end }}
        {{ end }}

    <label for="email">Email</label>
    <input type="email"
        {{ if (.Values) }}
            {{ if (.Values.email) }}
                value="{{ .Values.email }}"
            {{ end }}
        {{ end }}
        name="email" placeholder="Email">

        {{ if (.Errors) }}
            {{ if (.Errors.email) }}
                <div class="error">{{ .Errors.email }}</div>
            {{ end }}
        {{ end }}

    <button type="submit">Submit</button>
</form>
{{ end }}

{{ block "display" . }}
    <div id="contacts">
    {{ range .Contacts }}
        {{ template "contact" . }}
    {{ end }}
    </div>
{{ end }}


{{ block "contact" . }}
<div>
    Name: <span>{{ .Name }}</span>
    Email: <span>{{ .Email }}</span>
</div>
{{ end }}

{{ block "oob-contact" . }}
<div hx-swap-oob="afterbegin" id="contacts">
    {{ template "contact" . }}
</div>
{{ end }}

{{ block "test" . }}
<div>
__TESTING__
</div>
{{ end }}