wego/modules/utils/forms.go

678 lines
16 KiB
Go

// Copyright 2013 wetalk authors
//
// Licensed under the Apache License, Version 2.0 (the "License"): you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations
// under the License.
package utils
import (
"fmt"
"html/template"
"math"
"net/url"
"reflect"
"strings"
"time"
"github.com/go-tango/wego/setting"
"github.com/go-xweb/xweb/validation"
)
func init() {
initCommonField()
initExtraField()
}
type FormLocaler interface {
Tr(string, ...interface{}) string
}
type FormHelper interface {
Helps() map[string]string
}
type FormLabeler interface {
Labels() map[string]string
}
type FormPlaceholder interface {
Placeholders() map[string]string
}
type FieldCreater func(*FieldSet)
type FieldFilter func(*FieldSet)
var customCreaters = make(map[string]FieldCreater)
var customFilters = make(map[string]FieldFilter)
type fakeLocale struct{}
func (*fakeLocale) Tr(text string, args ...interface{}) string {
return text
}
var fakeLocaler FormLocaler = new(fakeLocale)
// register a custom label/input creater
func RegisterFieldCreater(name string, field FieldCreater) {
customCreaters[name] = field
}
// register a custom label/input creater
func RegisterFieldFilter(name string, field FieldFilter) {
customFilters[name] = field
}
type HtmlLazyField func() template.HTML
func (f HtmlLazyField) String() string {
return string(f())
}
type FieldSet struct {
Label template.HTML
Field HtmlLazyField
Id string
Name string
LabelText string
Value interface{}
Help string
Error string
Type string
Kind string
Placeholder string
Attrs string
FormElm reflect.Value
Locale FormLocaler
}
type FormSets struct {
FieldList []*FieldSet
Fields map[string]*FieldSet
Locale FormLocaler
inited bool
form interface{}
errs map[string]*validation.ValidationError
}
func (this *FormSets) SetError(fieldName, errMsg string) {
if fSet, ok := this.Fields[fieldName]; ok {
fSet.Error = this.Locale.Tr(errMsg)
}
}
// create formSets for generate label/field html code
func NewFormSets(form interface{}, errs map[string]*validation.ValidationError, locale FormLocaler) *FormSets {
fSets := new(FormSets)
fSets.errs = errs
fSets.Fields = make(map[string]*FieldSet)
if locale != nil {
fSets.Locale = locale
} else {
fSets.Locale = fakeLocaler
}
val := reflect.ValueOf(form)
panicAssertStructPtr(val)
elm := val.Elem()
var helps map[string]string
var labels map[string]string
var places map[string]string
// get custom field helo messages
if f, ok := form.(FormHelper); ok {
hlps := f.Helps()
if hlps != nil {
helps = hlps
}
}
// ge custom field labels
if f, ok := form.(FormLabeler); ok {
lbls := f.Labels()
if lbls != nil {
labels = lbls
}
}
// ge custom field placeholders
if f, ok := form.(FormPlaceholder); ok {
phs := f.Placeholders()
if phs != nil {
places = phs
}
}
outFor:
for i := 0; i < elm.NumField(); i++ {
f := elm.Field(i)
fT := elm.Type().Field(i)
name := fT.Name
value := f.Interface()
fTyp := "text"
switch f.Kind() {
case reflect.Bool:
fTyp = "checkbox"
default:
switch value.(type) {
case time.Time:
fTyp = "datetime"
}
}
fName := name
var attrm map[string]string
// parse struct tag settings
for _, v := range strings.Split(fT.Tag.Get("form"), ";") {
v = strings.TrimSpace(v)
if v == "-" {
continue outFor
} else if i := strings.Index(v, "("); i > 0 && strings.Index(v, ")") == len(v)-1 {
tN := v[:i]
v = strings.TrimSpace(v[i+1 : len(v)-1])
switch tN {
case "type":
fTyp = v
case "name":
fName = v
case "attr":
if attrm == nil {
attrm = make(map[string]string)
}
parts := strings.SplitN(v, ",", 2)
if len(parts) > 1 {
attrm[parts[0]] = parts[1]
} else {
attrm[v] = v
}
}
}
}
var attrs string
if attrm != nil {
for k, v := range attrm {
attrs += fmt.Sprintf(` %s="%s"`, k, v)
}
}
// set field id
fId := elm.Type().Name() + "-" + fName
var fSet FieldSet
fSet.Id = fId
fSet.Name = fName
fSet.Value = value
fSet.Attrs = attrs
fSet.FormElm = elm
fSet.Locale = locale
if i := strings.IndexRune(fTyp, ','); i != -1 {
fSet.Type = fTyp[:i]
fSet.Kind = fTyp[i+1:]
fTyp = fSet.Type
} else {
fSet.Type = fTyp
fSet.Kind = fTyp
}
// get field label text
fSet.LabelText = fName
if labels != nil {
if _, ok := labels[name]; ok {
fSet.LabelText = labels[name]
}
}
fSet.LabelText = locale.Tr(fSet.LabelText)
// get field help
if helps != nil {
if _, ok := helps[name]; ok {
fSet.Help = helps[name]
}
}
fSet.Help = locale.Tr(helps[name])
if places != nil {
if _, ok := places[name]; ok {
fSet.Placeholder = places[name]
}
}
fSet.Placeholder = locale.Tr(fSet.Placeholder)
if len(fSet.Placeholder) > 0 {
fSet.Placeholder = fmt.Sprintf(` placeholder="%s"`, fSet.Placeholder)
}
// create error string
if errs != nil {
if err, ok := errs[name]; ok {
fSet.Error = locale.Tr(err.Tmpl, err.LimitValue)
}
}
// create label html
switch fTyp {
case "checkbox", "hidden":
default:
fSet.Label = template.HTML(fmt.Sprintf(`
<label class="control-label" for="%s">%s</label>`, fSet.Id, fSet.LabelText))
}
if creater, ok := customCreaters[fTyp]; ok {
// use custome creater generate label/input html
creater(&fSet)
if filter, ok := customFilters[fTyp]; ok {
// use custome filter replace label/input html
filter(&fSet)
}
}
if fSet.Field == nil {
fSet.Field = func() template.HTML { return "" }
}
fSets.FieldList = append(fSets.FieldList, &fSet)
fSets.Fields[name] = &fSet
}
fSets.inited = true
return fSets
}
func initCommonField() {
RegisterFieldCreater("text", func(fSet *FieldSet) {
fSet.Field = func() template.HTML {
return template.HTML(fmt.Sprintf(
`<input id="%s" name="%s" type="text" value="%v" class="form-control"%s%s>`,
fSet.Id, fSet.Name, fSet.Value, fSet.Placeholder, fSet.Attrs))
}
})
RegisterFieldCreater("textarea", func(fSet *FieldSet) {
fSet.Field = func() template.HTML {
return template.HTML(fmt.Sprintf(
`<textarea id="%s" name="%s" rows="5" class="form-control"%s%s>%v</textarea>`,
fSet.Id, fSet.Name, fSet.Placeholder, fSet.Attrs, fSet.Value))
}
})
RegisterFieldCreater("password", func(fSet *FieldSet) {
fSet.Field = func() template.HTML {
return template.HTML(fmt.Sprintf(
`<input id="%s" name="%s" type="password" value="%v" class="form-control"%s%s>`,
fSet.Id, fSet.Name, fSet.Value, fSet.Placeholder, fSet.Attrs))
}
})
RegisterFieldCreater("hidden", func(fSet *FieldSet) {
fSet.Field = func() template.HTML {
return template.HTML(fmt.Sprintf(
`<input id="%s" name="%s" type="hidden" value="%v"%s>`, fSet.Id, fSet.Name, fSet.Value, fSet.Attrs))
}
})
datetimeFunc := func(fSet *FieldSet) {
fSet.Field = func() template.HTML {
t := fSet.Value.(time.Time)
tval := Date(t, setting.DateTimeFormat)
if tval == "0001-01-01 00:00:00" {
t = time.Now()
}
if fSet.Type == "date" {
tval = Date(t, setting.DateFormat)
}
return template.HTML(fmt.Sprintf(
`<input id="%s" name="%s" type="%s" value="%s" class="form-control"%s%s>`,
fSet.Id, fSet.Name, fSet.Type, tval, fSet.Placeholder, fSet.Attrs))
}
}
RegisterFieldCreater("date", datetimeFunc)
RegisterFieldCreater("datetime", datetimeFunc)
RegisterFieldCreater("checkbox", func(fSet *FieldSet) {
fSet.Field = func() template.HTML {
var checked string
if b, ok := fSet.Value.(bool); ok && b {
checked = "checked"
}
return template.HTML(fmt.Sprintf(
`<label for="%s" class="checkbox">%s<input id="%s" name="%s" type="checkbox" %s></label>`,
fSet.Id, fSet.LabelText, fSet.Id, fSet.Name, checked))
}
})
RegisterFieldCreater("select", func(fSet *FieldSet) {
fSet.Field = func() template.HTML {
var options string
str := fmt.Sprintf(`<select id="%s" name="%s" class="form-control"%s%s>%s</select>`,
fSet.Id, fSet.Name, fSet.Placeholder, fSet.Attrs)
fun := fSet.FormElm.Addr().MethodByName(fSet.Name + "SelectData")
if fun.IsValid() {
results := fun.Call([]reflect.Value{})
if len(results) > 0 {
v := results[0]
if v.CanInterface() {
if vu, ok := v.Interface().([][]string); ok {
var vs []string
val := reflect.ValueOf(fSet.Value)
if val.Kind() == reflect.Slice {
vs = make([]string, 0, val.Len())
for i := 0; i < val.Len(); i++ {
vs = append(vs, ToStr(val.Index(i).Interface()))
}
}
isMulti := len(vs) > 0
for _, parts := range vu {
var n, v string
switch {
case len(parts) > 1:
n, v = fSet.Locale.Tr(parts[0]), parts[1]
case len(parts) == 1:
n, v = fSet.Locale.Tr(parts[0]), parts[0]
}
var selected string
if isMulti {
for _, e := range vs {
if e == v {
selected = ` selected="selected"`
break
}
}
} else if ToStr(fSet.Value) == v {
selected = ` selected="selected"`
}
options += fmt.Sprintf(`<option value="%s"%s>%s</option>`, v, selected, n)
}
}
}
}
}
if len(options) == 0 {
options = fmt.Sprintf(`<option value="%v">%v</option>`, fSet.Value, fSet.Value)
}
return template.HTML(fmt.Sprintf(str, options))
}
})
}
func initExtraField() {
// a example to create a bootstrap style checkbox creater
RegisterFieldCreater("checkbox", func(fSet *FieldSet) {
fSet.Field = func() template.HTML {
value := false
if b, ok := fSet.Value.(bool); ok {
value = b
}
active := ""
if value {
active = " active"
}
return template.HTML(fmt.Sprintf(`<label>
<input type="hidden" name="%s" value="%v">
<button type="button" data-toggle="button" data-name="%s" class="btn btn-default btn-xs btn-checked%s">
<span class="glyphicon glyphicon-ok"></span>
</button>%s
</label>`, fSet.Name, value, fSet.Name, active, fSet.LabelText))
}
})
// a example to create a select2 box
RegisterFieldFilter("select", func(fSet *FieldSet) {
if strings.Index(fSet.Attrs, `rel="select2"`) != -1 {
field := fSet.Field.String()
fSet.Field = func() template.HTML {
field = strings.Replace(field, "<option", "<option></option><option", 1)
return template.HTML(field)
}
}
})
// a example to create a captcha field
RegisterFieldCreater("captcha", func(fSet *FieldSet) {
fSet.Label = template.HTML(fmt.Sprintf(`
<label class="control-label" for="%s">%s</label>`, fSet.Id, fSet.LabelText))
fSet.Field = func() template.HTML {
return template.HTML(fmt.Sprintf(`%v
<input id="%s" name="%s" type="text" value="" class="form-control" autocomplete="off"%s%s>`,
setting.Captcha.CreateHtml(), fSet.Id, fSet.Name, fSet.Placeholder, fSet.Attrs))
}
})
RegisterFieldCreater("empty", func(fSet *FieldSet) {
fSet.Label = ""
fSet.Field = func() template.HTML { return "" }
})
}
// parse request.Form values to form
func ParseForm(form interface{}, values url.Values) {
val := reflect.ValueOf(form)
elm := reflect.Indirect(val)
panicAssertStructPtr(val)
outFor:
for i := 0; i < elm.NumField(); i++ {
f := elm.Field(i)
fT := elm.Type().Field(i)
fName := fT.Name
for _, v := range strings.Split(fT.Tag.Get("form"), ";") {
v = strings.TrimSpace(v)
if v == "-" {
continue outFor
} else if i := strings.Index(v, "("); i > 0 && strings.Index(v, ")") == len(v)-1 {
tN := v[:i]
v = strings.TrimSpace(v[i+1 : len(v)-1])
switch tN {
case "name":
fName = v
}
}
}
value := ""
var vs []string
if v, ok := values[fName]; ok {
vs = v
if len(v) > 0 {
value = v[0]
}
}
switch fT.Type.Kind() {
case reflect.Bool:
b, _ := StrTo(value).Bool()
f.SetBool(b)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
x, _ := StrTo(value).Int64()
f.SetInt(x)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
x, _ := StrTo(value).Uint64()
f.SetUint(x)
case reflect.Float32, reflect.Float64:
x, _ := StrTo(value).Float64()
f.SetFloat(x)
case reflect.Struct:
if fT.Type.String() == "time.Time" {
if len(value) > 10 {
t, err := DateParse(value, setting.DateTimeFormat)
if err != nil {
continue
}
f.Set(reflect.ValueOf(t))
} else {
t, err := DateParse(value, setting.DateFormat)
if err != nil {
continue
}
f.Set(reflect.ValueOf(t))
}
}
case reflect.String:
f.SetString(value)
case reflect.Slice:
f.Set(reflect.ValueOf(vs))
}
}
}
// assert an object must be a struct pointer
func panicAssertStructPtr(val reflect.Value) {
if val.Kind() == reflect.Ptr && val.Elem().Kind() == reflect.Struct {
return
}
panic(fmt.Errorf("%s must be a struct pointer", val.Type().Name()))
}
// set values from one struct to other struct
// both need ptr struct
func SetFormValues(from interface{}, to interface{}, skips ...string) {
val := reflect.ValueOf(from)
elm := reflect.Indirect(val)
valTo := reflect.ValueOf(to)
elmTo := reflect.Indirect(valTo)
panicAssertStructPtr(val)
panicAssertStructPtr(valTo)
outFor:
for i := 0; i < elmTo.NumField(); i++ {
toF := elmTo.Field(i)
name := elmTo.Type().Field(i).Name
// skip specify field
for _, skip := range skips {
if skip == name {
continue outFor
}
}
f := elm.FieldByName(name)
if f.Kind() != reflect.Invalid {
// set value if type matched
if f.Type().String() == toF.Type().String() {
toF.Set(f)
} else {
fInt := false
switch f.Interface().(type) {
case int, int8, int16, int32, int64:
fInt = true
case uint, uint8, uint16, uint32, uint64:
default:
continue outFor
}
switch toF.Interface().(type) {
case int, int8, int16, int32, int64:
var v int64
if fInt {
v = f.Int()
} else {
vu := f.Uint()
if vu > math.MaxInt64 {
continue outFor
}
v = int64(vu)
}
if toF.OverflowInt(v) {
continue outFor
}
toF.SetInt(v)
case uint, uint8, uint16, uint32, uint64:
var v uint64
if fInt {
vu := f.Int()
if vu < 0 {
continue outFor
}
v = uint64(vu)
} else {
v = f.Uint()
}
if toF.OverflowUint(v) {
continue outFor
}
toF.SetUint(v)
}
}
}
}
}
// compare field values between two struct pointer
// return changed field names
func FormChanges(base interface{}, modified interface{}, skips ...string) (fields []string) {
val := reflect.ValueOf(base)
elm := reflect.Indirect(val)
valMod := reflect.ValueOf(modified)
elmMod := reflect.Indirect(valMod)
panicAssertStructPtr(val)
panicAssertStructPtr(valMod)
outFor:
for i := 0; i < elmMod.NumField(); i++ {
modF := elmMod.Field(i)
name := elmMod.Type().Field(i).Name
fT := elmMod.Type().Field(i)
for _, v := range strings.Split(fT.Tag.Get("form"), ";") {
v = strings.TrimSpace(v)
if v == "-" {
continue outFor
}
}
// skip specify field
for _, skip := range skips {
if skip == name {
continue outFor
}
}
f := elm.FieldByName(name)
if f.Kind() == reflect.Invalid {
continue
}
// compare two values use string
if ToStr(modF.Interface()) != ToStr(f.Interface()) {
fields = append(fields, name)
}
}
return
}