+++ title = "Building view-trees: The basics [Part 2]" date = 2023-12-04 +++ We laid our goals in the [part 1][part-1]. --- # Let's make something renderable ```go import "html/template" type RenderFunc func(r Renderable) (template.HTML, error) type Renderable interface { Template() (*template.Template, error) TemplateData() (any, error) } ``` Let's start with interfaces and type definitions of the concepts: 1. We want to be able to `Render` a `Renderable` struct into HTML, this can fail. 2. We also want the Renderable thing to give us all of the information it needs so we can render it. This can also fail. This interface is small, let's see how far we can push this. ## First Implementation ```go func Render(r Renderable) (template.HTML, error) { var empty template.HTML tpl, err := r.Template() if err != nil { return empty, err } data, err := r.TemplateData() if err != nil { return empty, err } var bs bytes.Buffer if err := tpl.Execute(&bs, data); err != nil { return empty, err } return template.HTML(bs.String()), nil } ``` The implementation is small, too, but what good are components if you can't compose them. ### Patches {{ veun_diff(patch=1) }} {{ veun_diff(patch=2) }} # Trees and Subviews In order to bring the component into our tree composition view library, we need to have `Renderable` objects have subtrees. ```go _, _ := Render(ContainerView{ Heading: ChildView1{}, Body: ChildView2{}, }) ``` ```mustache
{{ slot "heading" }}
{{ slot "body" }}
``` ## The POC The basic idea is to leverage `template.FuncMap` to create a `slot` function. ```go func (v ContainerView) Template() (*template.Template, error) { return template.New("containerView").Funcs(template.FuncMap{ "slot": func(name string) (template.HTML, error) { switch name { case "heading": return Render(v.Heading) case "body": return Render(v.Body) default: return template.HTML(""), nil } }, }).Parse(`
{{ slot "heading" }}
{{ slot "body" }}
`) } ``` {{ veun_diff(patch=3) }} ### Alternate approach Alternatively, we can directly inline the fields in the data so our template looks more like this: ```mustache
{{ render .Slots.Heading }}
``` ## Template compilation *Refactor 1*: Making it so that we can do pre-compilation of the template, we can pre-parse it. The immediate issue is that we don't have slots, and the slot func is necessary to compile the tempalte. We can stub that out: ```go func slotFuncStub(name string) (template.HTML, error) { return template.HTML(""), nil } func mustParseTemplate(name, contents string) *template.Template { return template.Must( template.New(name). Funcs(template.FuncMap{"slot": slotFuncStub}). Parse(contents), ) } var containerViewTpl = mustParseTemplate("containerView", `
{{ slot "heading" }}
{{ slot "body" }}
`) ``` And then update our `Template()` function: ```go containerViewTpl.Funcs(template.FuncMap{ "slot": func(name string) (template.HTML, error) { switch name { case "heading": return Render(v.Heading) case "body": return Render(v.Body) default: return template.HTML(""), nil } }, }) ``` {{ veun_diff(patch=4) }} *Refactor 2:* We can clean up the real slot function so that it is less brittle when views/slots are added and removed. ```go func tplWithRealSlotFunc( tpl *template.Template, slots map[string]Renderable, ) *template.Template { return tpl.Funcs(template.FuncMap{ "slot": func(name string) (template.HTML, error) { slot, ok := slots[name] if ok { return Render(slot) } return template.HTML(""), nil }, }) } // ... snip ... return tplWithRealSlotFunc(containerViewTpl, map[string]Renderable{ "heading": v.Heading, "body": v.Body, }), nil ``` At this point we've extracted common implementation details but have kept our main interface the same, which is cool! Our base renderer doesn't need to know much about anything else, doesn't need to know about slots, or funcs, or where templates come from. {{ veun_diff(patch=5) }} ## A `View{}` This is generally all well and good, we might want to have something produce a `Renderable` struct, in fact we might have a struct that is represents a `Renderable` object, what if we could capture the above pattern in a piece of data as well as behavior? ```go type View struct { Tpl *template.Template Slots map[string]Renderable Data any } func (v View) Template() (*template.Template, error) { return tplWithRealSlotFunc(v.Tpl, v.Slots), nil } func (v View) TemplateData() (any, error) { return v.Data, nil } ``` The container becomes representable in a different way and it would have the equivalent outcome when rendered. {{ veun_diff(patch=6) }} ```go View{ Tpl: containerViewTpl, Slots: map[string]Renderable{ "heading": ChildView1{}, "body": ChildView2{}, } } ``` But we still might want to have ContainerView be the thing we can "render", how would we do both? ```go type AsRenderable interface { func Renderable() (Renderable, error) } type Slots map[string]AsRenderable ``` {{ veun_diff(patch=7) }} And updating the `Render` function for the first time to take `AsRenderable` instead gives us our first really big interface change, but it unlocks something, too. A simpler way to build views: ```go func (v ContainerView) Renderable() (Renderable, error) { return View{ Tpl: containerViewTpl, Slots: Slots{"heading": v.Heading, "body": v.Body}, ), nil } ``` {{ veun_diff(patch=8) }} --- ### Next: - [Error handling][part-3] - [Async data fetching][part-4] - [http.Handler][part-5] - [Updating the base interface][part-6] - [What's up with Renderables][part-7] [part-1]: /writes/building-view-trees-in-go-part-1 [part-3]: /writes/building-view-trees-in-go-part-3 [part-4]: /writes/building-view-trees-in-go-part-4 [part-5]: /writes/building-view-trees-in-go-part-5 [part-6]: /writes/building-view-trees-in-go-part-6 [part-7]: /writes/building-view-trees-in-go-part-7