Decorator pattern with templates

Hello.

I’m trying to find a library or example for how to implement decorator pattern with a HTML template.

Here’s example with HTML form.

For example you have this kind of structure:

[
  {
    "name": "firstname",
    "description": "First name",
    "type": "input",
    "req": true,
    "placeholder": "Enter your first name",
    "value": "John"
  },
  {
    "name": "lastname",
    "description": "Last name",
    "type": "input",
    "req": true,
    "placeholder": "Enter your last name",
    "value": "Doe"
  },
  {
    "name": "age",
    "description": "Age",
    "type": "input",
    "req": true,
    "placeholder": "Enter your age",
    "value": "27"
  },
  {
    "name": "descr",
    "description": "Description",
    "type": "textarea",
    "req": false,
    "placeholder": "Enter description"
  }
]

The base/default decorators are for types:

input:

<label for="{{ .Name }}">{{if .Required}}*{{end}}{{ .Description }}</label>
<input type="text" name="{{ .Name }}" id="{{ .Name }}" value="{{ .Value }}" placeholder="{{ .Placeholder }}" />

textarea:

<label for="{{ .Name }}">{{if .Required}}*{{end}}{{ .Description }}</label>
<textarea name="{{ .Name }}" id="{{ .Name }}" placeholder="{{ .Placeholder }}">{{ .Value }}</textarea>

And then there are the actual decorators:

div_example:

<div class="decorator{{if .Required}} required{{end}}">{{.}}</div>

And then there’s the most highest decorator that encloses the whole form:

<form action="{{.Action}}" method="{{.Method}}">
{{.}}
</form>

So the end result is for example:

<form action="foo" method="post">
  <div class="decorator required">
    <label for="firstname">*First name</label>
    <input type="text" name="firstname" id="firstname" value="John" placeholder="Enter your first name" />
  </div>
   ....
  <div class="decorator">
    <textarea name="descr" id="descr" placeholder="Enter description"></textarea>
  </div>
</form>

Decorator chains can be changed, added or removed. So the output could also look like as that JSON in the beginning.

Here’s what I’ve got so far:

package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"os"
)

type FormFieldDescription struct {
	Name        string `json:"name,"`
	Description string `json:"description,"`
	Type        string `json:"type,"`
	Required    bool   `json:"req"`
	Placeholder string `json:"placeholder,omitempty"`
	Value       string `json:"value,omitempty"`
}

const (
	dec_input_text = `{{ define "default.field.input" }}
	<label for="{{ .Name }}">{{if .Required}}*{{end}}{{ .Description }}</label>
	<input type="text" name="{{ .Name }}" id="{{ .Name }}" value="{{ .Value }}" placeholder="{{ .Placeholder }}" />
	{{ end }}`
	
	dec_textarea = `{{ define "default.field.input" }}
	<label for="{{ .Name }}">{{if .Required}}*{{end}}{{ .Description }}</label>
	<textarea name="{{ .Name }}" id="{{ .Name }}" placeholder="{{ .Placeholder }}">{{ .Value }}</textarea>
	{{ end }}`

	dec_field_div = `{{define "default.field.decorator.div"}}<div class="decorator{{if .Required}} required{{end}}">
	{{.}}
	</div>
	{{end}}`

)

func main() {

	// Generated dynamically from struct
	var fields []FormFieldDescription
	fields = append(fields, FormFieldDescription{
		Name:        "firstname",
		Description: "First name",
		Placeholder: "Enter your first name",
		Type:        "input",
		Required:    true,
		Value:       "John",
	})
	fields = append(fields, FormFieldDescription{
		Name:        "lastname",
		Description: "Last name",
		Placeholder: "Enter your last name",
		Type:        "input",
		Required:    true,
		Value:       "Doe",
	})
	fields = append(fields, FormFieldDescription{
		Name:        "age",
		Description: "Age",
		Placeholder: "Enter your age",
		Type:        "input",
		Required:    true,
		Value:       "27",
	})
	fields = append(fields, FormFieldDescription{
		Name:        "descr",
		Description: "Description",
		Placeholder: "Enter description",
		Type:        "textarea",
		Required:    false,
		Value:       "",
	})

	var err error

	jsonb, err := json.MarshalIndent(&fields, "", "  ")

	fmt.Printf("Generating from:\n%v\n\n", string(jsonb))

	tpl := template.New("html")
	if err != nil {
		panic(err)
	}

	// Load decorators	
	tpl.Parse(dec_input_text)
	tpl.Parse(dec_textarea)
	tpl.Parse(dec_field_div)


	decorators := []string{"default", "default.field.decorator.div" /* N decorators ... */}

	fmt.Println("Into HTML:")

	// Apply decorators
	for _, item := range fields {
		tmpWriter := os.Stdout
		for _, decorator := range decorators {
			if decorator == "default" {
				decorator = fmt.Sprintf("default.field.%v", item.Type)
			}
			
			tpl.ExecuteTemplate(tmpWriter, decorator, item)
		}
	}

	// Then apply most highest decorators, ie. <form>

}

