How I write my UI components when using Htmx with go and templ; after a year of using Htmx

When I look at my UI all I see is a lot of HTML with a little of of JavaScript sprinkled in, where necessary. In between all of this soup (the slow cooked div kind :D) there is some Htmx in there somewhere. Most of the time, after I add the attributes that I need (mostly just swap, target, and the AJAX request), I never have to think about them and barely notice them.

If I were to think that this is all the impact that Htmx has had on how I build my websites, is to miss the forest for the seeds (or however you miss a forest). The real value of using Htmx is REpresentational State Transfer or REST. This approach to sending Html over the wire dictates how I write and organize my UI.

Components are more like sections

What I mean by that is, I structure my UI based on what needs to get swapped in or out when a AJAX call takes place. The page is just an organization of these sections. Each section, takes a type of data and problems. The data is the same type the server expects and problems are just a map of data that was not as expected by the server. This brings me to my next point.

There are only two sets of types on the server

One is the internal type that is associated with the database and the other types are shared both by the application and the UI. This means that validation is done once, when it is received as form values from the browser. I think an example would show this better.

Example

Let's assume the section consists of a form that accepts an email:

templ InfoForm(d data.InfoForm, p data.Problems) {
<form hx-post="/user-form" hx-swap="outerHTML">
    <input type="email" name="email" value={ d.Email } required />
    <span class="error">{ p["Email"] }</span>
    ...
</form>
}

There is generic validator I've defined that takes a *http.Request and an interface Decoder which implements a Decode method. So that for any reason you happened to use json for certain requests, it would work just as well.

Credit to Matt Ryer for the decodeValid method. Which I borrowed from his article here

This is what the decoder looks like:

func decodeValid[T validator](r *http.Request, d Decoder) (T, data.Problems, error) {
    // get values from form
    // check if the value is what you expected
}


interface Validator {
    Valid(ctx context.Context) data.Progblems
}

interface Decoder {
    Decode(dst interface{}, src map[string][]string) error
}

All the handler has to do is call decodeValid and if there are errors or problems, send the form back client. Let us assume that there were some problems that were caught during validation, this is what the response would look like:

type Info struct {
    Email string `schema:"email,required"` 
}

func (i Info) Valid(ctx context.Context)data.Problems {
    // handle validation
    // return problems if any
}

func InfoHandler(w http.ResponseWriter, r *http.Request) {
    info, problems := decodeValid[data.Info](r, decoder)
    // assuming there were no errors, and info is not nil
    if problems.Any() {
        renderComponent(w, r, view.InfoForm(info, problems)
        return
    }
    ...
}

This response to the client will have all the input fields populated with the values from the user and show problems where they happened. This allows me to work with data and validation for both the UI and the handler once.

It's a shared public type, with only the info that the user entered. There is no risk of sharing too much or too little info. You share exactly what you need.

Conclusion

Some of these ideas were borrowed from others and some have naturally occured as I've built more and more projects using HTMX, go and Templ. Recently, I was reading the section on testing in the pragmatic programmer. So that is the lens through which I'm looking at the code at the moment, making things easy to test is one my top priorities right now, and I want to isolate all the elements that I can test without writing integration tests, simply cause I don't like integration tests too much and I want to keep it to a minimum.