-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathstruc2frm.go
1133 lines (979 loc) · 32.7 KB
/
struc2frm.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
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Package struc2frm creates an HTML input form
// for a given struct type;
// see README.md for details.
package struc2frm
import (
"bytes"
"encoding/json"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"path"
"reflect"
"regexp"
"runtime"
"strings"
"time"
"unicode"
"github.com/go-playground/form"
"github.com/pkg/errors"
)
var defaultHTML = ""
func init() {
_, filename, _, _ := runtime.Caller(0)
sourceDirPath := path.Join(path.Dir(filename), "tpl-main.html")
bts, err := ioutil.ReadFile(sourceDirPath)
if err != nil {
log.Printf("Could not load main template: %v", err)
defaultHTML = staticTplMainHTML
log.Printf("Loaded %v chars from static.go instead", len(staticTplMainHTML))
return
}
defaultHTML = string(bts)
}
var defaultCSS = ""
func init() {
_, filename, _, _ := runtime.Caller(0)
sourceDirPath := path.Join(path.Dir(filename), "default.css")
bts, err := ioutil.ReadFile(sourceDirPath)
if err != nil {
log.Printf("Could not load default CSS: %v", err)
defaultCSS = staticDefaultCSS // <style> embedding added while rendering
log.Printf("Loaded %v chars from static.go instead", len(staticDefaultCSS))
return
}
defaultCSS = string(bts) // <style> embedding added while rendering
}
type option struct {
Key, Val string
}
type options []option
// CardViewOptions governs the display of the rendering of Card()
type CardViewOptions struct {
SkipEmpty bool // Fields with value "" are not rendered
SuffixPos int // 0 - no suffix rendered, 1 - suffix after label, 2 - suffix after value
}
// s2FT contains formatting options for converting a struct into a HTML form
type s2FT struct {
ShowHeadline bool // show headline derived from struct name
FormTag bool // include <form...> and </form>
Name string // form name; default 'frmMain'; if distinct names are needed, application may change the values
Action string // form action - default is empty string
Method string // form method - default is POST
InstanceID string // to distinguish several instances on same website
Salt string // generated from MAC address - see below
FormTimeout int // hours until a form post is rejected - CSRF token
FocusFirstError bool // setfocus(); takes precedence over focus attribute
ForceSubmit bool // show submit, despite having only auto-changing selects
Indent int // horizontal width of the labels column
IndentAddenum int // for h3-headline and submit button, depends on CSS paddings and margins of div and input
VerticalSpacer float64 // in CSS REM
CSS string // general formatting - provided defaults can be replaced
selectOptions map[string]options // select inputs get their options from here
errors map[string]string // validation errors by json name of input
CardViewOptions
}
var addressMAC = ""
func init() {
// MAC address as salt
// run only once at init() time to save time
ifs, _ := net.Interfaces()
for _, v := range ifs {
h := v.HardwareAddr.String()
if len(h) == 0 {
continue
}
addressMAC = h
break
}
}
// New converter
func New() *s2FT {
s2f := s2FT{
ShowHeadline: false,
FormTag: true,
// Name - see below
Method: "POST",
Salt: addressMAC,
FormTimeout: 2,
selectOptions: map[string]options{},
errors: map[string]string{},
FocusFirstError: true,
ForceSubmit: false,
Indent: 0, // non-zero values override the CSS
IndentAddenum: 2 * (4 + 4), // horizontal padding and margin
VerticalSpacer: 0.6,
CSS: defaultCSS,
}
s2f.InstanceID = fmt.Sprint(time.Now().UnixNano())
s2f.InstanceID = s2f.InstanceID[len(s2f.InstanceID)-8:] // use the last 8 digits
s2f.Name = fmt.Sprintf("frmMain_%s", s2f.InstanceID)
s2f.Name = "frmMain" // form name can be changed by application
return &s2f
}
// CloneForRequest takes a package instance of s2FT
// and clones it for safe usage in parallel http requests
func (s2f *s2FT) CloneForRequest() *s2FT {
clone := *s2f
clone.errors = map[string]string{}
clone.InstanceID = fmt.Sprint(time.Now().UnixNano())
clone.InstanceID = clone.InstanceID[len(clone.InstanceID)-8:] // last 8 digits
return &clone
}
var defaultS2F = New()
/*
.+ any chars
' boundary one
(.+?) any chars, ? means not greedy
' boundary two
.+ any chars
this leads nowhere, since opening and closing boundary are the same
*/
var comma = regexp.MustCompile(`.*'(.*,.*?)'.*`)
func commaInsideQuotesBAD(s string) bool {
matches := comma.FindAllStringSubmatch(s, -1) // -1 returns all matches
log.Printf("%v matches for \n\t%v\n\t%+v\n\n", len(matches), s, matches)
return comma.MatchString(s)
}
func commaInsideQuotes(s string) bool {
parts := strings.Split(s, "'")
for idx, pt := range parts {
if idx%2 == 0 {
continue
}
// log.Printf("checking part %v - %v", idx, pt)
if strings.Contains(pt, ",") {
return true
}
}
return false
}
func (s2f *s2FT) RenderCSS(w io.Writer) {
// generic CSS
fmt.Fprint(w, "\n<style>\n")
fmt.Fprint(w, s2f.CSS)
fmt.Fprint(w, "\n</style>\n")
if s2f.Indent == 0 { // using additional generic specs - for instance with media query
return
}
// instance specific
specific := `
<style>
/* instance specifics */
div.struc2frm- label {
min-width: %vpx;
}
div.struc2frm- h3 {
margin-left: %vpx;
}
div.struc2frm- button[type=submit],
div.struc2frm- input[type=submit]
{
margin-left: %vpx;
}
</style>
`
specific = fmt.Sprintf(
specific,
s2f.Indent,
s2f.Indent+s2f.IndentAddenum,
s2f.Indent+s2f.IndentAddenum,
)
specific = strings.ReplaceAll(specific, "div.struc2frm-", fmt.Sprintf("div.struc2frm-%v", s2f.InstanceID))
fmt.Fprint(w, specific)
}
func (s2f *s2FT) verticalSpacer() string {
return fmt.Sprintf("\t<div style='height:%3.1frem'> </div>", s2f.VerticalSpacer)
}
// SetOptions to prepare dropdown/select options - with keys and labels
// for rendering in Form()
func (s2f *s2FT) SetOptions(nameJSON string, keys, labels []string) {
if s2f.selectOptions == nil {
s2f.selectOptions = map[string]options{}
}
s2f.selectOptions[nameJSON] = options{} // always reset options to prevent accumulation of options on clones
if len(keys) != len(labels) {
s2f.selectOptions[nameJSON] = append(s2f.selectOptions[nameJSON], option{"key", "keys and labels length does not match"})
} else {
for i, key := range keys {
s2f.selectOptions[nameJSON] = append(s2f.selectOptions[nameJSON], option{key, labels[i]})
}
}
}
// AddError adds a validation message;
// key 'global' writes msg on top of form.
func (s2f *s2FT) AddError(nameJSON string, msg string) {
if s2f.errors == nil {
s2f.errors = map[string]string{}
}
if _, ok := s2f.errors[nameJSON]; ok {
s2f.errors[nameJSON] += "<br>\n"
}
s2f.errors[nameJSON] += msg
}
// AddErrors adds validation messages;
// key 'global' writes msg on top of form.
func (s2f *s2FT) AddErrors(errs map[string]string) {
if s2f.errors == nil {
s2f.errors = map[string]string{}
}
for nameJSON, msg := range errs {
if _, ok := s2f.errors[nameJSON]; ok {
s2f.errors[nameJSON] += "<br>\n"
}
s2f.errors[nameJSON] += msg
}
}
// DefaultOptionKey gives the value to be selected on form init
func (s2f *s2FT) DefaultOptionKey(name string) string {
if s2f.selectOptions == nil {
return ""
}
if len(s2f.selectOptions[name]) == 0 {
return ""
}
return s2f.selectOptions[name][0].Key
}
// rendering <option val='...'>...</option> tags
func (opts options) HTML(selecteds []string) string {
w := &bytes.Buffer{}
// log.Printf("select options - selecteds %v", selecteds)
for _, o := range opts {
found := false
for _, selected := range selecteds {
if o.Key == selected {
found = true
// log.Printf("found %v", o.Key)
}
}
if found {
fmt.Fprintf(w, "\t\t<option value='%v' selected >%v</option>\n", o.Key, o.Val)
} else {
fmt.Fprintf(w, "\t\t<option value='%v' >%v</option>\n", o.Key, o.Val)
}
}
return w.String()
}
// rendering <input type='radio' /> tags
func (opts options) Radio(name string, selecteds []string) string {
w := &bytes.Buffer{}
for _, o := range opts {
checked := ""
for _, selected := range selecteds {
if o.Key == selected {
checked = "checked=\"checked\""
// log.Printf("selected %v", o.Key)
}
}
// A click on a label for a *radio* does not focus the input; possibly because input name is not unique
// Possible remedy stackoverflow.com/questions/13273806/
// rejected for its HTML ugliness
if o.Val != "" {
fmt.Fprintf(w, "\t\t<label for='%v' >%v</label>\n", name, o.Val)
}
fmt.Fprintf(w,
"\t\t<input type='radio' name='%v' value='%v' %v />\n",
name, o.Key, checked,
)
}
return w.String()
}
/*ValToString converts reflect.Value to string.
go-playground/form.Decode nicely converts all kins of request.Form strings
into the desired struct types.
But to render the form to HTML, we have to convert those types back to string.
val.String() of a bool yields "<bool Value>"
val.String() of an int yields "<int Value>"
val.String() of a float yields "<float64 Value>"
*/
func ValToString(val reflect.Value) string {
tp := val.Kind()
valStr := val.String() // trivial case
if tp == reflect.Bool {
valStr = fmt.Sprint(val.Bool())
} else if tp == reflect.Int {
valStr = fmt.Sprint(val.Int())
} else if tp == reflect.Float64 {
valStr = fmt.Sprint(val.Float())
}
return valStr
}
// golang type and 'form' struct tag 'subtype' => html input type
func toInputType(t, attrs string) string {
switch t {
case "string", "[]string":
switch structTag(attrs, "subtype") { // various possibilities - distinguish by subtype
case "separator":
return "separator"
case "fieldset":
return "fieldset"
case "date":
return "date"
case "time":
return "time"
case "textarea":
return "textarea"
case "select":
return "select"
case "radiogroup":
return "radiogroup"
}
return "text"
case "int", "float64", "[]int", "[]float64":
switch structTag(attrs, "subtype") { // might want dropdown, for instance for list of years
case "select":
return "select"
}
return "number"
case "bool", "[]bool":
switch structTag(attrs, "subtype") { // not always checkbox, but sometimes dropdown
case "select":
return "select"
}
return "checkbox"
case "[]uint8":
return "file"
}
return "text"
}
// parsing the struct tag 'form';
// returning a *single* value for argument key;
// i.e. "maxlength='42',size='28',suffix='optional'"
// key=size
// returns 28
func structTag(tags, key string) string {
tagss := strings.Split(tags, ",")
for _, a := range tagss {
aLow := strings.ToLower(a)
if strings.HasPrefix(aLow, key) {
kv := strings.Split(a, "=")
if len(kv) == 2 {
return strings.Trim(kv[1], "'")
}
}
}
return ""
}
// convert the struct tag 'form' to html input attributes;
// mostly replacing comma with single space;
// i.e. "maxlength='42',size='28',suffix='optional'"
func structTagsToAttrs(tags string) string {
tagss := strings.Split(tags, ",")
ret := ""
for _, t := range tagss {
t = strings.TrimSpace(t)
tl := strings.ToLower(t) // tag lower
switch {
case strings.HasPrefix(tl, "subtype="): // string - [date,textarea,select] - not an HTML attribute; kept for debugging
ret += " " + t
case strings.HasPrefix(tl, "size="): // visible width of input field
ret += " " + t
case strings.HasPrefix(tl, "maxlength="): // digits of input data
ret += " " + t
case strings.HasPrefix(tl, "max="): // for input number
ret += " " + t
case strings.HasPrefix(tl, "min="): // for input number
ret += " " + t
case strings.HasPrefix(tl, "step="): // for input number - special value 'any'
ret += " " + t
case strings.HasPrefix(tl, "pattern="): // client side validation; i.e. date layout [0-9\\.\\-/]{10}
ret += " " + t
case strings.HasPrefix(tl, "placeholder="): // a watermark showing expected input; i.e. 2006/01/02 15:04
ret += " " + t
case strings.HasPrefix(tl, "rows="): // for texarea
ret += " " + t
case strings.HasPrefix(tl, "cols="): // for texarea
ret += " " + t
case strings.HasPrefix(tl, "accept="): // file upload extension
ret += " " + t
case strings.HasPrefix(tl, "onchange"): // file upload extension
ret += " " + "onchange='javascript:this.form.submit();'"
case strings.HasPrefix(tl, "wildcardselect"): // show extra input next to select - to select options
ret += " " + t
case strings.HasPrefix(tl, "accesskey="): // goes into input, not into label
ret += " " + t
case strings.HasPrefix(tl, "title="): // mouse over tooltip - alt
ret += " " + t
case strings.HasPrefix(tl, "autocapitalize="): // 'off' prevents upper case for first word on mobile phones
ret += " " + t
case strings.HasPrefix(tl, "inputmode="): // 'numeric' shows only numbers keysboard on mobile phones
ret += " " + t
case strings.HasPrefix(tl, "multiple"): // dropdown/select - select multiple items; no value
ret += " " + "multiple" // only the attribute; no value
case strings.HasPrefix(tl, "autofocus"):
ret += " " + "autofocus" // only the attribute; no value
default:
// "label=" is not converted into an attribute
// "label-style=" ~
// "suffix=" ~
// "nobreak=" ~
}
}
return ret
}
// for example 'Date layout' with accesskey 't' becomes 'Da<u>t</u>e layout'
func accessKeyify(s, attrs string) string {
ak := structTag(attrs, "accesskey")
if ak == "" {
return s
}
akr := rune(ak[0])
akrUp := unicode.ToUpper(akr)
s2 := []rune{}
found := false
// log.Printf("-%s- -%s-", s, ak)
for _, ru := range s {
// log.Printf("\tcomparing %#U to %#U - %#U", ru, akr, akrUp)
if (ru == akr || ru == akrUp) && !found {
s2 = append(s2, '<', 'u', '>')
s2 = append(s2, ru)
s2 = append(s2, '<', '/', 'u', '>')
found = true
continue
}
s2 = append(s2, ru)
}
return string(s2)
}
// labelize converts struct field names and json field names
// to human readable format:
// bond_fund => Bond fund
// bondFund => Bond fund
// bondFUND => Bond fund
//
// edge case: BONDFund would be converted to 'Bondfund'
func labelize(s string) string {
rs := make([]rune, 0, len(s))
previousUpper := false
for i, char := range s {
if i == 0 {
rs = append(rs, unicode.ToUpper(char))
previousUpper = true
} else {
if char == '_' {
char = ' '
}
if unicode.ToUpper(char) == char {
if !previousUpper && char != ' ' {
rs = append(rs, ' ')
}
rs = append(rs, unicode.ToLower(char))
previousUpper = true
} else {
rs = append(rs, char)
previousUpper = false
}
}
}
return string(rs)
}
// ParseMultipartForm parses an HTTP request form
// with file attachments
func ParseMultipartForm(r *http.Request) error {
if r.Method == "GET" {
return nil
}
const _24K = (1 << 20) * 24
err := r.ParseMultipartForm(_24K)
if err != nil {
log.Printf("Parse multipart form error: %v\n", err)
return err
}
return nil
}
// ExtractUploadedFile extracts a file from an HTTP POST request.
// It needs the request form to be prepared with ParseMultipartForm.
func ExtractUploadedFile(r *http.Request, names ...string) (bts []byte, fname string, err error) {
if r.Method == "GET" {
return
}
name := "upload"
if len(names) > 0 {
name = names[0]
}
_, fheader, err := r.FormFile(name)
if err != nil {
log.Printf("Error unpacking upload bytes from post request: %v\n", err)
return
}
fname = fheader.Filename
log.Printf("Uploaded filename = %+v", fname)
rdr, err := fheader.Open()
if err != nil {
log.Printf("Error opening uploaded file: %v\n", err)
return
}
defer rdr.Close()
bts, err = ioutil.ReadAll(rdr)
if err != nil {
log.Printf("Error reading uploaded file: %v\n", err)
return
}
log.Printf("Extracted %v bytes from uploaded file", len(bts))
return
}
// Form takes a struct instance
// and turns it into an HTML form.
func (s2f *s2FT) Form(intf interface{}) template.HTML {
v := reflect.ValueOf(intf) // interface val
typeOfS := v.Type()
// v = v.Elem() // de reference
if v.Kind().String() != "struct" {
return template.HTML(fmt.Sprintf("struct2form.Form() - arg1 must be struct - is %v", v.Kind()))
}
w := &bytes.Buffer{}
needSubmit := false // only select with onchange:submit() ?
// collect fields with initial focus and fields with errors
inputWithFocus := "" // first input having an autofocus attribute
firstInputWithError := "" // first input having an error message
if s2f.FocusFirstError {
for i := 0; i < v.NumField(); i++ {
inpName := typeOfS.Field(i).Tag.Get("json") // i.e. date_layout
inpName = strings.Replace(inpName, ",omitempty", "", -1)
_, hasError := s2f.errors[inpName]
if hasError {
firstInputWithError = inpName
break
}
}
}
// error focus takes precedence over init focus
if firstInputWithError != "" {
inputWithFocus = firstInputWithError
} else {
for i := 0; i < v.NumField(); i++ {
inpName := typeOfS.Field(i).Tag.Get("json") // i.e. date_layout
inpName = strings.Replace(inpName, ",omitempty", "", -1)
attrs := typeOfS.Field(i).Tag.Get("form") // i.e. form:"maxlength='42',size='28'"
if structTag(attrs, "autofocus") != "" {
inputWithFocus = inpName
}
}
}
s2f.RenderCSS(w)
// one class selector for general - one for specific instance
fmt.Fprintf(w, "<div class='struc2frm struc2frm-%v'>\n", s2f.InstanceID)
if s2f.ShowHeadline {
fmt.Fprintf(w, "<h3>%v</h3>\n", labelize(typeOfS.Name()))
}
// file upload requires distinct form attribute
uploadPostForm := false
for i := 0; i < v.NumField(); i++ {
tp := v.Field(i).Type().Name() // primitive type name: string, int
if typeOfS.Field(i).Type.Kind() == reflect.Slice {
tp = "[]" + typeOfS.Field(i).Type.Elem().Name()
}
if toInputType(tp, "") == "file" {
uploadPostForm = true
break
}
}
if s2f.FormTag {
if uploadPostForm {
fmt.Fprintf(w, "<form name='%v' action='%v' method='POST' enctype='multipart/form-data'>\n", s2f.Name, s2f.Action)
} else {
// browser default encoding for post is "application/x-www-form-urlencoded"
fmt.Fprintf(w, "<form name='%v' action='%v' method='%v' >\n", s2f.Name, s2f.Action, s2f.Method)
}
}
if errMsg, ok := s2f.errors["global"]; ok {
fmt.Fprintf(w, "\t<p class='error-block' >%v</p>\n", errMsg)
}
fmt.Fprintf(w, "\t<input name='token' type='hidden' value='%v' />\n", s2f.FormToken())
fieldsetOpen := false
// Render fields
for i := 0; i < v.NumField(); i++ {
// struct field name; i.e. Name, Birthdate
fn := typeOfS.Field(i).Name
if fn[0:1] != strings.ToUpper(fn[0:1]) { // only used to find unexported fields; otherwise json tag name is used
continue // skip unexported
}
inpName := typeOfS.Field(i).Tag.Get("json") // i.e. date_layout
inpName = strings.Replace(inpName, ",omitempty", "", -1)
inpLabel := labelize(inpName)
attrs := typeOfS.Field(i).Tag.Get("form") // i.e. form:"maxlength='42',size='28'"
if structTag(attrs, "label") != "" {
inpLabel = structTag(attrs, "label")
}
if strings.Contains(attrs, ", ") || strings.Contains(attrs, ", ") {
return template.HTML(fmt.Sprintf("struct2form.Form() - field %v: tag 'form' cannot contain ', ' or ' ,' ", inpName))
}
if commaInsideQuotes(attrs) {
return template.HTML(fmt.Sprintf("struct2form.Form() - field %v: tag 'form' - use , instead of ',' inside of single quotes values", inpName))
}
if attrs == "-" {
continue
}
// getting the value and the type of the iterated struct field
val := v.Field(i)
if false {
// if our entry form struct would contain pointer fields...
val = reflect.Indirect(val) // pointer converted to value
val = reflect.Indirect(val) // idempotent
val = val.Elem() // what is the difference?
}
tp := v.Field(i).Type().Name() // primitive type name: string, int
if typeOfS.Field(i).Type.Kind() == reflect.Slice {
tp = "[]" + v.Type().Field(i).Type.Elem().Name() // []byte => []uint8
}
valStr := ValToString(val)
valStrs := []string{valStr} // for select multiple='false'
// for select multiple='true'
// if tp == []string or []int or []float64 ...
// unpack slice from checkbox arrays or select/dropdown multiple
if typeOfS.Field(i).Type.Kind() == reflect.Slice {
// valSlice := reflect.MakeSlice(val.Type(), val.Cap(), val.Len())
// valSlice := val.Slice(0, val.Len())
valSlice := val // same as above
// log.Printf(
// "kind of type %v - elem type %v - capacity %v - length %v - %v",
// valSlice.Kind(), val.Type(), valSlice.Cap(), valSlice.Len(), valSlice,
// )
valStrs = []string{} // reset
for i := 0; i < valSlice.Len(); i++ {
// see package fmt/print.go - printValue()::865
// vx := valSlice.Slice(i, i+1) // this woudl be a subslice
valElem := valSlice.Index(i) // this is an element of a slice
// log.Printf("Elem is %v", ValToString(valElem))
valStrs = append(valStrs, ValToString(valElem))
}
// select multiple:
// having extracted valStrs, we want prevent additive request into form parsing;
// but CanSet() yields false;
// better setting init values *conditionally* *after* parsing
if valSlice.CanSet() && false {
log.Printf("%v - %v - %v - trying slice reset", inpName, tp, val.Type())
valueSlice := reflect.MakeSlice(val.Type(), 0, 5)
valueSlice.Set(valueSlice)
}
}
errMsg, hasError := s2f.errors[inpName]
if hasError {
fmt.Fprintf(w, "\t<p class='error-block' >%v</p>\n", errMsg)
}
labelStyle := structTag(attrs, "label-style") // for instance irregular width - overriding CSS style
// label positioning for tall inputs
specialVAlign := ""
if toInputType(tp, attrs) == "textarea" {
specialVAlign = "vertical-align: top;"
}
if toInputType(tp, attrs) == "select" {
if structTag(attrs, "multiple") != "" {
specialVAlign = "vertical-align: top;"
}
}
if toInputType(tp, attrs) != "separator" &&
toInputType(tp, attrs) != "fieldset" {
fmt.Fprintf(w,
"\t<label for='%s' style='%v%v' >%v</label>\n", // no whitespace - input immediately afterwards
inpName, labelStyle, specialVAlign, accessKeyify(inpLabel, attrs),
)
}
// various inputs
switch toInputType(tp, attrs) {
case "checkbox":
needSubmit = true
checked := ""
if val.Bool() {
checked = "checked"
}
fmt.Fprintf(w, "\t<input type='%v' name='%v' id='%v' value='%v' %v %v />\n", toInputType(tp, attrs), inpName, inpName, "true", checked, structTagsToAttrs(attrs))
fmt.Fprintf(w, "\t<input type='hidden' name='%v' value='false' />", inpName)
case "file":
needSubmit = true
// <input type="file" name="upload" id="upload" value="ignored.json" accept=".json" >
fmt.Fprintf(w, "\t<input type='%v' name='%v' id='%v' value='%v' %v />",
toInputType(tp, attrs), inpName, inpName, "ignored.json", structTagsToAttrs(attrs),
)
case "date", "time":
needSubmit = true
// <input type="date" name="myDate" max="1989-10-29" min="2001-01-02">
fmt.Fprintf(w, "\t<input type='%v' name='%v' id='%v' value='%v' %v />",
toInputType(tp, attrs), inpName, inpName, val, structTagsToAttrs(attrs),
)
case "textarea":
needSubmit = true
fmt.Fprintf(w, "\t<textarea name='%v' id='%v' %v />",
inpName, inpName, structTagsToAttrs(attrs),
)
fmt.Fprint(w, val)
fmt.Fprintf(w, "</textarea>")
case "radiogroup":
if structTag(attrs, "onchange") == "" {
needSubmit = true // select without auto submit => needs submit button
}
fmt.Fprint(w, "\t<div class='select-arrow'>\n")
fmt.Fprint(w, "\t<div class='radio-group'>\n")
fmt.Fprint(w, s2f.selectOptions[inpName].Radio(inpName, valStrs))
fmt.Fprint(w, "\t</div>")
fmt.Fprint(w, "\t</div>")
case "select":
if structTag(attrs, "onchange") == "" {
needSubmit = true // select without auto submit => needs submit button
}
fmt.Fprint(w, "\t<div class='select-arrow'>\n")
fmt.Fprintf(w, "\t<select name='%v' id='%v' %v />\n", inpName, inpName, structTagsToAttrs(attrs))
fmt.Fprint(w, s2f.selectOptions[inpName].HTML(valStrs))
fmt.Fprint(w, "\t</select>\n")
fmt.Fprint(w, "\t</div>")
if structTag(attrs, "wildcardselect") != "" {
fmt.Fprint(w, "\t\t<div class='wildcardselect'>\n")
// onchange only triggers on blur
// onkeydown makes too much noise
// oninput is just perfect
fmt.Fprintf(w, ` <input type='text' name='%v' id='%v' value='%v'
title='case sensitive | multiple patterns with * | separated by ; | ! negates'
oninput='javascript:selectOptions(this);'
maxlength='40'
xxtabindex=-1
placeholder='a*;b*'
/>`,
inpName+"_so",
inpName+"_so",
"",
)
fmt.Fprint(w, "\n\t\t</div>")
/*
JS function is printed repeatedly for multiple selects
and multiple forms per request.
The complexity of keeping track would be even more ugly.
*/
fmt.Fprintf(w, `
<script type="text/javascript">
var wildcardselectDebug = false;
function matchRule(str, rule) {
// define an arrow function with =>
// creating the func escapeRegex()
// escape all regex control characters; i.e. [ with \[
// this could be moved out into a plain JS function
var escapeRegex = (strArg) => strArg.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
// split by *
// escape regex chars of the parts
// join by .*
// "." matches single character, except newline or line terminator
// ".*" matches any string containing zero or more characters
rule = rule.split("*").map(escapeRegex).join(".*");
// "^" is expression start
// "$" is expression end
rule = "^" + rule + "$"
if (wildcardselectDebug) {
console.log(" testing rule '" + rule + "' on str '" + str + "'");
}
// create a regular expression object for matching string
var regex = new RegExp(rule);
//Returns true if it finds a match, otherwise it returns false
return regex.test(str);
}
function selectOptions(src) {
// console.log(src)
if (src) {
var myName = src.getAttribute("name");
// console.log("on input " + myName);
var selectName = myName.substring(0, myName.length - 3);
// console.log(" corresponding select is " + selectName);
var select = document.getElementById(selectName);
if (select) {
var wildcards = src.value;
var wildcardsArray = wildcards.split(";");
for (idx = 0; idx < wildcardsArray.length; ++idx) {
var wildcard = wildcardsArray[idx];
var negate = false;
if (wildcard.charAt(0) === "!") {
wildcard = wildcard.substring(1);
var negate = true;
}
for (var i = 0, l = select.options.length, o; i < l; i++) {
o = select.options[i];
var doesMatch = matchRule(o.text, wildcard);
// if (negate) {
// doesMatch = !doesMatch;
// }
if (doesMatch && !negate) {
o.selected = true;
if (wildcardselectDebug) {
console.log(" selected " + o.text + " - wildcard '" + wildcard + "' - negation " + negate);
}
} else if (doesMatch && negate) {
o.selected = false;
if (wildcardselectDebug) {
console.log(" unselected " + o.text + " - wildcard '" + wildcard + "' - negation " + negate);
}
} else {
if (wildcardselectDebug) {
console.log(" no match " + o.text + " - wildcard '" + wildcard + "' - negation " + negate);
}
}
}
}
}
}
}
</script>
`)
}
case "separator":
// when separator has an explicit label value
if structTag(attrs, "label") != "" {
fmt.Fprintf(w, "\t<div class='struc2frm-static'>%v</div>", inpLabel)
} else {
fmt.Fprint(w, "\t<div class='separator'></div>")
}
case "fieldset":
if fieldsetOpen {
fmt.Fprint(w, "</fieldset>\n")
}
fmt.Fprint(w, "<fieldset>")
fmt.Fprintf(w, "\t<legend> %v </legend>", inpLabel)
fieldsetOpen = true
default:
// plain vanilla input
needSubmit = true
fmt.Fprintf(w, "\t<input type='%v' name='%v' id='%v' value='%v' %v />", toInputType(tp, attrs), inpName, inpName, val, structTagsToAttrs(attrs))
}
sfx := structTag(attrs, "suffix")
if sfx != "" {
fmt.Fprintf(w, "<span class='postlabel' >%s</span>", sfx)
}
if toInputType(tp, attrs) != "separator" &&
toInputType(tp, attrs) != "fieldset" &&
structTag(attrs, "nobreak") == "" {
fmt.Fprintf(w, "\n")
fmt.Fprintf(w, s2f.verticalSpacer())
}
// close input with newline
fmt.Fprintf(w, "\n")
}
if fieldsetOpen {
fmt.Fprint(w, "</fieldset>\n")
}
if needSubmit || s2f.ForceSubmit {
// name should *not* be 'submit'
// avoiding error on this.form.submit()
// 'submit is not a function' stackoverflow.com/questions/833032/
fmt.Fprintf(w, "\t<button type='submit' name='btnSubmit' value='1' accesskey='s' ><b>S</b>ubmit</button>\n%v\n", s2f.verticalSpacer())
} else {
fmt.Fprintf(w, "\t<input type='hidden' name='btnSubmit' value='1'\n")
}
if s2f.FormTag {
fmt.Fprint(w, "</form>\n")
}
fmt.Fprint(w, "</div><!-- </div class='struc2frm'... -->\n")
if inputWithFocus != "" {
// finding form by name - setting focus by name;