I’m also unsure should the decorator pipeline chain flow be from base to highest decorator as it is now or vice versa.

I think chains also should be array of template functions as in

func input() Template {}
func div() Template {}

decoratorChain := [input, div]

Any pointers and help for how to implement this is welcome.

Ok. Now I’m getting correct results:

https://play.golang.org/p/0WPwGWEL-bI

package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"bytes"
)

type FormFieldDescription struct {
	Name        string `json:"name,"`
	Description string `json:"description,"`
	Type        string `json:"type,"`
	Required    bool   `json:"req"`
	Placeholder string `json:"placeholder,omitempty"`
	Value       string `json:"value,omitempty"`
}

const (
	dec_input_text = `{{ define "default.field.input" }}
	<label for="{{ .Item.Name }}">{{if .Item.Required}}*{{end}}{{ .Item.Description }}</label>
	<input type="text" name="{{ .Item.Name }}" id="{{ .Item.Name }}" value="{{ .Item.Value }}" placeholder="{{ .Item.Placeholder }}" />
	{{- end -}}`
	
	dec_textarea = `{{ define "default.field.textarea" }}
	<label for="{{ .Item.Name }}">{{if .Item.Required}}*{{end}}{{ .Item.Description }}</label>
	<textarea name="{{ .Item.Name }}" id="{{ .Item.Name }}" placeholder="{{ .Item.Placeholder }}">{{ .Item.Value }}</textarea>
	{{- end -}}`

	dec_field_div = `{{define "default.field.decorator.div"}}<div class="decorator{{if .Item.Required}} required{{end}}">
	{{.Parent}}
	</div>
	{{- end -}}`

)

type Decorator struct {
	Item FormFieldDescription
	Parent template.HTML
}

func main() {

	// Generated dynamically from struct
	var fields []FormFieldDescription
	fields = append(fields, FormFieldDescription{
		Name:        "firstname",
		Description: "First name",
		Placeholder: "Enter your first name",
		Type:        "input",
		Required:    true,
		Value:       "John",
	})
	fields = append(fields, FormFieldDescription{
		Name:        "lastname",
		Description: "Last name",
		Placeholder: "Enter your last name",
		Type:        "input",
		Required:    true,
		Value:       "Doe",
	})
	fields = append(fields, FormFieldDescription{
		Name:        "age",
		Description: "Age",
		Placeholder: "Enter your age",
		Type:        "input",
		Required:    true,
		Value:       "27",
	})
	fields = append(fields, FormFieldDescription{
		Name:        "descr",
		Description: "Description",
		Placeholder: "Enter description",
		Type:        "textarea",
		Required:    false,
		Value:       "",
	})

	var err error

	jsonb, err := json.MarshalIndent(&fields, "", "  ")

	fmt.Printf("Generating from:\n%v\n\n", string(jsonb))

	tpl := template.New("html")
	if err != nil {
		panic(err)
	}

	// Load decorators	
	tpl.Parse(dec_input_text)
	tpl.Parse(dec_textarea)
	tpl.Parse(dec_field_div)

	decorators := []string{"default", "default.field.decorator.div" /* N decorators ... */}

	fmt.Println("Into HTML:")

	// Apply decorators
	for _, item := range fields {
		var dec Decorator
		dec.Parent = ""
		var tmpWriter bytes.Buffer

		for _, decorator := range decorators {
			if decorator == "default" {
				decorator = fmt.Sprintf("default.field.%v", item.Type)
			}
			tmpWriter.Reset()
			dec.Item = item
			tpl.ExecuteTemplate(&tmpWriter, decorator, dec)
			dec.Parent = template.HTML(tmpWriter.String())
		}
		
		fmt.Println(tmpWriter.String())
	}

}

Does this look like correct approach?

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.