fpf

package module
v0.0.0-...-a315335 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 22, 2017 License: BSD-2-Clause Imports: 7 Imported by: 0

README

fpf

FPF (Form Population Filter) is a library designed to help with the population of form values and error message insertion.

Examples (and documentation) can be found here to give a sense of scenarios where it is useful. But essentially, if you want to provide feedback to a user after they've submitted a form incorrectly, FPF can do that, independent from any template engine you've used.

Documentation

Overview

Package fpf provides form value population and error message insertion.

Value Population

Value population populates HTML form elements with provided values.

This is useful when a user is sent back to a form they have previously filled out, especially in regards to one they submitted but had validation errors.

Value population is achieved by parsing the HTML node tree to discover form elements. If a value is provided for any discovered form element, then the form element is populated with the value.

The way the value is populated depends upon the element:

  • textarea: the text content is populated.

  • select: the option matching the value is given the attribute "selected".

  • input[type=radio], input[type=checkbox]: the input is given the "checked" attribute.

  • input: the input's "value" attribute is set.

Error Message Insertion

Error message insertion is achieved by providing a list of "incidents". A single incident can have one or many error messages and also be associated with one or many form elements.

If a discovered form element has an associated incident, the IncidentInsertion strategy provided is invoked to insert error messages into the HTML node tree in relation to the form element and its labels.

Index

Examples

Constants

This section is empty.

Variables

View Source
var DefaultIncidentInserter = &GenericIncidentInserter{
	ErrorClass:                   "error",
	SingleElementErrorLocation:   After,
	MultipleElementErrorLocation: Child,
	Template:                     template.Must(template.New("error").Parse(`<ul class="errors">{{ range . }}<li>{{.}}</li>{{end}}</ul>`)),
}

DefaultIncidentInserter is the default incident inserter used if no other incident inserter is provided.

Functions

This section is empty.

Types

type Form

type Form struct {
	ID        string
	Values    url.Values
	Incidents []Incident
	// contains filtered or unexported fields
}

Form represents a form by ID that we wish to populate with values and perform error insertion on.

type FormPopulationFilter

type FormPopulationFilter struct {
	// The incident insertion strategy to use
	IncidentInsertion IncidentInserter

	IncludeHiddenInputs   bool // Whether to populate hidden input values
	IncludePasswordInputs bool // Whether to populate password input values
}

func New

func New() *FormPopulationFilter

New returns a FormPopulationFilter with default configuration.

func (*FormPopulationFilter) Execute

func (fpf *FormPopulationFilter) Execute(forms []Form, w io.Writer, r io.Reader) error

Execute reads from r, modifies forms matching the provided form IDs, and writes the output to w. The input is assumed to be UTF-8 encoded.

func (*FormPopulationFilter) ExecuteTemplate

func (fpf *FormPopulationFilter) ExecuteTemplate(forms []Form, w io.Writer, t *template.Template, data interface{}) error

Execute executes the provided template with the provided data, modifies forms matching the provided form IDs, and writes the output to w. The template output is assumed to be UTF-8 encoded.

Example
package main

import (
	"html/template"
	"log"
	"net/url"
	"os"
	"time"

	"github.com/saracen/fpf"
)

func main() {
	const tpl = `
<html>
<head>
	<title>Your Information</title>
</head>
<body>
	<form action="/" method="post">
		<div class="form-group">
			<label for="name">Name</label>
			<input name="name" type="text" placeholder="John Smith" />
		</div>

		<div class="form-group">
			<label>Do you like food? <input type="checkbox" name="food" /></label>
		</div>

		<div class="form-group">
			<label for="month">Favourite Month</label>
			<select id="month" name="month">
			{{- range $index, $month := .Months }}
				<option value="{{ $index }}">{{ $month }}</option>
			{{- end }}
			</select>
		</div>
	</form>
</body>
</html>`

	// Parse template
	t, err := template.New("info").Parse(tpl)
	if err != nil {
		log.Fatal(err)
	}

	// Create data we'll use with the template
	var data struct {
		Months []time.Month
	}
	for i := time.January; i <= time.December; i++ {
		data.Months = append(data.Months, i)
	}

	// Set the values we want populated
	values := url.Values{}
	values.Set("name", "Arran Walker")
	values.Set("food", "1")
	values.Set("month", "3")

	// Execute template with fpf
	fp := fpf.New()
	err = fp.ExecuteTemplate([]fpf.Form{{Values: values}}, os.Stdout, t, data)
	if err != nil {
		log.Fatal(err)
	}

}
Output:


<html><head>
	<title>Your Information</title>
</head>
<body>
	<form action="/" method="post">
		<div class="form-group">
			<label for="name">Name</label>
			<input name="name" type="text" placeholder="John Smith" value="Arran Walker"/>
		</div>

		<div class="form-group">
			<label>Do you like food? <input type="checkbox" name="food" checked="checked"/></label>
		</div>

		<div class="form-group">
			<label for="month">Favourite Month</label>
			<select id="month" name="month">
				<option value="0">January</option>
				<option value="1">February</option>
				<option value="2">March</option>
				<option value="3" selected="selected">April</option>
				<option value="4">May</option>
				<option value="5">June</option>
				<option value="6">July</option>
				<option value="7">August</option>
				<option value="8">September</option>
				<option value="9">October</option>
				<option value="10">November</option>
				<option value="11">December</option>
			</select>
		</div>
	</form>

