Skip to content

Commit 5bb5ba8

Browse files
committed
Form validation with Zog
1 parent 5b619f7 commit 5bb5ba8

File tree

9 files changed

+380
-50
lines changed

9 files changed

+380
-50
lines changed

app/forms/forms.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package forms
2+
3+
type Errors = map[string][]string
4+
5+
func EmptyErrors() Errors {
6+
return make(map[string][]string)
7+
}
8+
9+
func HasError(errors Errors, name string) bool {
10+
_, ok := errors[name]
11+
return ok
12+
}
13+
14+
func GetErrors(errors Errors, name string) []string {
15+
errs, ok := errors[name]
16+
17+
if !ok {
18+
return []string{}
19+
}
20+
21+
return errs
22+
}

app/forms/spaces.forms.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package forms
2+
3+
import (
4+
"net/http"
5+
6+
z "github.com/Oudwins/zog"
7+
"github.com/Oudwins/zog/zhttp"
8+
)
9+
10+
type CreateSpace struct {
11+
Name string `zog:"name"`
12+
Identity string `zog:"identity"`
13+
Email string `zog:"email"`
14+
Code string `zog:"code"`
15+
}
16+
17+
func createSpaceSchema(requiresCode bool) *z.StructSchema {
18+
codeSchema := z.String().Trim()
19+
if requiresCode {
20+
codeSchema = codeSchema.Required()
21+
}
22+
23+
return z.Struct(z.Schema{
24+
"name": z.String().Trim().Required().Max(255),
25+
"identity": z.String().Trim().Required().Max(255),
26+
"email": z.String().Trim().Required().Email().Max(255),
27+
"code": codeSchema,
28+
})
29+
}
30+
31+
func ParseCreateSpace(r *http.Request, requiresCode bool) (CreateSpace, map[string][]string) {
32+
var form CreateSpace
33+
if !requiresCode {
34+
form.Code = "placeholder"
35+
}
36+
37+
errs := createSpaceSchema(requiresCode).Parse(zhttp.Request(r), &form)
38+
if errs == nil {
39+
return form, nil
40+
}
41+
42+
return form, z.Issues.SanitizeMap(errs)
43+
}

