Skip to main content

View models

With templ, you can pass any Go type into your template as parameters, and you can call arbitrary functions.

However, if the parameters of your template don't closely map to what you're displaying to users, you may find yourself calling a lot of functions within your templ files to reshape or adjust data, or to carry out complex repeated string interpolation or URL constructions.

This can make template rendering hard to test, because you need to set up complex data structures in the right way in order to render the HTML. If the template calls APIs or accesses databases from within the templates, it's even harder to test, because then testing your templates becomes an integration test.

A more reliable approach can be to create a "View model" that only contains the fields that you intend to display, and where the data structure closely matches the structure of the visual layout.

package invitesget

type Handler struct {
Invites *InviteService
}

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
invites, err := h.Invites.Get(getUserIDFromContext(r.Context()))
if err != nil {
//TODO: Log error server side.
}
m := NewInviteComponentViewModel(invites, err)
teamInviteComponent(m).Render(r.Context(), w)
}

func NewInviteComponentViewModel(invites []models.Invite, err error) (m InviteComponentViewModel) {
m.InviteCount = len(invites)
if err != nil {
m.ErrorMessage = "Failed to load invites, please try again"
}
return m
}


type InviteComponentViewModel struct {
InviteCount int
ErrorMessage string
}

templ teamInviteComponent(model InviteComponentViewModel) {
if model.InviteCount > 0 {
<div>You have { fmt.Sprintf("%d", model.InviteCount) } pending invites</div>
}
if model.ErrorMessage != "" {
<div class="error">{ model.ErrorMessage }</div>
}
}