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  }