app/handlers/spaces.handlers.go

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import (
66
"fmt"
77
"log/slog"
88
"net/http"
9-
"strings"
109
"time"
1110

1211
"github.com/gorilla/sessions"
1312
"github.com/nicolashery/simply-shared-notes/app/access"
1413
"github.com/nicolashery/simply-shared-notes/app/config"
1514
"github.com/nicolashery/simply-shared-notes/app/db"
15+
"github.com/nicolashery/simply-shared-notes/app/forms"
1616
"github.com/nicolashery/simply-shared-notes/app/publicid"
1717
"github.com/nicolashery/simply-shared-notes/app/session"
1818
"github.com/nicolashery/simply-shared-notes/app/views/pages"
@@ -22,42 +22,23 @@ func handleSpacesNew(cfg *config.Config) http.HandlerFunc {
2222
return func(w http.ResponseWriter, r *http.Request) {
2323
requiresCode := cfg.RequiresInvitationCode()
2424

25-
var code string
25+
var form forms.CreateSpace
2626
if requiresCode {
27-
code = r.URL.Query().Get("code")
27+
form.Code = r.URL.Query().Get("code")
2828
}
2929

30-
pages.SpacesNew(requiresCode, code).Render(r.Context(), w)
30+
pages.SpacesNew(requiresCode, &form, forms.EmptyErrors()).Render(r.Context(), w)
3131
}
3232
}
3333

34-
type CreateSpaceForm struct {
35-
Name string
36-
Identity string
37-
Email string
38-
Code string
39-
}
40-
41-
func parseCreateSpaceForm(r *http.Request, f *CreateSpaceForm) error {
42-
err := r.ParseForm()
43-
if err != nil {
44-
return err
45-
}
46-
47-
f.Name = strings.Trim(r.Form.Get("name"), " ")
48-
f.Identity = strings.Trim(r.Form.Get("identity"), " ")
49-
f.Email = strings.Trim(r.Form.Get("email"), " ")
50-
f.Code = strings.Trim(r.Form.Get("code"), " ")
51-
52-
return nil
53-
}
54-
5534
func handleSpacesCreate(cfg *config.Config, logger *slog.Logger, sqlDB *sql.DB, queries *db.Queries, sessionStore *sessions.CookieStore) http.HandlerFunc {
5635
return func(w http.ResponseWriter, r *http.Request) {
57-
var form CreateSpaceForm
58-
err := parseCreateSpaceForm(r, &form)
59-
if err != nil {
60-
http.Error(w, "failed to parse form", http.StatusBadRequest)
36+
requiresCode := cfg.RequiresInvitationCode()
37+
38+
form, errors := forms.ParseCreateSpace(r, requiresCode)
39+
if errors != nil {
40+
w.WriteHeader(http.StatusUnprocessableEntity)
41+
pages.SpacesNew(requiresCode, &form, errors).Render(r.Context(), w)
6142
return
6243
}
6344

@@ -120,7 +101,7 @@ func createSpaceAndFirstMember(
120101
ctx context.Context,
121102
sqlDB *sql.DB,
122103
queries *db.Queries,
123-
form CreateSpaceForm,
104+
form forms.CreateSpace,
124105
now time.Time,
125106
tokens access.AccessTokens,
126107
memberPublicId string,

app/views/layouts/space.layout.templ

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,10 @@ templ userMenu(access *access.Access, identity *identity.Identity) {
212212
</a>
213213
</li>
214214
<li>
215-
<a class="flex">
215+
<a
216+
href={ templ.URL("/new") }
217+
class="flex"
218+
>
216219
<svg
217220
class="w-5 h-5"
218221
xmlns="http://www.w3.org/2000/svg"

app/views/layouts/space.layout_templ.go

Lines changed: 12 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/views/pages/spaces_new.page.templ

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package pages
22

3-
import "github.com/nicolashery/simply-shared-notes/app/views/layouts"
3+
import (
4+
"github.com/nicolashery/simply-shared-notes/app/forms"
5+
"github.com/nicolashery/simply-shared-notes/app/views/layouts"
6+
)
47

5-
templ SpacesNew(requiresCode bool, code string) {
8+
templ SpacesNew(requiresCode bool, form *forms.CreateSpace, errors forms.Errors) {
69
@layouts.Landing("Create space") {
710
<div class="max-w-md mx-auto py-4">
811
<h1 class="text-2xl font-bold mb-1.5">Create a new space</h1>
@@ -12,23 +15,67 @@ templ SpacesNew(requiresCode bool, code string) {
1215
<form method="POST" action="/new" class="flex flex-col gap-4">
1316
<fieldset class="flex flex-col gap-1.5">
1417
<label for="name" class="text-sm font-semibold">Space name</label>
15-
<input type="text" name="name" id="name" class="input w-full"/>
18+
<input
19+
type="text"
20+
name="name"
21+
id="name"
22+
value={ form.Name }
23+
class={ "input w-full", templ.KV("input-error", forms.HasError(errors, "name")) }
24+
/>
25+
for _, e := range forms.GetErrors(errors, "name") {
26+
<p class="text-sm text-error">
27+
{ e }
28+
</p>
29+
}
1630
<p class="text-sm opacity-60">You can always change the name later in the space settings.</p>
1731
</fieldset>
1832
<fieldset class="flex flex-col gap-1.5">
1933
<label for="identity" class="text-sm font-semibold">Your name</label>
20-
<input type="text" name="identity" id="identity" class="input w-full"/>
34+
<input
35+
type="text"
36+
name="identity"
37+
id="identity"
38+
value={ form.Identity }
39+
class={ "input w-full", templ.KV("input-error", forms.HasError(errors, "identity")) }
40+
/>
41+
for _, e := range forms.GetErrors(errors, "identity") {
42+
<p class="text-sm text-error">
43+
{ e }
44+
</p>
45+
}
2146
<p class="text-sm opacity-60">We'll use this to create the first member of your new space. You can add more later from the members page.</p>
2247
</fieldset>
2348
<fieldset class="flex flex-col gap-1.5">
2449
<label for="email" class="text-sm font-semibold">Your email</label>
25-
<input type="email" name="email" id="email" class="input w-full"/>
50+
<input
51+
type="email"
52+
name="email"
53+
id="email"
54+
value={ form.Email }
55+
class={ "input w-full", templ.KV("input-error", forms.HasError(errors, "email")) }
56+
/>
57+
for _, e := range forms.GetErrors(errors, "email") {
58+
<p class="text-sm text-error">
59+
{ e }
60+
</p>
61+
}
2662
<p class="text-sm opacity-60">Your email is only used to send you the secure access link to your space. We will never spam or share your email with a third party.</p>
2763
</fieldset>
2864
if requiresCode {
2965
<fieldset class="flex flex-col gap-1.5">
3066
<label for="code" class="text-sm font-semibold">Invitation code</label>
31-
<input type="text" name="code" id="code" value={ code } class="input w-full"/>
67+
<input
68+
type="text"
69+
name="code"
70+
id="code"
71+
value={ form.Code }
72+
class={ "input w-full", templ.KV("input-error", forms.HasError(errors, "code")) }
73+
/>
74+
for _, e := range forms.GetErrors(errors, "code") {
75+
<p class="text-sm text-error">
76+
{ e }
77+
</p>
78+
}
3279
<p class="text-sm opacity-60">Creating spaces is only possible with an invitation code.</p>
3380
</fieldset>
3481
}

0 commit comments

Comments
 (0)