-
Notifications
You must be signed in to change notification settings - Fork 0
/
form.go
372 lines (333 loc) · 10.3 KB
/
form.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
package aform
import (
"fmt"
"golang.org/x/text/language"
"html/template"
"net/http"
)
// Form represents a form.
type Form struct {
fields []fieldInterface
autoID string
requiredCSSClass string
errorCSSClass string
labelSuffix string
bound bool
validated bool
fieldNames []string
boundData map[string][]string
cleanedData map[string][]string
errors map[string][]Error
cleanFunc func(*Form)
locales []language.Tag
}
// FormOption describes a functional option for configuring a Form.
type FormOption func(*Form) error
// FormPointerOrFieldPointer defines a union type to allow the usage of the helper
// function Must with forms and all fields types.
type FormPointerOrFieldPointer interface {
*Form | *BooleanField | *EmailField | *CharField | *ChoiceField | *MultipleChoiceField
}
// Must is a helper that wraps a call to a function returning (*Form, error)
// or (*Field, error) and panics if the error is non-nil. It is intended
// for use in form creations. e.g.
// var fld = Must(DefaultCharField("Name"))
// var f = Must(New(WithCharField(fld)))
func Must[FormPtrFieldPtr FormPointerOrFieldPointer](f FormPtrFieldPtr, err error) FormPtrFieldPtr {
if err != nil {
panic(err)
}
return f
}
// New returns a Form.
func New(opts ...FormOption) (*Form, error) {
f := &Form{
autoID: defaultAutoID,
cleanFunc: func(f *Form) {},
}
for _, opt := range opts {
if err := opt(f); err != nil {
return nil, err
}
}
return f, nil
}
// AsDiv renders the form as a list of <div> tags, with each <div> containing
// one field.
func (f *Form) AsDiv() template.HTML {
return mustFormAsDivTemplate(f)
}
// BindRequest binds req form data to the Form. After a first binding, following
// bindings are ignored. If you want to bind new data, you should create another
// identical Form to do it. Data is bound but not validated.
// Validation is done when IsValid, CleanedData or Errors are called.
// Error messages are localized according to Accept-Language header. To modify
// this behavior use directly BindData.
func (f *Form) BindRequest(req *http.Request) {
acceptLanguage := req.Header.Get("Accept-Language")
f.BindData(req.Form, acceptLanguage)
return
}
// BindData binds data to the Form. After a first binding, following bindings
// are ignored. If you want to bind new data, you should create another
// identical Form to do it. Data is bound but not validated.
// Validation is done when IsValid, CleanedData or Errors are called.
func (f *Form) BindData(data map[string][]string, langs ...string) {
if f.bound {
return
}
f.bound = true
filteredData := map[string][]string{}
for _, name := range f.fieldNames {
values, ok := data[name]
if ok {
filteredData[name] = values
}
}
f.boundData = filteredData
propagateLocalesIfNotEmpty(f.fields, []language.Tag{selectLanguage(f.locales, langs...)})
return
}
// IsBound returns true if the form is already bound to data
// with BindRequest or BindData.
func (f *Form) IsBound() bool {
return f.bound
}
// WithBooleanField returns a FormOption that adds the BooleanField fld
// to the list of fields.
func WithBooleanField(fld *BooleanField) FormOption {
return func(f *Form) error {
return f.addField(fld)
}
}
// WithCharField returns a FormOption that adds the CharField fld
// to the list of fields.
func WithCharField(fld *CharField) FormOption {
return func(f *Form) error {
return f.addField(fld)
}
}
// WithEmailField returns a FormOption that adds the EmailField fld
// to the list of fields.
func WithEmailField(fld *EmailField) FormOption {
return func(f *Form) error {
return f.addField(fld)
}
}
// WithChoiceField returns a FormOption that adds the ChoiceField fld
// to the list of fields.
func WithChoiceField(fld *ChoiceField) FormOption {
return func(f *Form) error {
return f.addField(fld)
}
}
// WithMultipleChoiceField returns a FormOption that adds the
// MultipleChoiceField fld to the list of fields.
func WithMultipleChoiceField(fld *MultipleChoiceField) FormOption {
return func(f *Form) error {
return f.addField(fld)
}
}
func (f *Form) addField(fld fieldInterface) error {
f.fields = append(f.fields, fld)
f.fieldNames = append(f.fieldNames, normalizedNameForField(fld))
propagateLabelSuffix([]fieldInterface{fld}, f.labelSuffix)
propagateRequiredCSSClassIfNotEmpty([]fieldInterface{fld}, f.requiredCSSClass)
propagateErrorCSSClassIfNotEmpty([]fieldInterface{fld}, f.errorCSSClass)
propagateLocalesIfNotEmpty([]fieldInterface{fld}, f.locales)
return propagateAutoIDIfNotDefault([]fieldInterface{fld}, f.autoID)
}
// Fields returns the list of fields added to the form. First added comes
// first.
func (f *Form) Fields() []*Field {
fields := make([]*Field, len(f.fields))
for i, fld := range f.fields {
fields[i] = fld.field()
}
return fields
}
// FieldByName returns the field with the normalized name field. If there is
// no Field matching, an error is returned.
func (f *Form) FieldByName(field string) (*Field, error) {
fld, err := f.internalFieldByName(field)
if err != nil {
return nil, err
}
return fld.field(), nil
}
func (f *Form) internalFieldByName(field string) (fieldInterface, error) {
nName := normalizedName(field)
for _, fld := range f.fields {
if fld.HTMLName() == field || fld.HTMLName() == nName {
return fld, nil
}
}
return nil, fmt.Errorf("no field with this name %s", field)
}
// WithAutoID returns a FormOption that changes how HTML IDs are automatically
// built for all the fields.
// Correct values are either an empty string or a string containing one verb
// '%s'. An empty string deactivates auto ID. It's equivalent as using
// DisableAutoID. With a string containing one verb '%s' it's possible to
// customize auto ID. e.g. "id_%s"
//
// A non-empty string without a verb '%s' is invalid.
func WithAutoID(autoID string) FormOption {
return func(f *Form) error {
if err := validAutoID(autoID); err != nil {
return err
}
return setAndPropagateAutoID(f, autoID)
}
}
// DisableAutoID returns a FormOption that deactivates the automatic ID
// creation for all the fields. When auto ID is disabled fields don't have
// a label tag unless a field override the auto ID behaviour with
// Field.SetAutoID.
func DisableAutoID() FormOption {
return func(f *Form) error {
return setAndPropagateAutoID(f, "")
}
}
func setAndPropagateAutoID(f *Form, autoID string) error {
f.autoID = autoID
return propagateAutoIDIfNotDefault(f.fields, autoID)
}
func propagateAutoIDIfNotDefault(fields []fieldInterface, autoID string) error {
if autoID == defaultAutoID {
return nil
}
for _, fld := range fields {
if fld.AutoID() != defaultAutoID {
continue
}
if err := fld.SetAutoID(autoID); err != nil {
return err
}
}
return nil
}
// WithLabelSuffix returns a FormOption that set a suffix to labels. Label
// suffix is added to all fields.
func WithLabelSuffix(labelSuffix string) FormOption {
return func(f *Form) error {
f.labelSuffix = labelSuffix
propagateLabelSuffix(f.fields, labelSuffix)
return nil
}
}
func propagateLabelSuffix(fields []fieldInterface, labelSuffix string) {
for _, fld := range fields {
if fld.LabelSuffix() != defaultLabelSuffix {
continue
}
fld.SetLabelSuffix(labelSuffix)
}
}
// WithRequiredCSSClass returns a FormOption that adds class to CSS classes
// when a field is required. CSS class is added to all required fields.
func WithRequiredCSSClass(class string) FormOption {
return func(f *Form) error {
f.requiredCSSClass = class
propagateRequiredCSSClass(f.fields, class)
return nil
}
}
func propagateRequiredCSSClassIfNotEmpty(fields []fieldInterface, class string) {
if len(class) == 0 {
return
}
propagateRequiredCSSClass(fields, class)
}
func propagateRequiredCSSClass(fields []fieldInterface, class string) {
for _, fld := range fields {
fld.SetRequiredCSSClass(class)
}
}
// WithErrorCSSClass returns a FormOption that adds class to CSS classes
// when a field validation returns an error. CSS class is added to all
// fields that have an error after validation.
func WithErrorCSSClass(class string) FormOption {
return func(f *Form) error {
f.errorCSSClass = class
propagateErrorCSSClass(f.fields, class)
return nil
}
}
func propagateErrorCSSClassIfNotEmpty(fields []fieldInterface, class string) {
if len(class) == 0 {
return
}
propagateErrorCSSClass(fields, class)
}
func propagateErrorCSSClass(fields []fieldInterface, class string) {
for _, fld := range fields {
fld.SetErrorCSSClass(class)
}
}
// WithLocales returns a FormOption that sets the list of locales used to
// translate error messages. Default locale is "en".
func WithLocales(locales []language.Tag) FormOption {
return func(f *Form) error {
f.locales = locales
propagateLocalesIfNotEmpty(f.fields, locales)
return nil
}
}
func propagateLocalesIfNotEmpty(fields []fieldInterface, locales []language.Tag) {
firstLocale := defaultLanguage
if len(locales) > 0 {
firstLocale = locales[0]
}
for _, fld := range fields {
fld.SetLocale(firstLocale)
}
}
// CleanedData maps a field normalized name to the list of bound data after validation
type CleanedData map[string][]string
// Get gets the first cleaned data associated with the given field.
// If there are no cleaned data associated with the field, Get returns
// the empty string. If the field accepts multiple data, use the map
// directly to access all of them.
func (d CleanedData) Get(field string) string {
if d == nil {
return ""
}
data, ok := d[field]
if !ok {
return ""
}
if len(data) == 0 {
return ""
}
return data[0]
}
// Has checks whether a given field has at least one cleaned data.
func (d CleanedData) Has(field string) bool {
_, ok := d[field]
return ok
}
// FormErrors maps a field normalized name to the list of errors after validation
type FormErrors map[string][]Error
// Get gets the first error associated with the given field.
// If there are no errors associated with the field, Get returns
// an empty Error with its code and message equal empty string.
// To access multiple errors, use the map directly.
func (e FormErrors) Get(field string) Error {
if e == nil {
return Error{}
}
errs, ok := e[field]
if !ok {
return Error{}
}
if len(errs) == 0 {
return Error{}
}
return errs[0]
}
// Has checks whether a given field has an error.
func (e FormErrors) Has(field string) bool {
_, ok := e[field]
return ok
}