go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/viewutil/form_for.go (about) 1 /* 2 3 Copyright (c) 2023 - Present. Will Charczuk. All rights reserved. 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository. 5 6 */ 7 8 package viewutil 9 10 import ( 11 "fmt" 12 "html/template" 13 "reflect" 14 "strings" 15 "time" 16 17 "go.charczuk.com/sdk/stringutil" 18 ) 19 20 // FormFor yields an html form input for a given value. 21 // 22 // The value is reflected and the `form:"..."` struct tags 23 // are used to set options for individual fields. 24 func FormFor(obj any, action string) template.HTML { 25 if typed, ok := obj.(FormProvider); ok { 26 return typed.Form(action) 27 } 28 29 rt := reflect.TypeOf(obj) 30 rv := reflect.ValueOf(obj) 31 sb := new(strings.Builder) 32 sb.WriteString(fmt.Sprintf(`<form method="POST" data-form-for="%s" action="%s">`, rt.Name(), action)) 33 sb.WriteString("\n") 34 for x := 0; x < rt.NumField(); x++ { 35 f := rt.Field(x) 36 v := rv.Field(x) 37 input := controlForField(f, v) 38 if input != "" { 39 sb.WriteString(input) 40 sb.WriteString("\n") 41 } 42 } 43 sb.WriteString(`<input type="submit">Submit</input>`) 44 sb.WriteString("\n") 45 sb.WriteString("</form>") 46 return template.HTML(sb.String()) 47 } 48 49 // FormProvider will shortcut reflection in the `FormFor` call. 50 type FormProvider interface { 51 Form(string) template.HTML 52 } 53 54 // InputProvider will shortcut reflection in the input generation step 55 // of a `FormFor` or `ControlFor` call. 56 type InputProvider interface { 57 Input() template.HTML 58 } 59 60 // ControlFor returns just the input for a given struct field by name. 61 func ControlFor(obj any, fieldName string) template.HTML { 62 rt := reflect.TypeOf(obj) 63 fieldType, ok := rt.FieldByName(fieldName) 64 if !ok { 65 return "" 66 } 67 rv := reflect.ValueOf(obj) 68 fieldValue := rv.FieldByName(fieldName) 69 return template.HTML(controlForField(fieldType, fieldValue)) 70 } 71 72 func controlForField(field reflect.StructField, fieldValue reflect.Value) string { 73 tag := parseFormStructTag(field.Tag.Get("form")) 74 if tag.Skip { 75 return "" 76 } 77 78 // drill through the value itself 79 // and if it's not nil, make a string version of it 80 for fieldValue.Kind() == reflect.Ptr { 81 fieldValue = fieldValue.Elem() 82 } 83 fieldValueElem := fieldValue.Interface() 84 85 inputName := field.Name 86 var label string 87 if tag.Label != "" { 88 label = fmt.Sprintf(`<label for="%s">%s</label>`, inputName, tag.Label) 89 } 90 91 var attrs []string 92 attrs = append(attrs, fmt.Sprintf(`name="%s"`, inputName)) 93 attrs = append(attrs, inputTypeForControl(field, fieldValue, fieldValueElem, tag)) 94 attrs = append(attrs, tag.Attrs...) 95 96 switch t := fieldValueElem.(type) { 97 case bool: 98 if t { 99 attrs = append(attrs, "checked") 100 } 101 case *bool: 102 if t != nil && *t { 103 attrs = append(attrs, "checked") 104 } 105 default: 106 var value string 107 if fieldValueElem != nil { 108 value = fmt.Sprint(fieldValueElem) 109 } 110 if value != "" { 111 attrs = append(attrs, fmt.Sprintf(`value="%s"`, value)) 112 } 113 } 114 return label + "<input " + strings.Join(attrs, " ") + "/>" 115 } 116 117 func inputTypeForControl(fieldType reflect.StructField, fieldTypeValue reflect.Value, fieldValue any, tag formStructTag) string { 118 if tag.InputType != "" { 119 return fmt.Sprintf(`type="%s"`, tag.InputType) 120 } 121 switch fieldValue.(type) { 122 case string, *string: 123 return `type="text"` 124 case time.Time, *time.Time: 125 return `type="datetime-local"` 126 case bool, *bool: 127 return `type="checkbox"` 128 case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: 129 return `type="number"` 130 case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64, *float32, *float64: 131 return `type="number"` 132 default: 133 return `type="text"` 134 } 135 } 136 137 type formStructTag struct { 138 Skip bool 139 Label string 140 InputType string 141 Attrs []string 142 } 143 144 func parseFormStructTag(tagValue string) (output formStructTag) { 145 if tagValue == "-" { 146 output.Skip = true 147 return 148 } 149 150 fields := stringutil.SplitQuoted(tagValue, ",") 151 for _, field := range fields { 152 key, value, _ := strings.Cut(field, "=") 153 switch key { 154 case "label": 155 output.Label = stringutil.TrimQuotes(value) 156 case "type": 157 output.InputType = stringutil.TrimQuotes(value) 158 default: 159 output.Attrs = append(output.Attrs, field) 160 } 161 } 162 return 163 }