diff --git a/components/input/value-binder.go b/components/input/value-binder.go index d024f5c..c420c44 100644 --- a/components/input/value-binder.go +++ b/components/input/value-binder.go @@ -3,6 +3,7 @@ package input import ( "encoding" "fmt" + "reflect" "strconv" "time" ) @@ -44,65 +45,14 @@ func (f ValueBindAny) StringValue() string { case encoding.TextMarshaler: data, _ := v.MarshalText() // Ignore any errors, as we can't handle them here. return string(data) - case *bool: - return strconv.FormatBool(*v) - case bool: - return strconv.FormatBool(v) - case *string: - return *v - case string: - return v - case *int: - return strconv.FormatInt(int64(*v), 10) - case int: - return strconv.FormatInt(int64(v), 10) - case *int8: - return strconv.FormatInt(int64(*v), 10) - case int8: - return strconv.FormatInt(int64(v), 10) - case *int16: - return strconv.FormatInt(int64(*v), 10) - case int16: - return strconv.FormatInt(int64(v), 10) - case *int32: - return strconv.FormatInt(int64(*v), 10) - case int32: - return strconv.FormatInt(int64(v), 10) - case *int64: - return strconv.FormatInt(int64(*v), 10) - case int64: - return strconv.FormatInt(int64(v), 10) - case *uint: - return strconv.FormatUint(uint64(*v), 10) - case uint: - return strconv.FormatUint(uint64(v), 10) - case *uint8: - return strconv.FormatUint(uint64(*v), 10) - case uint8: - return strconv.FormatUint(uint64(v), 10) - case *uint16: - return strconv.FormatUint(uint64(*v), 10) - case uint16: - return strconv.FormatUint(uint64(v), 10) - case *uint32: - return strconv.FormatUint(uint64(*v), 10) - case uint32: - return strconv.FormatUint(uint64(v), 10) - case *uint64: - return strconv.FormatUint(uint64(*v), 10) - case uint64: - return strconv.FormatUint(uint64(v), 10) - case *float32: - return strconv.FormatFloat(float64(*v), 'f', -1, 32) // TODO: Format number in current user's locale - case float32: - return strconv.FormatFloat(float64(v), 'f', -1, 32) // TODO: Format number in current user's locale - case *float64: - return strconv.FormatFloat(float64(*v), 'f', -1, 64) - case float64: - return strconv.FormatFloat(float64(v), 'f', -1, 64) } - return "" + // Determine the reflection type so we can support any type that wraps basic types. + str, err := valueBindAnyMarshalBasicValue(reflect.ValueOf(f.Value)) + if err != nil { + return err.Error() + } + return str } func (f ValueBindAny) SetStringValue(value string) error { @@ -117,106 +67,175 @@ func (f ValueBindAny) SetStringValue(value string) error { *v = val case encoding.TextUnmarshaler: return v.UnmarshalText([]byte(value)) + } + + return valueBindAnyUnmarshalBasicValue(value, reflect.ValueOf(f.Value)) +} + +func (f ValueBindAny) HTMLInputType() string { + switch f.Value.(type) { + case *time.Time: + return "datetime-local" case *bool: - val, err := strconv.ParseBool(value) + return "text" // We will render the boolean as text, as this is an input field, not a checkbox. + case *string: + return "text" + case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64: + return "number" + case *float32, *float64: + return "number" + } + + return "text" +} + +func valueBindAnyMarshalBasicValue(v reflect.Value) (string, error) { + if !v.IsValid() { + return "", fmt.Errorf("bound value is invalid") + } + + switch v.Kind() { + case reflect.Pointer, reflect.Interface: + if v.IsNil() { + return "", fmt.Errorf("bound value pointer is nil") + } + return valueBindAnyMarshalBasicValue(v.Elem()) + case reflect.Bool: + return strconv.FormatBool(v.Bool()), nil + case reflect.String: + return v.String(), nil + case reflect.Int: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Int8: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Int16: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Int32: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Int64: + return strconv.FormatInt(v.Int(), 10), nil + case reflect.Uint: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Uint8: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Uint16: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Uint32: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Uint64: + return strconv.FormatUint(v.Uint(), 10), nil + case reflect.Float32: + return strconv.FormatFloat(v.Float(), 'f', -1, 32), nil + case reflect.Float64: + return strconv.FormatFloat(v.Float(), 'f', -1, 64), nil + } + + return "", fmt.Errorf("bound type %T is not supported", v.Interface()) +} + +func valueBindAnyUnmarshalBasicValue(str string, v reflect.Value) error { + if !v.IsValid() { + return fmt.Errorf("bound value is invalid") + } + + // Get value that is stored in pointer or interface. + kind := v.Kind() + if kind == reflect.Pointer || kind == reflect.Interface { + if v.IsNil() { + return fmt.Errorf("bound value pointer is nil") + } + return valueBindAnyUnmarshalBasicValue(str, v.Elem()) + } + + // Check if we are allowed to modify the value. + if !v.CanSet() { + return fmt.Errorf("bound value can't be modified") + } + + switch kind { + case reflect.Bool: + val, err := strconv.ParseBool(str) if err != nil { return err } - *v = val - case *string: - *v = value - case *int: - val, err := strconv.ParseInt(value, 10, 0) + v.SetBool(val) + case reflect.String: + v.SetString(str) + case reflect.Int: + val, err := strconv.ParseInt(str, 10, 0) if err != nil { return err } - *v = int(val) - case *int8: - val, err := strconv.ParseInt(value, 10, 8) + v.SetInt(val) + case reflect.Int8: + val, err := strconv.ParseInt(str, 10, 8) if err != nil { return err } - *v = int8(val) - case *int16: - val, err := strconv.ParseInt(value, 10, 16) + v.SetInt(val) + case reflect.Int16: + val, err := strconv.ParseInt(str, 10, 16) if err != nil { return err } - *v = int16(val) - case *int32: - val, err := strconv.ParseInt(value, 10, 32) + v.SetInt(val) + case reflect.Int32: + val, err := strconv.ParseInt(str, 10, 32) if err != nil { return err } - *v = int32(val) - case *int64: - val, err := strconv.ParseInt(value, 10, 64) + v.SetInt(val) + case reflect.Int64: + val, err := strconv.ParseInt(str, 10, 64) if err != nil { return err } - *v = int64(val) - case *uint: - val, err := strconv.ParseUint(value, 10, 0) + v.SetInt(val) + case reflect.Uint: + val, err := strconv.ParseUint(str, 10, 0) if err != nil { return err } - *v = uint(val) - case *uint8: - val, err := strconv.ParseUint(value, 10, 8) + v.SetUint(val) + case reflect.Uint8: + val, err := strconv.ParseUint(str, 10, 8) if err != nil { return err } - *v = uint8(val) - case *uint16: - val, err := strconv.ParseUint(value, 10, 16) + v.SetUint(val) + case reflect.Uint16: + val, err := strconv.ParseUint(str, 10, 16) if err != nil { return err } - *v = uint16(val) - case *uint32: - val, err := strconv.ParseUint(value, 10, 32) + v.SetUint(val) + case reflect.Uint32: + val, err := strconv.ParseUint(str, 10, 32) if err != nil { return err } - *v = uint32(val) - case *uint64: - val, err := strconv.ParseUint(value, 10, 64) + v.SetUint(val) + case reflect.Uint64: + val, err := strconv.ParseUint(str, 10, 64) if err != nil { return err } - *v = uint64(val) - case *float32: - val, err := strconv.ParseFloat(value, 32) + v.SetUint(val) + case reflect.Float32: + val, err := strconv.ParseFloat(str, 32) if err != nil { return err } - *v = float32(val) - case *float64: - val, err := strconv.ParseFloat(value, 64) + v.SetFloat(val) + case reflect.Float64: + val, err := strconv.ParseFloat(str, 64) if err != nil { return err } - *v = float64(val) + v.SetFloat(val) default: - return fmt.Errorf("bound type %T is not supported", f.Value) + return fmt.Errorf("bound type %T is not supported", v.Interface()) } return nil } - -func (f ValueBindAny) HTMLInputType() string { - switch f.Value.(type) { - case *time.Time: - return "datetime-local" - case *bool: - return "text" // We will render the boolean as text, as this is an input field, not a checkbox. - case *string: - return "text" - case *int, *int8, *int16, *int32, *int64, *uint, *uint8, *uint16, *uint32, *uint64: - return "number" - case *float32, *float64: - return "number" - } - - return "text" -}