Skip to content

Commit

Permalink
Error rendering (#1)
Browse files Browse the repository at this point in the history
Implemented error handling by accepting a variadic parameter of errors
and seeing which fo those errors implement the fieldError interface.
Those errors are then used to build a map of field keys and their
corresponding errors and this is accessible in templates via the `errors`
function.
  • Loading branch information
joncalhoun authored Jul 4, 2018
1 parent 47b710f commit 81cb575
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 58 deletions.
146 changes: 109 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,109 @@ Easily create HTML forms with Go structs.

## Overview

The `form` package makes it easy to take a Go struct and turn it into an HTML form using whatever HTML format you want. Below is an example, along with the output. This entire example can be found in the [examples/readme](examples/readme) directory.
The `form` package makes it easy to take a Go struct and turn it into an HTML form using whatever HTML format you want. Below is an example, along with the output, but first let's just look at an example of what I mean.

Let's say you have a Go struct that looks like this:

```go
type customer struct {
Name string
Email string
Address *address
}

type address struct {
Street1 string
Street2 string
City string
State string
Zip string `form:"label=Postal Code"`
}
```

Now you want to generate an HTML form for it, but that is somewhat annoying if you want to persist user-entered values if there is an error, or if you want to support loading URL query params and auto-filling the form for the user. With this package you can very easily do both of those things simply by defining what the HTML for an input field should be:

```html
<div class="mb-4">
<label class="block text-grey-darker text-sm font-bold mb-2" {{with .ID}}for="{{.}}"{{end}}>
{{.Label}}
</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker leading-tight {{if errors}}border-red{{end}}" {{with .ID}}id="{{.}}"{{end}} type="{{.Type}}" name="{{.Name}}" placeholder="{{.Placeholder}}" {{with .Value}}value="{{.}}"{{end}}>
{{range errors}}
<p class="text-red pt-2 text-xs italic">{{.}}</p>
{{end}}
</div>
```

This particular example is using [Tailwind CSS](https://tailwindcss.com/docs/what-is-tailwind/) to style the values, along with the `errors` template function which is provided via this `form` package when it creates the inputs for each field.

Now we can render this entire struct as a form by simply using the `inputs_for` template function which is provided by the `form.Builder`'s `FuncMap` method:

```html
<form class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" action="/" method="post">
{{inputs_for .Customer}}
<!-- ... add buttons here -->
</form>
```

And with it we will generate an HTML form like the one below:

![Example output from the forms package](example.png)

Data set in the `.Customer` variable in our template will also be used when rendering the form, which is why you see `Michael Scott` and `[email protected]` in the screenshot - these were set in the `.Customer` and were thus used to set the input's value.

Error rendering is also possible, but requires the usage of the `inputs_and_errors_for` template function, and you need to pass in errors that implement the `fieldError` interface (shown below, but NOT exported):

```go
type fieldError interface {
FieldError() (field, err string)
}
```

For instance, in [examples/errors/errors.go](examples/errors/errors.go) we pass data similar the following into our template when executing it:

```go
data := struct {
Form customer
Errors []error
}{
Form: customer{
Name: "Michael Scott",
Email: "[email protected]",
Address: nil,
},
Errors: []error{
fieldError{
Field: "Email",
Issue: "is already taken",
},
fieldError{
Field: "Address.Street1",
Issue: "is required",
},
...
},
}
tpl.Execute(w, data)
```

And then in the template we call the `inputs_and_errors_for` function:

```html
<form class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4" action="/" method="post">
{{inputs_and_errors_for .Form .Errors}}
<!-- ... buttons here -->
</form>
```

And we get an output like this:

![Example output from the forms package with errors](examples/errors/errors.png)


## Complete Examples

This entire example can be found in the [examples/readme](examples/readme) directory. Additional examples can also be found in the [examples/](examples/) directory and are a great way to see how this package could be used.

**Source Code**

Expand Down Expand Up @@ -48,9 +150,7 @@ func main() {
InputTemplate: tpl,
}

pageTpl := template.Must(template.New("").Funcs(template.FuncMap{
"inputs_for": fb.Inputs,
}).Parse(`
pageTpl := template.Must(template.New("").Funcs(fb.FuncMap()).Parse(`
<html>
<body>
<form>
Expand Down Expand Up @@ -141,6 +241,11 @@ If you also need to parse forms created by this package, I recommend using the [

There is an example of this in the [examples/tailwind](examples/tailwind) directory.

## Rendering errors

If you want to render errors, see the [examples/errors/errors.go](examples/errors/errors.go) example and most notably check out the `inputs_and_errors_for` function provided to templates via the `Builder.FuncMap()` function.

*TODO: Add some better examples here, but the provided code sample **is** a complete example.*

## This may have bugs

Expand All @@ -157,39 +262,6 @@ This section is mostly for myself to jot down notes, but feel free to read away.

Long term this could also support parsing forms, but gorilla/schema does a great job of that already so I don't see any reason to at this time. It would likely be easier to just make the default input names line up with what gorilla/schema expects and provide examples for how to use the two together.

#### Error rendering

I could also look into ways to handle errors and add messages to forms. This shouldn't be *too* hard to do. It would probably be something like an optional argument passed into the form builder and then we process it looking for implementations of an interface like:

```go
for _, err := range errors {
if fe, ok := err.(interface{
Field() string
Message() string
}); ok {
map[fe.Field()] = fe.Message()
}
}
```

Then we could pass it as an `Error` field each time we render:

```html
<div>
<label>
{{.Label}}
</label>
<input name="{{.Name}}" placeholder="{{.Placeholder}}" {{with .Value}}value="{{.}}"{{end}} class="{{with .Errors}}border-red{{end}}">
{{range .Errors}}
<p class="text-red-dark py-2 text-sm">{{.}}</p>
{{end}}
{{with .Footer}}
<p class="text-grey pt-2 text-xs italic">{{.}}</p>
{{end}}
</div>
```


#### Checkboxes and other data types

Maybe allow for various templates for different types, but for now this is possible to do in the HTML templates so it isn't completely missing.
Expand Down
136 changes: 131 additions & 5 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,141 @@ type Builder struct {
// Builder.InputTemplate with each field. The returned HTML is simply
// all of these results appended one after another.
//
// Inputs only accepts structs. This may change later, but that is all I
// needed for my use case so it is what it does.
func (b *Builder) Inputs(v interface{}) template.HTML {
// Inputs only accepts structs for the first argument. This may change
// later, but that is all I needed for my use case so it is what it does.
// If you need support for something else like maps let me know.
//
// Inputs' second argument - errs - will be used to render errors for
// individual fields. This is done by looking for errors that implement
// the fieldError interface:
//
// type fieldError interface {
// FieldError() (field, err string)
// }
//
// Where the first return value is expected to be the field with an error
// and the second is the actual error message to be displayed. This is then
// used to provide an `errors` template function that will return a slice
// of errors (if there are any) for the current field your InputTemplate
// is rendering. See examples/errors/errors.go for an example of this in
// action.
//
// This interface is not exported and you can pass other errors into Inputs
// but they currently won't be used.
func (b *Builder) Inputs(v interface{}, errs ...error) (template.HTML, error) {
tpl, err := b.InputTemplate.Clone()
if err != nil {
return "", err
}
fields := fields(v)
errors := errors(errs)
var html template.HTML
for _, field := range fields {
var sb strings.Builder
b.InputTemplate.Execute(&sb, field)
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
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 (b *Builder) FuncMap() template.FuncMap {
return template.FuncMap{
"inputs_for": b.Inputs,
"inputs_and_errors_for": func(v interface{}, errs []error) (template.HTML, error) {
return b.Inputs(v, errs...)
},
}
}

// FuncMap is present to make it a little easier to build the InputTemplate
// field of the Builder type. 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 via the Inputs method call, so this basically
// just provides a stubbed out errors function that returns nil so the template
// compiles correctly.
//
// See examples/errors/errors.go for a clear example of this being used.
func FuncMap() template.FuncMap {
return template.FuncMap{
"errors": ErrorsStub,
}
}

// ErrorsStub is a stubbed out function that simply returns nil. It is present
// to make it a little easier to build the InputTemplate field of the Builder
// type, since your template will likely use the errors function in the
// template before it can truly be defined. You probably just want to use
// the provided FuncMap helper, but this can be useful when you need to
// build your own template.FuncMap.
//
// See examples/errors/errors.go for a clear example of the FuncMap function
// being used, and see FuncMap for an example of how ErrorsStub can be used.
func ErrorsStub() []string {
return nil
}

// fieldError is an interface defining an error that represents something
// wrong with a particular struct field. The name should correspond to the
// name value used when building the HTML form, which is currently a period
// separated list of all fields that lead up to the particular field.
// Eg, in the following struct the Mouse field would have a key of Cat.Mouse:
//
// type Dog struct {
// Cat: struct{
// Mouse string
// }
// }
//
// The top level Dog struct name is not used because this is unnecessary,
// but any other nested struct names are necessary to properly determine
// the field.
//
// It should also be noted that if you provide a custom field name, that
// name should also be used in fieldError implementations.
type fieldError interface {
FieldError() (field, err string)
}

// 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.
//
// It works by looking for errors that implement the following interface:
//
// interface {
// FieldError() (string, string)
// }
//
// Where the first string returned is expected to be the field name, and
// the second return value is expected to be an error with that field.
// Any errors that implement this interface are then used to build the
// slice of errors for the field, meaning you can provide multiple
// errors for the same field and all will be utilized.
func errors(errs []error) map[string][]string {
ret := make(map[string][]string)
for _, err := range errs {
fe, ok := err.(fieldError)
if !ok {
continue
}
field, fieldErr := fe.FieldError()
ret[field] = append(ret[field], fieldErr)
}
return ret
}
6 changes: 5 additions & 1 deletion builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ func TestBuilder_Inputs(t *testing.T) {
b := &Builder{
InputTemplate: tc.tpl,
}
if got := b.Inputs(tc.arg); !reflect.DeepEqual(got, tc.want) {
got, err := b.Inputs(tc.arg)
if err != nil {
t.Errorf("Builder.Inputs() err = %v, want %v", err, nil)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("Builder.Inputs() = %v, want %v", got, tc.want)
}
})
Expand Down
Binary file added example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 1 addition & 3 deletions examples/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ func main() {
InputTemplate: tpl,
}

pageTpl := template.Must(template.New("").Funcs(template.FuncMap{
"inputs_for": fb.Inputs,
}).Parse(`
pageTpl := template.Must(template.New("").Funcs(fb.FuncMap()).Parse(`
<html>
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
Expand Down
Loading

0 comments on commit 81cb575

Please sign in to comment.