package form import ( "errors" "fmt" "html/template" "strings" ) type builder struct { tpl *template.Template } // Inputs will parse the provided struct into fields and then execute the // template with each field. The returned HTML is simply all of these results // appended one after another. // // Inputs' second argument - errs - will be used to render errors for // individual fields. func (b *builder) Inputs(v interface{}, errs ...error) (template.HTML, error) { tpl, err := b.tpl.Clone() if err != nil { return "", err } fields := fields(v) errors := fieldErrors(errs) var html template.HTML for _, field := range fields { var sb strings.Builder tpl.Funcs(template.FuncMap{ "errors": func() []string { if errs, ok := errors[field.Name]; ok { return errs } return nil }, }) err := tpl.Execute(&sb, field) if err != nil { return "", err } html = html + template.HTML(sb.String()) } return html, nil } // FuncMap returns a template.FuncMap that defines both the inputs_for and // inputs_and_errors_for functions for usage in the template package. The // latter is provided via a closure because variadic parameters and the // template package don't play very nicely and this just simplifies things // a lot for end users of the form package. func FuncMap(formTpl *template.Template) template.FuncMap { b := builder{tpl: formTpl} return template.FuncMap{ "inputs_for": b.Inputs, "inputs_and_errors_for": func(v interface{}, errs []error) (template.HTML, error) { return b.Inputs(v, errs...) }, } } // ParsingFuncMap is present to make it a little easier to build the input template. // In order to parse a template that uses the `errors` function, you need to have // that template defined when the template is parsed. We clearly don't know whether // a field has an error or not until it is parsed. func ParsingFuncMap() template.FuncMap { return template.FuncMap{ "errors": func() []string { return nil }, } } type FieldError struct { Field string Issue string } func (fe FieldError) Error() string { return fmt.Sprintf("%s: %v", fe.Field, fe.Issue) } // errors will build a map where each key is the field name, and each // value is a slice of strings representing errors with that field. func fieldErrors(errs []error) map[string][]string { ret := make(map[string][]string) for _, err := range errs { var fe FieldError if !errors.As(err, &fe) { fmt.Println(err, "isnt field error") continue } ret[fe.Field] = append(ret[fe.Field], fe.Issue) } return ret }