</body></html>
Example (ErrorInsertion)
package main

import (
	"html/template"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"

	"github.com/saracen/fpf"
)

func main() {
	const tpl = `
<html>
<head>
	<title>{{ .Title }}</title>
</head>
<body>
	<form action="/" method="post">
		<div class="form-group">
			<label for="username">Username</label>
			<input name="username" type="text" />
		</div>

		<div class="form-group">
			<div class="form-group-left">
				<label for="password">Password</label>
				<input name="password" type="password" />
			</div>
			<div class="form-group-right">
				<label for="password-confirm">Confirm Password</label>
				<input name="password-confirm" type="password" />
			</div>
		</div>

		<div class="form-group">
			<label>Opt In Newsletter <input type="checkbox" name="newsletter" /></label>
		</div>
		<div class="form-group">
			<label>Opt In Spam <input type="checkbox" name="spam" /></label>
		</div>
	</form>
</body>
</html>`

	// Parse template
	t, err := template.New("register").Parse(tpl)
	if err != nil {
		log.Fatal(err)
	}

	// Create validation function
	validate := func(register url.Values) (incidents []fpf.Incident) {
		// validate username
		if len(register.Get("username")) < 5 {
			incidents = append(incidents, fpf.Incident{
				[]string{"username"},
				[]string{"Username needs to be 5 or more characters long."},
			})
		}

		// validate password
		if len(register.Get("password")) < 6 {
			incidents = append(incidents, fpf.Incident{
				[]string{"password", "password-confirm"},
				[]string{"Password needs to be 6 or more characters long."},
			})
		} else if register.Get("password") != register.Get("password-confirm") {
			incidents = append(incidents, fpf.Incident{
				[]string{"password", "password-confirm"},
				[]string{"Passwords do not match."},
			})
		}

		return incidents
	}

	// Using httptest server for example purposes
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var incidents []fpf.Incident

		if r.Method == "POST" {
			err := r.ParseForm()
			if err != nil {
				return
			}

			// Validate posted form
			if incidents = validate(r.PostForm); len(incidents) == 0 {
				// Validated successful
				// Add saving / additional logic
				return
			}
		}

		// Execute template with fpf
		fp := fpf.New()
		fp.ExecuteTemplate([]fpf.Form{{Values: r.PostForm, Incidents: incidents}}, os.Stdout, t, struct{ Title string }{"Registration Page"})
	}))
	defer ts.Close()

	// Emulate browser client post
	resp, err := http.PostForm(ts.URL, url.Values{
		"username":         {"sara"},
		"password":         {"password"},
		"password-comfirm": {"password123"},
		"spam":             {"on"},
	})
	if err != nil {
		log.Fatal(err)
	}

	defer resp.Body.Close()
	io.Copy(os.Stdout, resp.Body)

}
Output:

<html><head>
	<title>Registration Page</title>
</head>
<body>
	<form action="/" method="post">
		<div class="form-group">
			<label for="username">Username</label>
			<input name="username" type="text" value="sara" class="error"/><ul class="errors"><li>Username needs to be 5 or more characters long.</li></ul>
		</div>

		<div class="form-group">
			<div class="form-group-left">
				<label for="password">Password</label>
				<input name="password" type="password" class="error"/>
			</div>
			<div class="form-group-right">
				<label for="password-confirm">Confirm Password</label>
				<input name="password-confirm" type="password" class="error"/>
			</div>
		<ul class="errors"><li>Passwords do not match.</li></ul></div>

		<div class="form-group">
			<label>Opt In Newsletter <input type="checkbox" name="newsletter"/></label>
		</div>
		<div class="form-group">
			<label>Opt In Spam <input type="checkbox" name="spam" checked="checked"/></label>
		</div>
	</form>

</body></html>

type GenericIncidentInserter

type GenericIncidentInserter struct {
	// The error class given to labels and form input elements
	ErrorClass string

	// The location of to insert error messages
	SingleElementErrorLocation   Location
	MultipleElementErrorLocation Location

	// The error template that will be inserted
	Template *template.Template
}

GenericIncidentInserter provides a basic strategy for inserting error messages into the HTML node tree.

func (*GenericIncidentInserter) Insert

func (i *GenericIncidentInserter) Insert(elements []LabelableElement, errors []string) error

Insert uses a basic strategy for error insertions:

  • If there is more than one element then error messages are added as children to the elements' lowest common ancestor.

  • If there is only one element, the error messages are inserted beneath it

type Incident

type Incident struct {
	Names  []string
	Errors []string
}

Incident is a collection of one or more form element names and their error messages.

Multiple form element names are required when there's a group of elements that share common errors. For example, the inputs "new-password" and "new-password-confirm" can share the error "passwords do not match".

type IncidentInserter

type IncidentInserter interface {
	Insert(elements []LabelableElement, errors []string) error
}

IncidentInserter provides an interface for custom error message insertion strategies.

The insert method is provided with a list of the affected elements and error messages that to be inserted.

type LabelableElement

type LabelableElement struct {
	Element *html.Node
	Labels  []*html.Node
}

LabelableElement contains a form element and its associated labels.

type Location

type Location string
const (
	Before Location = "before"
	After  Location = "after"
	Child  Location = "child"
)

Directories

Path Synopsis
Package attributes provides helper functions for golang.org/x/net/html's node attributes.
Package attributes provides helper functions for golang.org/x/net/html's node attributes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL