Working create form

main
Drew Bednar 2 months ago
parent cd7c75c707
commit ec2c95efb6

@ -177,4 +177,8 @@ The [official template docs](https://pkg.go.dev/text/template#hdr-Functions) can
### Headers
- [Primer on Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
- [Primer on Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP)
## Form Validation
- https://www.alexedwards.net/blog/validation-snippets-for-go: Covers snippets for common form validation logic.

@ -19,6 +19,10 @@ type Snippet struct {
ExpiresAt time.Time
}
func (s *Snippet) GetTitle() {
return
}
type SnippetService struct {
DB *sql.DB
}

@ -0,0 +1,13 @@
package server
// Define a snippetCreateForm struct to represent the form data and validation
// errors for the form fields. Note that all the struct fields are deliberately
// exported (i.e. start with a capital letter). This is because struct fields
// must be exported in order to be read by the html/template package when
// rendering the template.
type snippetCreateForm struct {
Title string
Content string
Expires int
FieldErrors map[string]string
}

@ -6,6 +6,8 @@ import (
"log/slog"
"net/http"
"strconv"
"strings"
"unicode/utf8"
"git.runcible.io/learning/ratchet/internal/model"
)
@ -120,9 +122,17 @@ func handleSnippetView(logger *slog.Logger, tc *TemplateCache, snippetService *m
})
}
func handleSnippetCreateGet() http.Handler {
func handleSnippetCreateGet(tc *TemplateCache) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Create snippet form.."))
data := newTemplateData()
// Initialize a new snippetCreateForm instance and pass it to the template.
// Notice how this is also a great opportunity to set any default or
// 'initial' values for the form --- here we set the initial value for the
// snippet expiry to 365 days.
data.Form = snippetCreateForm{
Expires: 365,
}
renderTemplate(w, r, tc, http.StatusOK, "create.go.tmpl", data)
})
}
@ -135,13 +145,91 @@ func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, snippetServ
// example of a custom header. Must be done before calling WriteHeader
// or they will fail to take effect.
w.Header().Add("Server", "Dirp")
// Create some variables holding dummy data. We'll remove these later on
// during the build.
title := "O snail"
content := "O snail\nClimb Mount Fuji,\nBut slowly, slowly!\n\n Kobayashi Issa"
expires := 7
id, err := snippetService.Insert(title, content, expires)
// Limit the request body size to 4096 bytes
// by default it's 10mb unless enctype="multipart/form-data in which case
// thier is no limit. You would do this however to limit the number of bytes
// that r.ParseForm will process. In this case it is only the first 4096 bytes
// reading beyond that will cause the MaxBytesReader to throw and error surfaced
// in your call to r.ParseForm()
// Also if the limit is reached the reader flags the server which will terminate
// the under lying TCP connection.
r.Body = http.MaxBytesReader(w, r.Body, 4096)
// ParseForm is idempotent. Can be called many times.
// First we call r.ParseForm() which adds any data in POST request bodies
// to the r.PostForm map. This also works in the same way for PUT and PATCH
// requests. If there are any errors, we use our app.ClientError() helper to
// send a 400 Bad Request response to the user.
// If you have a form that submits data using the HTTP method GET, rather than POST,
// the form data will be included as URL query string parameters /foo/bar?title=value&content=value.
// can be retrieved using r.URL.Query().Get() which will return the value or an empty string.
// r.Form could be used, but it is clearer and more explicit to stick with r.URL.Query and r.PostForm
// to access submitted data.
err := r.ParseForm()
if err != nil {
clientError(w, http.StatusBadRequest)
return
}
// The r.PostForm.Get() method always returns the form data as a *string*.
// However, we're expecting our expires value to be a number, and want to
// represent it in our Go code as an integer. So we need to manually convert
// the form data to an integer using strconv.Atoi(), and we send a 400 Bad
// Request response if the conversion fails.
expires, err := strconv.Atoi(r.PostForm.Get("expires"))
if err != nil {
clientError(w, http.StatusBadRequest)
return
}
// Create an instance of the snippetCreateForm struct containing the values
// from the form and an empty map for any validation errors.
form := snippetCreateForm{
Title: r.PostForm.Get("title"),
Content: r.PostForm.Get("content"),
Expires: expires,
FieldErrors: map[string]string{},
}
// VALIDATION
if strings.TrimSpace(form.Title) == "" {
form.FieldErrors["title"] = "field cannot be blank"
// we want to count the number of unicode code points not bytes in the string
} else if utf8.RuneCountInString(form.Title) > 100 {
form.FieldErrors["title"] = "This field cannot exceed 100 characters"
}
if strings.TrimSpace(form.Content) == "" {
form.FieldErrors["content"] = "field cannot be blank"
}
if expires != 1 && expires != 7 && expires != 365 {
form.FieldErrors["expires"] = "This field must equal 1,7, or 365"
}
// If there are any validation errors, then re-display the create.tmpl template,
// passing in the snippetCreateForm instance as dynamic data in the Form
// field. Note that we use the HTTP status code 422 Unprocessable Entity
// when sending the response to indicate that there was a validation error.
if len(form.FieldErrors) > 0 {
data := newTemplateData()
data.Form = form
renderTemplate(w, r, tc, http.StatusUnprocessableEntity, "create.go.tmpl", data)
return
}
// NOTE
//
// In the case of a form field with multiple values youll need to work with the
// r.PostForm map directly. The underlying type of the r.PostForm map is url.Values,
// which in turn has the underlying type map[string][]string. So, for fields with
// multiple values you can loop over the underlying map to access them like so:
// for i, item := range r.PostForm["items"] {
// fmt.Fprintf(w, "%d: Item %s\n", i, item)
// }
id, err := snippetService.Insert(form.Title, form.Content, form.Expires)
if err != nil {
serverError(w, r, err)
}

@ -20,7 +20,7 @@ func addRoutes(mux *http.ServeMux,
// GET will match GET & HEAD http request methods
mux.Handle("GET /{$}", handleHome(logger, tc, snippetService))
mux.Handle("GET /snippet/view/{id}", handleSnippetView(logger, tc, snippetService))
mux.Handle("GET /snippet/create", handleSnippetCreateGet())
mux.Handle("GET /snippet/create", handleSnippetCreateGet(tc))
mux.Handle("POST /snippet/create", handleSnippetCreatePost(logger, tc, snippetService))
// mux.Handle("/something", handleSomething(logger, config))
// mux.Handle("/healthz", handleHealthzPlease(logger))

@ -20,6 +20,7 @@ type templateData struct {
CurrentYear int
Snippet model.Snippet
Snippets []model.Snippet
Form any
}
// newTemplateData is useful to inject default values. Example CSRF tokens for forms.

@ -0,0 +1,37 @@
{{define "title"}}Create a New Snippet{{end}}
{{define "main"}}
<form action='/snippet/create' method='POST'>
<div>
<label>Title:</label>
<!-- Use the `with` action to render the value of .Form.FieldErrors.title
if it is not empty. -->
{{with .Form.FieldErrors.title}}
<label class='error'>{{.}}</label>
{{end}}
<input type='text' name='title' value="{{.Form.Title}}">
</div>
<div>
<label>Content:</label>
<!-- Likewise render the value of .Form.FieldErrors.content if it is not
empty. -->
{{with .Form.FieldErrors.content}}
<label class='error'>{{.}}</label>
{{end}}
<textarea name='content'>{{.Form.Content}}</textarea>
</div>
<div>
<label>Delete in:</label>
<!-- And render the value of .Form.FieldErrors.expires if it is not empty. -->
{{with .Form.FieldErrors.expires}}
<label class='error'>{{.}}</label>
{{end}}
<input type='radio' name='expires' value='365' {{if (eq .Form.Expires 365)}}checked{{end}}> One Year
<input type='radio' name='expires' value='7' {{if (eq .Form.Expires 7)}}checked{{end}}> One Week
<input type='radio' name='expires' value='1' {{if (eq .Form.Expires 1)}}checked{{end}}> One Day
</div>
<div>
<input type='submit' value='Publish snippet'>
</div>
</form>
{{end}}

@ -1,5 +1,6 @@
{{define "nav" -}}
<nav>
<a href='/'>Home</a>
<a href='/snippet/create'>Create snippet</a>
</nav>
{{- end}}
Loading…
Cancel
Save