├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README_zh-CN.md ├── common ├── filesystem.go ├── html.go ├── tplfunc.go └── types.go ├── config ├── config.go ├── element.go ├── element_choice.go ├── element_interface.go ├── element_list_group.go ├── element_list_group_test.go ├── language.go └── utils.go ├── defaults ├── templates.go └── templates │ ├── allfields.html │ ├── base │ ├── button.html │ ├── datetime │ │ ├── date.html │ │ ├── datetime.html │ │ └── time.html │ ├── fieldset.html │ ├── fieldset_buttons.html │ ├── generic.html │ ├── input.html │ ├── langset.html │ ├── number │ │ ├── number.html │ │ └── range.html │ ├── options │ │ ├── checkbox.html │ │ ├── radiobutton.html │ │ └── select.html │ ├── static.html │ └── text │ │ ├── passwordinput.html │ │ ├── textareainput.html │ │ └── textinput.html │ ├── baseform.html │ ├── bootstrap3 │ ├── button.html │ ├── datetime │ │ ├── date.html │ │ ├── datetime.html │ │ └── time.html │ ├── fieldset.html │ ├── fieldset_buttons.html │ ├── generic.html │ ├── input.html │ ├── langset.html │ ├── number │ │ ├── number.html │ │ └── range.html │ ├── options │ │ ├── checkbox.html │ │ ├── radiobutton.html │ │ └── select.html │ ├── static.html │ └── text │ │ ├── passwordinput.html │ │ ├── textareainput.html │ │ └── textinput.html │ └── bootstrapform.html ├── example ├── forms.json ├── main.go └── run.bat ├── fields ├── button.go ├── datetime.go ├── field.go ├── field_interface.go ├── field_test.go ├── number.go ├── options.go ├── static.go └── text.go ├── fieldset.go ├── forms.go ├── forms_marshal.go ├── forms_test.go ├── go.mod ├── go.sum ├── json.go ├── langset.go ├── utils.go ├── validate.go └── widgets └── widgets.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.log 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | script: "go build -v" 3 | go: 4 | - 1.16 5 | - tip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 coscms 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | forms 2 | ========== 3 | 4 | [![GoDoc](https://godoc.org/github.com/coscms/forms?status.png)](http://godoc.org/github.com/coscms/forms) 5 | 6 | Description 7 | =========== 8 | 9 | `forms` makes form creation and handling easy. It allows the creation of form without having to write HTML code or bother to make the code Bootstrap compatible. 10 | You can just create your form instance and add / populate / customize fields based on your needs. Or you can let `forms` do that for you starting from any object instance. 11 | 12 | To integrate `forms` forms into your application simply pass the form object to the template and call its Render method. 13 | In your code: 14 | 15 | ```go 16 | tmpl.Execute(buf, map[string]interface{}{"form": form}) 17 | ``` 18 | 19 | In your template: 20 | 21 | ```html 22 | {{ if .form }}{{ .form.Render }}{{ end }} 23 | ``` 24 | 25 | Installation 26 | ============ 27 | 28 | To install this package simply: 29 | 30 | ```go 31 | go get github.com/coscms/forms 32 | ``` 33 | 34 | Forms 35 | ===== 36 | 37 | There are two predefined themes for forms: base HTML forms and Bootstrap forms: they have different structures and predefined classes. 38 | Style aside, forms can be created from scratch or starting from a base instance. 39 | 40 | From scratch 41 | ------------ 42 | 43 | You can create a form instance by simply deciding its theme and providing its method and action: 44 | 45 | ```go 46 | form := NewWithConfig(&config.Config{ 47 | Theme:"base", 48 | Method:POST, 49 | Action:"/action.html", 50 | }) 51 | ``` 52 | 53 | Now that you have a form instance you can customize it by adding classes, parameters, CSS values or id. Each method returns a pointer to the same form, so multiple calls can be chained: 54 | 55 | ```go 56 | form.SetID("TestForm").AddClass("form").AddCSS("border", "auto") 57 | ``` 58 | 59 | Obviously, elements can be added as well: 60 | 61 | ```go 62 | form.Elements(fields.TextField("text_field")) 63 | ``` 64 | 65 | Elements can be either FieldSets or Fields: the formers are simply collections of fields translated into a `
` element. 66 | Elements are added in order, and they are displayed in the exact same order. Note that single elements can be removed from a form referencing them by name: 67 | 68 | ```go 69 | form.RemoveElement("text_field") 70 | ``` 71 | 72 | Typical usage looks like this: 73 | 74 | ```go 75 | form := NewWithConfig(&config.Config{ 76 | Theme:"base", 77 | Method:POST, 78 | Action:"/action.html", 79 | }).Elements( 80 | fields.TextField("text_field").SetLabel("Username"), 81 | FieldSet("psw_fieldset", 82 | fields.PasswordField("psw1").AddClass("password_class").SetLabel("Password 1"), 83 | fields.PasswordField("psw2").AddClass("password_class").SetLabel("Password 2"), 84 | ), 85 | fields.SubmitButton("btn1", "Submit"), 86 | ) 87 | ``` 88 | 89 | validation: 90 | 91 | ```go 92 | type User struct { 93 | Username string `valid:"Required;AlphaDash;MaxSize(30)"` 94 | Password1 string `valid:"Required"` 95 | Password2 string `valid:"Required"` 96 | } 97 | u := &User{} 98 | form.SetModel(u) //Must set model 99 | err := form.valid() 100 | if err != nil { 101 | // validation does not pass 102 | } 103 | ``` 104 | 105 | Details about validation, please visit: https://github.com/webx-top/validation 106 | 107 | A call to `form.Render()` returns the following form: 108 | 109 | ```html 110 |
111 | 112 | 113 |
114 | 115 | 116 | 117 | 118 |
119 | 120 |
121 | ``` 122 | 123 | From model instance 124 | ------------------- 125 | 126 | Instead of manually creating a form, it can be automatically created from an existing model instance: the package will try to infer the field types based on the instance fields and fill them accordingly. 127 | Default type-to-field mapping is as follows: 128 | 129 | * string: TextField 130 | * bool: Checkbox 131 | * time.Time: DatetimeField 132 | * int: NumberField 133 | * struct: recursively parse 134 | 135 | You can customize field behaviors by adding tags to instance fields. 136 | Without tags this code: 137 | 138 | ```go 139 | type User struct { 140 | Username string 141 | Password1 string 142 | Password2 string 143 | } 144 | u := &User{} 145 | form := NewFromModel(u, &config.Config{ 146 | Theme:"bootstrap3", 147 | Method:POST, 148 | Action:"/action.html", 149 | }) 150 | ``` 151 | 152 | validation: 153 | 154 | ```go 155 | err := form.valid() 156 | if err != nil { 157 | // validation does not pass 158 | } 159 | form.Render() 160 | ``` 161 | 162 | would yield this HTML form: 163 | 164 | ```html 165 |
166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 |
174 | ``` 175 | 176 | A submit button is added by default. 177 | 178 | Notice that the form is still editable and fields can be added, modified or removed like before. 179 | 180 | When creating a form from a model instance, field names are created by appending the field name to the baseline; the baseline is empty for single level structs but is crafted when nested structs are found: in this case it becomes the field name followed by a dot. 181 | So for example, if the struct is: 182 | 183 | ```go 184 | type A struct { 185 | field1 int 186 | field2 int 187 | } 188 | type B struct { 189 | field0 int 190 | struct1 A 191 | } 192 | ``` 193 | 194 | The final form will contain fields "field0", "struct1.field1" and "struct1.field2". 195 | 196 | Tags 197 | ---- 198 | 199 | Struct tags can be used to slightly modify automatic form creation. In particular the following tags are parsed: 200 | 201 | * form_options: can contain the following keywords separated by Semicolon (;) 202 | 203 | - -: skip field, do not convert to HTML field 204 | - checked: for Checkbox fields, check by default 205 | - multiple: for select fields, allows multiple choices 206 | 207 | * form_widget: override custom widget with one of the following 208 | 209 | - text 210 | - hidden 211 | - textarea 212 | - password 213 | - select 214 | - datetime 215 | - date 216 | - time 217 | - number 218 | - range 219 | - radio 220 | - checkbox 221 | - static (simple text) 222 | 223 | * form_fieldset: define fieldset name 224 | * form_sort: sort number (asc, 0 ~ total-1) 225 | * form_choices: defines options for select and radio input fields 226 | - radio/checkbox example(format: id|value): 1|Option One|2|Option 2|3|Option 3 227 | - select example(format: group|id|value): G1|A|Option A|G1|B|Option B 228 | - "" group is the default one and does not trigger a `` rendering. 229 | * form_max: max value (number, range, datetime, date and time fields) 230 | * form_min: min value (number, range, datetime, date and time fields) 231 | * form_step: step value (range field) 232 | * form_rows: number of rows (textarea field) 233 | * form_cols: number of columns (textarea field) 234 | * form_value: input field value (used if field is empty) 235 | * form_label: label for input field 236 | 237 | The code would therefore be better like this: 238 | 239 | ```go 240 | type User struct { 241 | Username string 242 | Password1 string `form_widget:"password" form_label:"Password 1"` 243 | Password2 string `form_widget:"password" form_label:"Password 2"` 244 | SkipThis int `form_options:"-"` 245 | } 246 | u := User{} 247 | form := NewFromModel(u, &config.Config{ 248 | Theme:"bootstrap3", 249 | Method:POST, 250 | Action:"/action.html", 251 | }) 252 | form.Render() 253 | ``` 254 | 255 | which translates into: 256 | 257 | ```html 258 |
259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 |
267 | ``` 268 | 269 | Fields 270 | ====== 271 | 272 | Field objects in `forms` implement the `fields.FieldInterface` which exposes methods to edit classes, parameters, tags and CSS styles. 273 | See the [documentation](http://godoc.org/github.com/coscms/forms) for details. 274 | 275 | Most of the field widgets have already been created and integrate with Bootstrap. It is possible, however, to define custom widgets to render fields by simply assigning an object implementing the widgets.WidgetInterface to the Widget field. 276 | 277 | Also, error messages can be added to fields via the `AddError(err)` method: in a Bootstrap environment they will be correctly rendered. 278 | 279 | Text fields 280 | ----------- 281 | 282 | This category includes text, password, textarea and hidden fields. They are all instantiated by providing the name, except the TextAreaField which also requires a dimension in terms of rows and columns. 283 | 284 | ```go 285 | f0 := fields.TextField("text") 286 | f1 := fields.PasswordField("password") 287 | f2 := fields.HiddenField("hidden") 288 | f3 := fields.TextAreaField("textarea", 30, 50) 289 | ``` 290 | 291 | Option fields 292 | ------------- 293 | 294 | This category includes checkbox, select and radio button fields. 295 | Checkbox field requires a name and a set of options to populate the field. The options are just a set of InputChoice (ID-Value pairs) objects: 296 | 297 | ```go 298 | opts := []fields.InputChoice{ 299 | fields.InputChoice{ID:"A", Val:"Option A"}, 300 | fields.InputChoice{ID:"B", Val:"Option B"}, 301 | } 302 | f := fields.CheckboxField("checkbox", opts) 303 | f.AddSelected("A", "B") 304 | ``` 305 | 306 | Radio buttons, instead, require a name and a set of options to populate the field. The options are just a set of InputChoice (ID-Value pairs) objects: 307 | 308 | ```go 309 | opts := []fields.InputChoice{ 310 | fields.InputChoice{ID:"A", Val:"Option A"}, 311 | fields.InputChoice{ID:"B", Val:"Option B"}, 312 | } 313 | f := fields.RadioField("radio", opts) 314 | ``` 315 | 316 | Select fields, on the other hand, allow option grouping. This can be achieved by passing a `map[string][]InputChoice` in which keys are groups containing choices given as values; the default (empty) group is "", which is not translated into any `` element. 317 | 318 | ```go 319 | opts := map[string][]fields.InputChoice{ 320 | "": []fields.InputChoice{fields.InputChoice{"A", "Option A"}}, 321 | "group1": []fields.InputChoice{ 322 | fields.InputChoice{ID:"B", Val:"Option B"}, 323 | fields.InputChoice{ID:"C", Val:"Option C"}, 324 | } 325 | } 326 | f := fields.SelectField("select", opts) 327 | ``` 328 | 329 | Select fields can allow multiple choices. To enable this option simply call the `MultipleChoice()` method on the field and provide the selected choices via `AddSelected(...string)`: 330 | 331 | ```go 332 | f.MultipleChoice() 333 | f.AddSelected("A", "B") 334 | ``` 335 | 336 | Number fields 337 | ------------- 338 | 339 | Number and range fields are included. 340 | Number field only require a name to be instantiated; minimum and maximum values can optionally be set by adding `min` and `max` parameters respectively. 341 | 342 | ```go 343 | f := fields.NumberField("number") 344 | f.SetParam("min", "1") 345 | ``` 346 | 347 | Range fields, on the other hand, require both minimum and maximum values (plus the identifier). The optional "step" value is set via `SetParam`. 348 | 349 | ```go 350 | f := fields.RangeField("range", 1, 10) 351 | f.SetParam("step", "2") 352 | ``` 353 | 354 | Datetime fields 355 | --------------- 356 | 357 | Datetime, date and time input fields are defined in `forms`. 358 | 359 | ```go 360 | f0 := fields.DatetimeField("datetime") 361 | f1 := fields.DateField("date") 362 | f2 := fields.TimeField("time") 363 | ``` 364 | 365 | Values can be set via `SetValue` method; there's no input validation but format strings are provided to ensure the correct time-to-string conversion. 366 | 367 | ```go 368 | t := time.Now() 369 | f0.SetValue(t.Format(fields.DATETIME_FORMAT)) 370 | f1.SetValue(t.Format(fields.DATE_FORMAT)) 371 | f2.SetValue(t.Format(fields.TIME_FORMAT)) 372 | ``` 373 | 374 | Buttons 375 | ------- 376 | 377 | Buttons can be created calling either the `Button`, `SubmitButton` or `ResetButton` constructor methods and providing a text identifier and the content of the button itself. 378 | 379 | ```go 380 | btn0 := fields.Button("btn", "Click me!") 381 | ``` 382 | 383 | License 384 | ======= 385 | 386 | `forms` is released under the MIT license. See [LICENSE](https://github.com/coscms/forms/blob/master/LICENSE). -------------------------------------------------------------------------------- /README_zh-CN.md: -------------------------------------------------------------------------------- 1 | forms 2 | ========== 3 | 4 | [![GoDoc](https://godoc.org/github.com/coscms/forms?status.png)](http://godoc.org/github.com/coscms/forms) 5 | 6 | 7 | 简介 8 | =========== 9 | 10 | 用`forms`可以方便的创建HTML表单,支持指定表单模板,在使用时您不用写HTML代码就可以便捷的创建出符合个人需求的表单。你可以根据自身需要创建一个表单实例然后添加、填充、自定义表单字段。 11 | 12 | `forms`可以很容易的集成到你的应用中,只需要通过调用form对象的Render方法即可渲染出表单的HTML代码: 13 | 14 | tmpl.Execute(buf, map[string]interface{}{"form": form}) 15 | 16 | 在模板中使用: 17 | 18 | {{ if .form }}{{ .form.Render }}{{ end }} 19 | 20 | 21 | 安装方式 22 | ============ 23 | 24 | 使用以下代码安装forms包: 25 | 26 | go get github.com/coscms/forms 27 | 28 | 表单模板 29 | ===== 30 | 31 | 本包中已经内置了两套表单模板:base和bootstrap3。它们代表两种不同的风格。除此之外,您也可以创建自己的表单模板。 32 | 33 | 入门指引 34 | ------------ 35 | 36 | 创建一个form实例,并指定表单模板为base,表单的提交方式为POST,表单的提交网址为“/action.html”: 37 | 38 | form := NewForm("base", POST, "/action.html") 39 | 40 | 41 | 现在,我们可以通过form实例来自定义表单的属性。form实例中的每一个方法都会返回form指针,因此你可以非常方便的采用链式调用来多次执行方法。下面,我们来定义HTML标签`
`中的id、class和style属性: 42 | 43 | form.SetID("TestForm").AddClass("form").AddCSS("border", "auto") 44 | 45 | 添加其它表单字段也很容易,比如,我们要添加一个name属性值为“text_field”的文本输入框: 46 | 47 | form.Elements(fields.TextField("text_field")) 48 | 49 | Elements方法可以添加`
`包围起来的表单字段或单独的表单字段,然后根据在Elements方法中的添加顺序来依次显示表单元素。 我们也可以通过name属性值来删除某个表单元素: 50 | 51 | form.RemoveElement("text_field") 52 | 53 | 典型用法: 54 | 55 | form := NewForm("base", POST, "/action.html").Elements( 56 | fields.TextField("text_field").SetLabel("Username"), 57 | FieldSet("psw_fieldset", 58 | fields.PasswordField("psw1").AddClass("password_class").SetLabel("Password 1"), 59 | fields.PasswordField("psw2").AddClass("password_class").SetLabel("Password 2"), 60 | ), 61 | fields.SubmitButton("btn1", "Submit"), 62 | ) 63 | 64 | 表单验证: 65 | 66 | 67 | type User struct { 68 | Username string `valid:"Required;AlphaDash;MaxSize(30)"` 69 | Password1 string `valid:"Required"` 70 | Password2 string `valid:"Required"` 71 | } 72 | 73 | u := &User{} 74 | form.SetModel(u) //必须设置数据模型 75 | valid, passed := form.valid() 76 | if !passed { 77 | //验证未通过时的操作代码 78 | } 79 | _ = valid 80 | 81 | 82 | 表单验证的详细用法请访问: [https://github.com/webx-top/validation](https://github.com/webx-top/validation) 83 | 84 | 调用 `form.Render()` 返回如下表单: 85 | 86 | 87 | 88 | 89 |
90 | 91 | 92 | 93 | 94 |
95 | 96 |
97 | 98 | 从model实例创建表单 99 | ------------------- 100 | 101 | 我们可以通过model实例来自动创建表单,免除了手动一个个添加表单字段的麻烦。本forms包会根据model实例中的属性字段及其类型来依次填充到表单中作为表单字段。 102 | model实例内属性字段类型与表单字段对应关系如下: 103 | 104 | * string: TextField (文本输入框) 105 | * bool: Checkbox (复选框) 106 | * time.Time: DatetimeField (日期输入框) 107 | * int: NumberField (数字输入框) 108 | * struct: 递归解析 109 | 110 | 也可以通过添加tag到model实例中的属性字段内来定义表单字段的类型。 111 | 不带tag的代码: 112 | 113 | type User struct { 114 | Username string 115 | Password1 string 116 | Password2 string 117 | } 118 | 119 | u := &User{} 120 | 121 | form := NewFormFromModel(u, "bootstrap3", POST, "/action.html") 122 | 123 | 验证表单数据: 124 | 125 | valid, passed := form.valid() 126 | if !passed { 127 | // validation does not pass 128 | } 129 | _ = valid 130 | 131 | form.Render() 132 | 133 | 生成的HTML代码如下: 134 | 135 |
136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 |
144 | 145 | 默认会添加一个提交按钮。 146 | 147 | 注意:我们可以象前面介绍的那样添加、修改或删除某一个表单字段。 148 | 149 | When creating a form from a model instance, field names are created by appending the field name to the baseline; the baseline is empty for single level structs but is crafted when nested structs are found: in this case it becomes the field name followed by a dot. 150 | So for example, if the struct is: 151 | 152 | type A struct { 153 | field1 int 154 | field2 int 155 | } 156 | 157 | type B struct { 158 | field0 int 159 | struct1 A 160 | } 161 | 162 | The final form will contain fields "field0", "struct1.field1" and "struct1.field2". 163 | 164 | Tags 165 | ---- 166 | 167 | Struct tags can be used to slightly modify automatic form creation. 下面列出的这些tag会被解析: 168 | 169 | * form_options: 可以包含如下关键词,同时使用多个关键词时,用分号(;)隔开 170 | - -: 跳过此字段, 不转为HTML表单字段 171 | - checked: 针对Checkbox,默认选中 172 | - multiple: 指定select为允许多选 173 | * form_widget: 指定表单部件类型。支持以下类型: 174 | - text 175 | - hidden 176 | - textarea 177 | - password 178 | - select 179 | - datetime 180 | - date 181 | - time 182 | - number 183 | - range 184 | - radio 185 | - checkbox 186 | - static (简单的静态文本) 187 | * form_fieldset: 定义fieldset标题文字 188 | * form_sort: 排序编号 (按升序排列, 编号从0开始,范围为0 ~ 总数-1) 189 | * form_choices: select或radio输入字段的选项 190 | - radio/checkbox 范例(格式: id|value): 1|选项一|2|选项二|3|选项三 191 | - select 范例(格式: group|id|value): 组1|A|选项A|组1|B|选项B 192 | - "" 组名为空白时,默认将不渲染``。 193 | * form_max: 允许的最大值 (用于number、range、datetime、date 和 time 类型输入框) 194 | * form_min: 允许的最小值 (用于number、range、datetime、date 和 time 类型输入框) 195 | * form_step: 步进值 (用于range输入字段) 196 | * form_rows: 行数 (用于textarea) 197 | * form_cols: 列数 (用于textarea) 198 | * form_value: 输入字段的默认值 199 | * form_label: label内容 200 | 201 | 例如: 202 | 203 | type User struct { 204 | Username string 205 | Password1 string `form_widget:"password" form_label:"Password 1"` 206 | Password2 string `form_widget:"password" form_label:"Password 2"` 207 | SkipThis int `form_options:"-"` 208 | } 209 | 210 | u := User{} 211 | 212 | form := NewFormFromModel(u, "bootstrap3", POST, "/action.html") 213 | form.Render() 214 | 215 | 它们最后会翻译成以下代码: 216 | 217 |
218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 |
226 | 227 | Fields 228 | ====== 229 | 230 | Field objects in `forms` implement the `fields.FieldInterface` which exposes methods to edit classes, parameters, tags and CSS styles. 231 | See the [documentation](http://godoc.org/github.com/coscms/forms) for details. 232 | 233 | Most of the field widgets have already been created and integrate with Bootstrap. It is possible, however, to define custom widgets to render fields by simply assigning an object implementing the widgets.WidgetInterface to the Widget field. 234 | 235 | Also, error messages can be added to fields via the `AddError(err)` method: in a Bootstrap environment they will be correctly rendered. 236 | 237 | Text fields 238 | ----------- 239 | 240 | This category includes text, password, textarea and hidden fields. They are all instantiated by providing the name, except the TextAreaField which also requires a dimension in terms of rows and columns. 241 | 242 | f0 := fields.TextField("text") 243 | f1 := fields.PasswordField("password") 244 | f2 := fields.HiddenField("hidden") 245 | f3 := fields.TextAreaField("textarea", 30, 50) 246 | 247 | Option fields 248 | ------------- 249 | 250 | This category includes checkbox, select and radio button fields. 251 | Checkbox field requires a name and a set of options to populate the field. The options are just a set of InputChoice (ID-Value pairs) objects: 252 | 253 | opts := []fields.InputChoice{ 254 | fields.InputChoice{ID:"A", Val:"Option A"}, 255 | fields.InputChoice{ID:"B", Val:"Option B"}, 256 | } 257 | f := fields.CheckboxField("checkbox", opts) 258 | f.AddSelected("A", "B") 259 | 260 | Radio buttons, instead, require a name and a set of options to populate the field. The options are just a set of InputChoice (ID-Value pairs) objects: 261 | 262 | opts := []fields.InputChoice{ 263 | fields.InputChoice{ID:"A", Val:"Option A"}, 264 | fields.InputChoice{ID:"B", Val:"Option B"}, 265 | } 266 | f := fields.RadioField("radio", opts) 267 | 268 | Select fields, on the other hand, allow option grouping. This can be achieved by passing a `map[string][]InputChoice` in which keys are groups containing choices given as values; the default (empty) group is "", which is not translated into any `` element. 269 | 270 | opts := map[string][]fields.InputChoice{ 271 | "": []fields.InputChoice{fields.InputChoice{"A", "Option A"}}, 272 | "group1": []fields.InputChoice{ 273 | fields.InputChoice{ID:"B", Val:"Option B"}, 274 | fields.InputChoice{ID:"C", Val:"Option C"}, 275 | } 276 | } 277 | f := fields.SelectField("select", opts) 278 | 279 | Select fields can allow multiple choices. To enable this option simply call the `MultipleChoice()` method on the field and provide the selected choices via `AddSelected(...string)`: 280 | 281 | f.MultipleChoice() 282 | f.AddSelected("A", "B") 283 | 284 | Number fields 285 | ------------- 286 | 287 | Number and range fields are included. 288 | Number field only require a name to be instantiated; minimum and maximum values can optionally be set by adding `min` and `max` parameters respectively. 289 | 290 | f := fields.NumberField("number") 291 | f.SetParam("min", "1") 292 | 293 | Range fields, on the other hand, require both minimum and maximum values (plus the identifier). The optional "step" value is set via `SetParam`. 294 | 295 | f := fields.RangeField("range", 1, 10) 296 | f.SetParam("step", "2") 297 | 298 | 299 | Datetime fields 300 | --------------- 301 | 302 | Datetime, date and time input fields are defined in `go-form-it`. 303 | 304 | f0 := fields.DatetimeField("datetime") 305 | f1 := fields.DateField("date") 306 | f2 := fields.TimeField("time") 307 | 308 | Values can be set via `SetValue` method; there's no input validation but format strings are provided to ensure the correct time-to-string conversion. 309 | 310 | t := time.Now() 311 | f0.SetValue(t.Format(fields.DATETIME_FORMAT)) 312 | f1.SetValue(t.Format(fields.DATE_FORMAT)) 313 | f2.SetValue(t.Format(fields.TIME_FORMAT)) 314 | 315 | Buttons 316 | ------- 317 | 318 | Buttons can be created calling either the `Button`, `SubmitButton` or `ResetButton` constructor methods and providing a text identifier and the content of the button itself. 319 | 320 | btn0 := fields.Button("btn", "Click me!") 321 | 322 | 323 | License 324 | ======= 325 | 326 | `forms` is released under the MIT license. See [LICENSE](https://github.com/coscms/forms/blob/master/LICENSE). -------------------------------------------------------------------------------- /common/filesystem.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "io/fs" 6 | ) 7 | 8 | var FileSystem FileSystems 9 | 10 | type ( 11 | FileSystems []fs.FS 12 | ) 13 | 14 | func (f FileSystems) Open(name string) (file fs.File, err error) { 15 | for _, i := range f { 16 | file, err = i.Open(name) 17 | if err == nil || !errors.Is(err, fs.ErrNotExist) { 18 | return 19 | } 20 | } 21 | return 22 | } 23 | 24 | func (f FileSystems) Size() int { 25 | return len(f) 26 | } 27 | 28 | func (f FileSystems) IsEmpty() bool { 29 | return f.Size() == 0 30 | } 31 | 32 | func (f *FileSystems) Register(fileSystem fs.FS) { 33 | *f = append(*f, fileSystem) 34 | } 35 | -------------------------------------------------------------------------------- /common/html.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "html/template" 5 | "strings" 6 | 7 | "github.com/webx-top/com" 8 | ) 9 | 10 | type ( 11 | HTMLAttrValues []string 12 | HTMLAttributes map[template.HTMLAttr]interface{} 13 | HTMLData map[string]interface{} 14 | ) 15 | 16 | func (s HTMLAttrValues) String() string { 17 | return strings.Join([]string(s), ` `) 18 | } 19 | 20 | func (s HTMLAttrValues) IsEmpty() bool { 21 | return len(s) == 0 22 | } 23 | 24 | func (s HTMLAttrValues) Size() int { 25 | return len(s) 26 | } 27 | 28 | func (s HTMLAttrValues) Exists(attr string) bool { 29 | return com.InSlice(attr, s) 30 | } 31 | 32 | func (s *HTMLAttrValues) Add(value string) { 33 | (*s) = append((*s), value) 34 | } 35 | 36 | func (s *HTMLAttrValues) Remove(value string) { 37 | ind := -1 38 | for i, v := range *s { 39 | if v == value { 40 | ind = i 41 | break 42 | } 43 | } 44 | 45 | if ind != -1 { 46 | *s = append((*s)[:ind], (*s)[ind+1:]...) 47 | } 48 | } 49 | 50 | func (s HTMLAttributes) Exists(attr string) bool { 51 | _, ok := s[template.HTMLAttr(attr)] 52 | return ok 53 | } 54 | 55 | func (s HTMLAttributes) FillFrom(data map[string]interface{}) { 56 | for k, v := range data { 57 | s[template.HTMLAttr(k)] = v 58 | } 59 | } 60 | 61 | func (s HTMLAttributes) FillFromStringMap(data map[string]string) { 62 | for k, v := range data { 63 | s[template.HTMLAttr(k)] = v 64 | } 65 | } 66 | 67 | func (s HTMLData) Exists(key string) bool { 68 | _, ok := s[key] 69 | return ok 70 | } 71 | -------------------------------------------------------------------------------- /common/tplfunc.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | // Package common This package provides basic constants used by forms packages. 20 | package common 21 | 22 | import ( 23 | "html/template" 24 | "io" 25 | "io/fs" 26 | "os" 27 | "path/filepath" 28 | ) 29 | 30 | var TplFuncs = func() template.FuncMap { 31 | return template.FuncMap{} 32 | } 33 | 34 | func ParseFiles(files ...string) (*template.Template, error) { 35 | if !FileSystem.IsEmpty() { 36 | return ParseFS(FileSystem, files...) 37 | } 38 | name := filepath.Base(files[0]) 39 | b, err := os.ReadFile(files[0]) 40 | if err != nil { 41 | return nil, err 42 | } 43 | tmpl := template.New(name) 44 | tmpl.Funcs(TplFuncs()) 45 | tmpl = template.Must(tmpl.Parse(string(b))) 46 | if len(files) > 1 { 47 | tmpl, err = tmpl.ParseFiles(files[1:]...) 48 | } 49 | return tmpl, err 50 | } 51 | 52 | func ParseFS(fs fs.FS, files ...string) (*template.Template, error) { 53 | name := filepath.Base(files[0]) 54 | tmpl := template.New(name) 55 | tmpl.Funcs(TplFuncs()) 56 | fp, err := fs.Open(files[0]) 57 | if err != nil { 58 | return tmpl, err 59 | } 60 | b, err := io.ReadAll(fp) 61 | fp.Close() 62 | if err != nil { 63 | return tmpl, err 64 | } 65 | tmpl = template.Must(tmpl.Parse(string(b))) 66 | if len(files) > 1 { 67 | tmpl, err = tmpl.ParseFS(fs, files[1:]...) 68 | } 69 | return tmpl, err 70 | } 71 | -------------------------------------------------------------------------------- /common/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package common 20 | 21 | import ( 22 | "bytes" 23 | "errors" 24 | "html/template" 25 | "io/fs" 26 | "log" 27 | "os" 28 | "path/filepath" 29 | "reflect" 30 | "strings" 31 | "sync" 32 | 33 | "github.com/coscms/forms/config" 34 | "github.com/webx-top/tagfast" 35 | "golang.org/x/sync/singleflight" 36 | ) 37 | 38 | // Available form themes 39 | const ( 40 | BASE = "base" 41 | BOOTSTRAP = "bootstrap3" 42 | ) 43 | 44 | var ( 45 | tmplDirs = map[string]string{ 46 | BASE: "templates", 47 | BOOTSTRAP: "templates", 48 | } 49 | LabelFn = func(s string) string { 50 | return s 51 | } 52 | 53 | //private 54 | cachedTemplate = make(map[string]*template.Template) 55 | cachedConfig = make(map[string]*config.Config) 56 | lockTemplate = new(sync.RWMutex) 57 | lockConfig = new(sync.RWMutex) 58 | lockTmplDir = new(sync.RWMutex) 59 | sg singleflight.Group 60 | ) 61 | 62 | const ( 63 | PACKAGE_NAME = "github.com/coscms/forms" 64 | ) 65 | 66 | // Input field types 67 | const ( 68 | BUTTON = "button" 69 | CHECKBOX = "checkbox" 70 | COLOR = "color" 71 | DATE = "date" 72 | DATETIME = "datetime" 73 | DATETIME_LOCAL = "datetime-local" 74 | EMAIL = "email" 75 | FILE = "file" 76 | HIDDEN = "hidden" 77 | IMAGE = "image" 78 | MONTH = "month" 79 | NUMBER = "number" 80 | PASSWORD = "password" 81 | RADIO = "radio" 82 | RANGE = "range" 83 | RESET = "reset" 84 | SEARCH = "search" 85 | SUBMIT = "submit" 86 | TEL = "tel" 87 | TEXT = "text" 88 | TIME = "time" 89 | URL = "url" 90 | WEEK = "week" 91 | TEXTAREA = "textarea" 92 | SELECT = "select" 93 | STATIC = "static" 94 | ) 95 | 96 | func SetTmplDir(theme, tmplDir string) { 97 | lockTmplDir.Lock() 98 | tmplDirs[theme] = tmplDir 99 | lockTmplDir.Unlock() 100 | } 101 | 102 | func TmplDir(theme string) (tmplDir string) { 103 | tmplDir, _ = tmplDirs[theme] 104 | return 105 | } 106 | 107 | // LookupPath creates the complete path of the desired widget template 108 | func LookupPath(widget string) string { 109 | if !FileSystem.IsEmpty() { 110 | fp, err := FileSystem.Open(widget) 111 | if err != nil { 112 | if !errors.Is(err, fs.ErrNotExist) { 113 | log.Println(err.Error()) 114 | return widget 115 | } 116 | } else { 117 | defer fp.Close() 118 | fi, err := fp.Stat() 119 | if err == nil && !fi.IsDir() { 120 | return widget 121 | } 122 | } 123 | } 124 | if !TmplExists(widget) { 125 | return filepath.Join(os.Getenv("GOPATH"), "src", PACKAGE_NAME, `defaults`, widget) 126 | } 127 | return widget 128 | } 129 | 130 | func TmplExists(tmpl string) bool { 131 | _, err := os.Stat(tmpl) 132 | return !os.IsNotExist(err) 133 | } 134 | 135 | func GetOrSetCachedTemplate(cachedKey string, generator func() (*template.Template, error)) (c *template.Template, err error) { 136 | var ok bool 137 | lockTemplate.RLock() 138 | c, ok = cachedTemplate[cachedKey] 139 | lockTemplate.RUnlock() 140 | if ok { 141 | return c, nil 142 | } 143 | getValue, getErr, _ := sg.Do(cachedKey, func() (interface{}, error) { 144 | c, err = generator() 145 | if err != nil { 146 | return nil, err 147 | } 148 | lockTemplate.Lock() 149 | cachedTemplate[cachedKey] = c 150 | lockTemplate.Unlock() 151 | return c, nil 152 | }) 153 | if getErr != nil { 154 | return nil, getErr 155 | } 156 | return getValue.(*template.Template), nil 157 | } 158 | 159 | func ClearCachedTemplate() { 160 | lockTemplate.Lock() 161 | cachedTemplate = make(map[string]*template.Template) 162 | lockTemplate.Unlock() 163 | } 164 | 165 | func DelCachedTemplate(key string) bool { 166 | lockTemplate.Lock() 167 | defer lockTemplate.Unlock() 168 | if _, ok := cachedTemplate[key]; ok { 169 | delete(cachedTemplate, key) 170 | return true 171 | } 172 | return false 173 | } 174 | 175 | func GetOrSetCachedConfig(cachedKey string, generator func() (*config.Config, error)) (c *config.Config, err error) { 176 | var ok bool 177 | lockConfig.RLock() 178 | c, ok = cachedConfig[cachedKey] 179 | lockConfig.RUnlock() 180 | if ok { 181 | return c, nil 182 | } 183 | getValue, getErr, _ := sg.Do(cachedKey, func() (interface{}, error) { 184 | c, err = generator() 185 | if err != nil { 186 | return nil, err 187 | } 188 | lockConfig.Lock() 189 | cachedConfig[cachedKey] = c 190 | lockConfig.Unlock() 191 | return c, nil 192 | }) 193 | if getErr != nil { 194 | return nil, getErr 195 | } 196 | return getValue.(*config.Config), nil 197 | } 198 | 199 | func ClearCachedConfig() { 200 | lockConfig.Lock() 201 | cachedConfig = make(map[string]*config.Config) 202 | lockConfig.Unlock() 203 | } 204 | 205 | func DelCachedConfig(key string) bool { 206 | lockConfig.Lock() 207 | defer lockConfig.Unlock() 208 | if _, ok := cachedConfig[key]; ok { 209 | delete(cachedConfig, key) 210 | return true 211 | } 212 | return false 213 | } 214 | 215 | func ParseTmpl(data interface{}, 216 | fn_tpl template.FuncMap, 217 | fn_fixTpl func(tpls ...string) ([]string, error), 218 | tpls ...string) string { 219 | buf := bytes.NewBuffer(nil) 220 | tpf := strings.Join(tpls, `|`) 221 | tpl, err := GetOrSetCachedTemplate(tpf, func() (*template.Template, error) { 222 | c := template.New(filepath.Base(tpls[0])) 223 | if fn_tpl != nil { 224 | c.Funcs(fn_tpl) 225 | } 226 | var err error 227 | if fn_fixTpl != nil { 228 | tpls, err = fn_fixTpl(tpls...) 229 | if err != nil { 230 | return nil, err 231 | } 232 | } 233 | if !FileSystem.IsEmpty() { 234 | return c.ParseFS(FileSystem, tpls...) 235 | 236 | } 237 | return c.ParseFiles(tpls...) 238 | }) 239 | if err != nil { 240 | return err.Error() 241 | } 242 | err = tpl.Execute(buf, data) 243 | if err != nil { 244 | return err.Error() 245 | } 246 | return buf.String() 247 | } 248 | 249 | func TagVal(t reflect.Type, fieldNo int, tagName string) string { 250 | return tagfast.Value(t, t.Field(fieldNo), tagName) 251 | } 252 | 253 | func Tag(t reflect.Type, f reflect.StructField, tagName string) (value string, tf tagfast.Faster) { 254 | return tagfast.Tag(t, f, tagName) 255 | } 256 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package config 20 | 21 | type Config struct { 22 | ID string `json:"id"` 23 | Theme string `json:"theme"` 24 | Template string `json:"template"` 25 | Method string `json:"method"` 26 | Action string `json:"action"` 27 | Attributes [][]string `json:"attributes"` // 28 | WithButtons bool `json:"withButtons"` 29 | Buttons []string `json:"buttons"` 30 | BtnsTemplate string `json:"btnsTemplate"` 31 | Elements []*Element `json:"elements"` 32 | Languages []*Language `json:"languages"` 33 | Data map[string]interface{} `json:"data,omitempty"` 34 | TrimNamePrefix string `json:"trimNamePrefix,omitempty"` 35 | } 36 | 37 | func (c *Config) Merge(source *Config) *Config { 38 | if len(c.ID) == 0 && len(source.ID) > 0 { 39 | c.ID = source.ID 40 | } 41 | if len(c.Theme) == 0 && len(source.Theme) > 0 { 42 | c.Theme = source.Theme 43 | } 44 | if len(c.Template) == 0 && len(source.Template) > 0 { 45 | c.Template = source.Template 46 | } 47 | if len(c.Method) == 0 && len(source.Method) > 0 { 48 | c.Method = source.Method 49 | } 50 | if len(c.Action) == 0 && len(source.Action) > 0 { 51 | c.Action = source.Action 52 | } 53 | var found bool 54 | for _, v := range source.Attributes { 55 | if len(v) == 0 { 56 | continue 57 | } 58 | for _, v2 := range c.Attributes { 59 | if len(v2) == 0 { 60 | continue 61 | } 62 | if v2[0] == v[0] { 63 | found = true 64 | break 65 | } 66 | } 67 | if !found { 68 | c.Attributes = append(c.Attributes, v) 69 | } else { 70 | found = false 71 | } 72 | } 73 | for _, v := range source.Buttons { 74 | for _, v2 := range c.Buttons { 75 | if v == v2 { 76 | found = true 77 | break 78 | } 79 | } 80 | if !found { 81 | c.Buttons = append(c.Buttons, v) 82 | } else { 83 | found = false 84 | } 85 | } 86 | if c.WithButtons != source.WithButtons { 87 | c.WithButtons = source.WithButtons 88 | } 89 | if len(c.BtnsTemplate) == 0 && len(source.BtnsTemplate) > 0 { 90 | c.BtnsTemplate = source.BtnsTemplate 91 | } 92 | for _, v := range source.Elements { 93 | if len(v.Name) > 0 { 94 | for _, v2 := range c.Elements { 95 | if v.Name == v2.Name { 96 | found = true 97 | v2.Merge(v) 98 | break 99 | } 100 | } 101 | } 102 | if !found { 103 | c.Elements = append(c.Elements, v) 104 | } else { 105 | found = false 106 | } 107 | } 108 | for _, v := range source.Languages { 109 | if len(v.ID) > 0 { 110 | for _, v2 := range c.Languages { 111 | if v.ID == v2.ID { 112 | found = true 113 | break 114 | } 115 | } 116 | } 117 | if !found { 118 | c.Languages = append(c.Languages, v) 119 | } else { 120 | found = false 121 | } 122 | } 123 | if source.Data != nil { 124 | if c.Data == nil { 125 | c.Data = map[string]interface{}{} 126 | } 127 | for k, v := range source.Data { 128 | c.Data[k] = v 129 | } 130 | } 131 | if len(c.TrimNamePrefix) == 0 && len(source.TrimNamePrefix) > 0 { 132 | c.TrimNamePrefix = source.TrimNamePrefix 133 | } 134 | return c 135 | } 136 | 137 | func (c *Config) AddElement(elements ...*Element) *Config { 138 | c.Elements = append(c.Elements, elements...) 139 | return c 140 | } 141 | 142 | func (c *Config) AddLanguage(languages ...*Language) *Config { 143 | c.Languages = append(c.Languages, languages...) 144 | return c 145 | } 146 | 147 | func (c *Config) AddButton(buttons ...string) *Config { 148 | c.Buttons = append(c.Buttons, buttons...) 149 | return c 150 | } 151 | 152 | func (c *Config) AddAttribute(attributes ...string) *Config { 153 | c.Attributes = append(c.Attributes, attributes) 154 | return c 155 | } 156 | 157 | func (c *Config) Set(name string, value interface{}) *Config { 158 | if c.Data == nil { 159 | c.Data = map[string]interface{}{} 160 | } 161 | c.Data[name] = value 162 | return c 163 | } 164 | 165 | func (c *Config) Clone() *Config { 166 | elements := make([]*Element, len(c.Elements)) 167 | for index, elem := range c.Elements { 168 | elements[index] = elem.Clone() 169 | } 170 | languages := make([]*Language, len(c.Languages)) 171 | for index, value := range c.Languages { 172 | languages[index] = value.Clone() 173 | } 174 | r := &Config{ 175 | ID: c.ID, 176 | Theme: c.Theme, 177 | Template: c.Template, 178 | Method: c.Method, 179 | Action: c.Action, 180 | Attributes: make([][]string, len(c.Attributes)), 181 | WithButtons: c.WithButtons, 182 | Buttons: make([]string, len(c.Buttons)), 183 | BtnsTemplate: c.BtnsTemplate, 184 | Elements: elements, 185 | Languages: languages, 186 | Data: map[string]interface{}{}, 187 | } 188 | copy(r.Buttons, c.Buttons) 189 | for k, v := range c.Data { 190 | r.Data[k] = v 191 | } 192 | for k, v := range c.Attributes { 193 | cv := make([]string, len(v)) 194 | copy(cv, v) 195 | r.Attributes[k] = cv 196 | } 197 | return r 198 | } 199 | 200 | func (c *Config) HasName(name string) bool { 201 | return c.hasName(name, c.Elements, c.Languages) 202 | } 203 | 204 | func (c *Config) hasName(name string, elements []*Element, languages []*Language) bool { 205 | for _, elem := range elements { 206 | if elem.Name == name { 207 | return elem.Type != `langset` && elem.Type != `fieldset` 208 | } 209 | if elem.Type == `langset` { 210 | if c.hasName(name, elem.Elements, elem.Languages) { 211 | return true 212 | } 213 | continue 214 | } 215 | if elem.Type == `fieldset` { 216 | if c.hasName(name, elem.Elements, languages) { 217 | return true 218 | } 219 | continue 220 | } 221 | if len(languages) == 0 { 222 | continue 223 | } 224 | for _, lang := range languages { 225 | if lang.HasName(name) || name == lang.Name(elem.Name) { 226 | return true 227 | } 228 | } 229 | } 230 | return false 231 | } 232 | 233 | func (c *Config) GetNames() []string { 234 | return getNames(c.Elements, c.Languages) 235 | } 236 | 237 | func (c *Config) SetDefaultValue(fieldDefaultValue func(fieldName string) string) { 238 | if fieldDefaultValue != nil { 239 | setDefaultValue(c.Elements, c.Languages, fieldDefaultValue) 240 | } 241 | } 242 | 243 | func (c *Config) SetValue(fieldValue func(fieldName string) string) { 244 | if fieldValue != nil { 245 | setValue(c.Elements, c.Languages, fieldValue) 246 | } 247 | } 248 | 249 | func (c *Config) GetValue(fieldValue func(fieldName string, fieldValue string) error) error { 250 | if fieldValue != nil { 251 | return getValue(c.Elements, c.Languages, fieldValue) 252 | } 253 | return nil 254 | } 255 | -------------------------------------------------------------------------------- /config/element.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strings" 4 | 5 | type Element struct { 6 | ID string `json:"id"` 7 | Type string `json:"type"` 8 | Name string `json:"name"` 9 | Label string `json:"label"` 10 | LabelCols int `json:"labelCols,omitempty"` 11 | FieldCols int `json:"fieldCols,omitempty"` 12 | LabelClasses []string `json:"labelClasses,omitempty"` 13 | Value string `json:"value"` 14 | HelpText string `json:"helpText"` 15 | Template string `json:"template"` 16 | Valid string `json:"valid"` 17 | Attributes [][]string `json:"attributes"` 18 | Choices []*Choice `json:"choices"` 19 | Elements []*Element `json:"elements"` 20 | Format string `json:"format"` 21 | Languages []*Language `json:"languages,omitempty"` 22 | Data map[string]interface{} `json:"data,omitempty"` 23 | } 24 | 25 | func (c *Element) Merge(source *Element) *Element { 26 | if len(c.ID) == 0 && len(source.ID) > 0 { 27 | c.ID = source.ID 28 | } 29 | if len(c.Type) == 0 && len(source.Type) > 0 { 30 | c.Type = source.Type 31 | } 32 | if len(c.Template) == 0 && len(source.Template) > 0 { 33 | c.Template = source.Template 34 | } 35 | if len(c.Label) == 0 && len(source.Label) > 0 { 36 | c.Label = source.Label 37 | } 38 | if len(c.Name) == 0 && len(source.Name) > 0 { 39 | c.Name = source.Name 40 | } 41 | if c.LabelCols == 0 && source.LabelCols > 0 { 42 | c.LabelCols = source.LabelCols 43 | } 44 | if c.FieldCols == 0 && source.FieldCols > 0 { 45 | c.FieldCols = source.FieldCols 46 | } 47 | if len(c.Value) == 0 && len(source.Value) > 0 { 48 | c.Value = source.Value 49 | } 50 | if len(c.HelpText) == 0 && len(source.HelpText) > 0 { 51 | c.HelpText = source.HelpText 52 | } 53 | if len(c.Valid) == 0 && len(source.Valid) > 0 { 54 | c.Valid = source.Valid 55 | } 56 | if len(c.Format) == 0 && len(source.Format) > 0 { 57 | c.Format = source.Format 58 | } 59 | var found bool 60 | for _, v := range source.Attributes { 61 | if len(v) == 0 { 62 | continue 63 | } 64 | for _, v2 := range c.Attributes { 65 | if len(v2) == 0 { 66 | continue 67 | } 68 | if v2[0] == v[0] { 69 | found = true 70 | break 71 | } 72 | } 73 | if !found { 74 | c.Attributes = append(c.Attributes, v) 75 | } else { 76 | found = false 77 | } 78 | } 79 | for _, v := range source.LabelClasses { 80 | for _, v2 := range c.LabelClasses { 81 | if v == v2 { 82 | found = true 83 | break 84 | } 85 | } 86 | if !found { 87 | c.LabelClasses = append(c.LabelClasses, v) 88 | } else { 89 | found = false 90 | } 91 | } 92 | for _, v := range source.Choices { 93 | for _, v2 := range c.Choices { 94 | if v.Group == v2.Group { 95 | found = true 96 | v2.Merge(v) 97 | break 98 | } 99 | } 100 | if !found { 101 | c.Choices = append(c.Choices, v) 102 | } else { 103 | found = false 104 | } 105 | } 106 | for _, v := range source.Elements { 107 | if len(v.Name) > 0 { 108 | for _, v2 := range c.Elements { 109 | if v.Name == v2.Name { 110 | found = true 111 | v2.Merge(v) 112 | break 113 | } 114 | } 115 | } 116 | if !found { 117 | c.Elements = append(c.Elements, v) 118 | } else { 119 | found = false 120 | } 121 | } 122 | for _, v := range source.Languages { 123 | if len(v.ID) > 0 { 124 | for _, v2 := range c.Languages { 125 | if v.ID == v2.ID { 126 | found = true 127 | break 128 | } 129 | } 130 | } 131 | if !found { 132 | c.Languages = append(c.Languages, v) 133 | } else { 134 | found = false 135 | } 136 | } 137 | if source.Data != nil { 138 | if c.Data == nil { 139 | c.Data = map[string]interface{}{} 140 | } 141 | for k, v := range source.Data { 142 | c.Data[k] = v 143 | } 144 | } 145 | return c 146 | } 147 | 148 | func (e *Element) Cols() int { 149 | return GetCols(e.LabelCols, e.FieldCols) 150 | } 151 | 152 | func (e *Element) Clone() *Element { 153 | elements := make([]*Element, len(e.Elements)) 154 | languages := make([]*Language, len(e.Languages)) 155 | choices := make([]*Choice, len(e.Choices)) 156 | for index, elem := range e.Elements { 157 | elements[index] = elem.Clone() 158 | } 159 | for index, value := range e.Languages { 160 | languages[index] = value.Clone() 161 | } 162 | for index, value := range e.Choices { 163 | choices[index] = value.Clone() 164 | } 165 | r := &Element{ 166 | ID: e.ID, 167 | Type: e.Type, 168 | Name: e.Name, 169 | Label: e.Label, 170 | LabelCols: e.LabelCols, 171 | FieldCols: e.FieldCols, 172 | LabelClasses: make([]string, len(e.LabelClasses)), 173 | Value: e.Value, 174 | HelpText: e.HelpText, 175 | Template: e.Template, 176 | Valid: e.Valid, 177 | Attributes: make([][]string, len(e.Attributes)), 178 | Choices: choices, 179 | Elements: elements, 180 | Format: e.Format, 181 | Languages: languages, 182 | Data: map[string]interface{}{}, 183 | } 184 | for k, v := range e.Data { 185 | r.Data[k] = v 186 | } 187 | if len(e.LabelClasses) > 0 { 188 | copy(r.LabelClasses, e.LabelClasses) 189 | } 190 | for k, v := range e.Attributes { 191 | cv := make([]string, len(v)) 192 | copy(cv, v) 193 | r.Attributes[k] = cv 194 | } 195 | return r 196 | } 197 | 198 | func (e *Element) HasAttr(attrs ...string) bool { 199 | mk := map[string]struct{}{} 200 | for _, attr := range attrs { 201 | mk[strings.ToLower(attr)] = struct{}{} 202 | } 203 | for _, v := range e.Attributes { 204 | if len(v) == 0 || len(v[0]) == 0 { 205 | continue 206 | } 207 | v[0] = strings.ToLower(v[0]) 208 | if _, ok := mk[v[0]]; ok { 209 | return true 210 | } 211 | } 212 | return false 213 | } 214 | 215 | func (e *Element) AddElement(elements ...*Element) *Element { 216 | e.Elements = append(e.Elements, elements...) 217 | return e 218 | } 219 | 220 | func (e *Element) AddLanguage(languages ...*Language) *Element { 221 | e.Languages = append(e.Languages, languages...) 222 | return e 223 | } 224 | 225 | func (e *Element) AddAttribute(attributes ...string) *Element { 226 | e.Attributes = append(e.Attributes, attributes) 227 | return e 228 | } 229 | 230 | func (e *Element) AddChoice(choices ...*Choice) *Element { 231 | e.Choices = append(e.Choices, choices...) 232 | return e 233 | } 234 | 235 | func (e *Element) AddLabelClass(labelClasses ...string) *Element { 236 | e.LabelClasses = append(e.LabelClasses, labelClasses...) 237 | return e 238 | } 239 | 240 | func (e *Element) Set(name string, value interface{}) *Element { 241 | if e.Data == nil { 242 | e.Data = map[string]interface{}{} 243 | } 244 | e.Data[name] = value 245 | return e 246 | } 247 | -------------------------------------------------------------------------------- /config/element_choice.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Choice struct { 4 | Group string `json:"group"` 5 | Option []string `json:"option"` //["value","text"] 6 | Checked bool `json:"checked"` 7 | } 8 | 9 | func (c *Choice) Clone() *Choice { 10 | r := &Choice{ 11 | Group: c.Group, 12 | Option: make([]string, len(c.Option)), 13 | Checked: c.Checked, 14 | } 15 | copy(r.Option, c.Option) 16 | return r 17 | } 18 | 19 | func (c *Choice) Merge(source *Choice) *Choice { 20 | var found bool 21 | for _, v := range source.Option { 22 | if len(v) == 0 { 23 | continue 24 | } 25 | for _, v2 := range c.Option { 26 | if len(v2) == 0 { 27 | continue 28 | } 29 | if v[0] == v2[0] { 30 | found = true 31 | break 32 | } 33 | } 34 | if !found { 35 | c.Option = append(c.Option, v) 36 | } else { 37 | found = false 38 | } 39 | } 40 | return c 41 | } 42 | -------------------------------------------------------------------------------- /config/element_interface.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "html/template" 4 | 5 | // FormElement interface defines a form object (usually a Field or a FieldSet) that can be rendered as a template.HTML object. 6 | type FormElement interface { 7 | Render() template.HTML 8 | Name() string 9 | Cols() int 10 | OriginalName() string 11 | SetName(string) 12 | String() string 13 | SetData(key string, value interface{}) 14 | Data() map[string]interface{} 15 | SetLang(lang string) 16 | Lang() string 17 | Clone() FormElement 18 | } 19 | 20 | type HasError interface { 21 | HasError() bool 22 | } 23 | -------------------------------------------------------------------------------- /config/element_list_group.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Grouped struct { 4 | HasError bool 5 | Elements []FormElement 6 | } 7 | 8 | type Groups []Grouped 9 | 10 | func SplitGroup(elements []FormElement) Groups { 11 | result := Groups{} 12 | t := 0 13 | g := Grouped{} 14 | for idx, ele := range elements { 15 | if idx == 0 { 16 | if !g.HasError { 17 | if he, ok := ele.(HasError); ok { 18 | g.HasError = he.HasError() 19 | } 20 | } 21 | g.Elements = append(g.Elements, ele) 22 | t += ele.Cols() 23 | } else { 24 | cols := ele.Cols() 25 | if cols == 0 || t+cols > 12 { 26 | result = append(result, g) 27 | g = Grouped{} 28 | t = 0 29 | } 30 | if !g.HasError { 31 | if he, ok := ele.(HasError); ok { 32 | g.HasError = he.HasError() 33 | } 34 | } 35 | g.Elements = append(g.Elements, ele) 36 | t += ele.Cols() 37 | } 38 | } 39 | if len(g.Elements) > 0 { 40 | result = append(result, g) 41 | } 42 | return result 43 | } 44 | -------------------------------------------------------------------------------- /config/element_list_group_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/coscms/forms/config" 7 | "github.com/coscms/forms/fields" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/webx-top/com" 10 | ) 11 | 12 | func TestSplitGroup(t *testing.T) { 13 | r := config.SplitGroup([]config.FormElement{ 14 | &fields.Field{ 15 | OrigName: `1`, 16 | LabelCols: 0, 17 | FieldCols: 4, 18 | }, 19 | &fields.Field{ 20 | OrigName: `2`, 21 | LabelCols: 0, 22 | FieldCols: 4, 23 | }, 24 | &fields.Field{ 25 | OrigName: `3`, 26 | LabelCols: 0, 27 | FieldCols: 8, 28 | }, 29 | &fields.Field{ 30 | OrigName: `4`, 31 | LabelCols: 0, 32 | FieldCols: 4, 33 | }, 34 | &fields.Field{ 35 | OrigName: `5`, 36 | LabelCols: 0, 37 | FieldCols: 4, 38 | Errors: []string{`Test`}, 39 | }, 40 | }) 41 | assert.Equal(t, 3, len(r)) 42 | com.Dump(r) 43 | } 44 | -------------------------------------------------------------------------------- /config/language.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func NewLanguage(lang, label, namefmt string) *Language { 8 | return &Language{ 9 | ID: lang, 10 | Label: label, 11 | NameFormat: namefmt, 12 | fields: make([]FormElement, 0), 13 | fieldMap: make(map[string]int), 14 | } 15 | } 16 | 17 | type Language struct { 18 | ID string `json:"id"` 19 | Label string `json:"label"` 20 | NameFormat string `json:"nameFormat"` 21 | fields []FormElement 22 | fieldMap map[string]int 23 | } 24 | 25 | func (l *Language) Name(name string) string { 26 | if len(l.NameFormat) == 0 { 27 | return name 28 | } 29 | if l.NameFormat == `~` { 30 | l.NameFormat = `Language[` + l.ID + `][%s]` 31 | } 32 | return fmt.Sprintf(l.NameFormat, name) 33 | } 34 | 35 | func (l *Language) HasName(name string) bool { 36 | if l.fieldMap == nil { 37 | return false 38 | } 39 | _, ok := l.fieldMap[name] 40 | return ok 41 | } 42 | 43 | func (l *Language) AddField(f ...FormElement) { 44 | if l.fieldMap == nil { 45 | l.fieldMap = map[string]int{} 46 | l.fields = []FormElement{} 47 | } 48 | for _, field := range f { 49 | name := l.Name(field.OriginalName()) 50 | if _, ok := l.fieldMap[name]; ok { 51 | continue 52 | } 53 | l.fieldMap[name] = len(l.fields) 54 | l.fields = append(l.fields, field) 55 | } 56 | } 57 | 58 | func (l *Language) Field(name string) FormElement { 59 | if l.fieldMap == nil { 60 | return nil 61 | } 62 | if idx, ok := l.fieldMap[l.Name(name)]; ok { 63 | return l.fields[idx] 64 | } 65 | return nil 66 | } 67 | 68 | func (l *Language) Fields() []FormElement { 69 | return l.fields 70 | } 71 | 72 | func (l *Language) Groups() Groups { 73 | return SplitGroup(l.fields) 74 | } 75 | 76 | func (l *Language) Clone() *Language { 77 | lg := NewLanguage(l.ID, l.Label, l.NameFormat) 78 | copy(lg.fields, l.fields) 79 | for k, v := range l.fieldMap { 80 | lg.fieldMap[k] = v 81 | } 82 | return lg 83 | } 84 | -------------------------------------------------------------------------------- /config/utils.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | STATIC = "static" 5 | Disabled = "disabled" 6 | Readonly = "readonly" 7 | ) 8 | 9 | func getNames(elements []*Element, languages []*Language) []string { 10 | var names []string 11 | for _, elem := range elements { 12 | if elem.Type == `langset` { 13 | names = append(names, getNames(elem.Elements, elem.Languages)...) 14 | continue 15 | } 16 | if elem.Type == `fieldset` { 17 | names = append(names, getNames(elem.Elements, languages)...) 18 | continue 19 | } 20 | if len(elem.Name) > 0 && elem.Type != STATIC && !elem.HasAttr(Disabled, Readonly) { 21 | if len(languages) == 0 { 22 | names = append(names, elem.Name) 23 | } else { 24 | for _, lang := range languages { 25 | names = append(names, lang.Name(elem.Name)) 26 | } 27 | } 28 | } 29 | } 30 | return names 31 | } 32 | 33 | func setDefaultValue(elements []*Element, languages []*Language, fieldDefaultValue func(string) string) { 34 | for _, elem := range elements { 35 | if elem.Type == `langset` { 36 | setDefaultValue(elem.Elements, elem.Languages, fieldDefaultValue) 37 | continue 38 | } 39 | if elem.Type == `fieldset` { 40 | setDefaultValue(elem.Elements, languages, fieldDefaultValue) 41 | continue 42 | } 43 | if len(elem.Value) > 0 { 44 | continue 45 | } 46 | if len(elem.Name) > 0 { 47 | if len(languages) == 0 { 48 | elem.Value = fieldDefaultValue(elem.Name) 49 | continue 50 | } 51 | for _, lang := range languages { 52 | elem.Value = fieldDefaultValue(lang.Name(elem.Name)) 53 | } 54 | } 55 | } 56 | } 57 | 58 | func setValue(elements []*Element, languages []*Language, fieldValue func(string) string) { 59 | for _, elem := range elements { 60 | if elem.Type == `langset` { 61 | setValue(elem.Elements, elem.Languages, fieldValue) 62 | continue 63 | } 64 | if elem.Type == `fieldset` { 65 | setValue(elem.Elements, languages, fieldValue) 66 | continue 67 | } 68 | if len(elem.Name) > 0 { 69 | if len(languages) == 0 { 70 | elem.Value = fieldValue(elem.Name) 71 | continue 72 | } 73 | for _, lang := range languages { 74 | elem.Value = fieldValue(lang.Name(elem.Name)) 75 | } 76 | } 77 | } 78 | } 79 | 80 | func getValue(elements []*Element, languages []*Language, fieldValue func(string, string) error) (err error) { 81 | for _, elem := range elements { 82 | if elem.Type == `langset` { 83 | getValue(elem.Elements, elem.Languages, fieldValue) 84 | continue 85 | } 86 | if elem.Type == `fieldset` { 87 | getValue(elem.Elements, languages, fieldValue) 88 | continue 89 | } 90 | if len(elem.Name) > 0 { 91 | if len(languages) == 0 { 92 | if err = fieldValue(elem.Name, elem.Value); err != nil { 93 | return 94 | } 95 | continue 96 | } 97 | for _, lang := range languages { 98 | if err = fieldValue(lang.Name(elem.Name), elem.Value); err != nil { 99 | return 100 | } 101 | } 102 | } 103 | } 104 | return 105 | } 106 | 107 | func GetCols(labelCols int, fieldCols int) int { 108 | return GetLabelCols(labelCols) + GetFieldCols(fieldCols) 109 | } 110 | 111 | func GetLabelCols(labelCols int) int { 112 | if labelCols == 0 { 113 | labelCols = 2 114 | } 115 | return labelCols 116 | } 117 | 118 | func GetFieldCols(fieldCols int) int { 119 | if fieldCols == 0 { 120 | fieldCols = 8 121 | } 122 | return fieldCols 123 | } 124 | -------------------------------------------------------------------------------- /defaults/templates.go: -------------------------------------------------------------------------------- 1 | package defaults 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/coscms/forms/common" 7 | ) 8 | 9 | //go:embed templates 10 | var templateFS embed.FS 11 | 12 | func init() { 13 | common.FileSystem.Register(templateFS) 14 | } 15 | -------------------------------------------------------------------------------- /defaults/templates/allfields.html: -------------------------------------------------------------------------------- 1 | {{- range .fields }} 2 | {{- .Render }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/button.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/datetime/date.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/datetime/datetime.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/datetime/time.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/fieldset.html: -------------------------------------------------------------------------------- 1 | 2 | {{- range .fields }} 3 | {{- .Render }} 4 | {{- end }} 5 | -------------------------------------------------------------------------------- /defaults/templates/base/fieldset_buttons.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{- range .fields }} 4 | {{- .Render }} 5 |       6 | {{- end }} 7 |
8 |
-------------------------------------------------------------------------------- /defaults/templates/base/generic.html: -------------------------------------------------------------------------------- 1 | {{- define "generic" }} 2 | {{- if .label }} 3 | {{.label}} 4 | {{- end }} 5 | 6 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/input.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/langset.html: -------------------------------------------------------------------------------- 1 | {{- $langset := . }} 2 | 3 | {{- range .langs }} 4 |
5 | {{- range $langset.fields }} 6 | {{- .Render }} 7 | {{- end }} 8 |
9 | {{- end }} 10 | -------------------------------------------------------------------------------- /defaults/templates/base/number/number.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/number/range.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/options/checkbox.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- $p := . }} 3 | {{- range .choices }} 4 | {{.Val}} 5 | 6 | {{- end }} 7 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/options/radiobutton.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- $p := . }} 3 | {{- range .choices }} 4 | {{.Val}} 5 | 6 | {{- end }} 7 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/options/select.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- if .label }} 3 | {{.label}} 4 | {{- end }} 5 | 19 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/static.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- if .label }} 3 | {{.label}} 4 | {{- end }} 5 |

{{.text}}

6 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/text/passwordinput.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- if .label }} 3 | {{.label}} 4 | {{- end }} 5 | 6 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/text/textareainput.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- if .label }} 3 | {{.label}} 4 | {{- end }} 5 | 7 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/base/text/textinput.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- if .label }} 3 | {{.label}} 4 | {{- end }} 5 | 6 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/baseform.html: -------------------------------------------------------------------------------- 1 | 2 | {{- range .fields }} 3 | {{- .Render }} 4 | {{- end }} 5 | -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/button.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/datetime/date.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/datetime/datetime.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/datetime/time.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/fieldset.html: -------------------------------------------------------------------------------- 1 | 2 | {{range .fields}}{{ .Render }}{{end}} 3 | -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/fieldset_buttons.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{- range .fields }} 4 | {{.Render}} 5 |       6 | {{- end }} 7 |
8 |
-------------------------------------------------------------------------------- /defaults/templates/bootstrap3/generic.html: -------------------------------------------------------------------------------- 1 | {{- define "generic" }} 2 | {{- if eq .type "hidden"}} 3 | 4 | {{- else }} 5 |
6 | {{- if .label }} 7 | 8 | {{- end }} 9 | 10 | {{- if or .helptext .errors }} 11 | 12 | {{if .helptext}}{{ .helptext }}{{end}} 13 | {{- if .errors }} 14 |
    15 | {{- range .errors }} 16 |
  • {{.}}
  • 17 | {{- end }} 18 |
19 | {{- end }} 20 |
21 | {{- end }} 22 |
23 | {{- end }} 24 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/input.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/langset.html: -------------------------------------------------------------------------------- 1 | {{- $langset := . }} 2 | 3 | 4 | 9 | 10 |
11 | {{- range $k, $v := .langs }} 12 |
13 | {{- range $v.Fields }} 14 | {{- .Render }} 15 | {{- end }} 16 |
17 | {{- end }} 18 |
19 | -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/number/number.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/number/range.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- template "generic" . }} 3 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/options/checkbox.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- $p := . }} 3 |
4 | {{- if .label }} 5 | 6 | {{- end }} 7 | {{- range .choices }} 8 |
9 | 13 |
14 | {{- end }} 15 |
16 | {{- end }} 17 | -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/options/radiobutton.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 | {{- $p := . }} 3 |
4 | {{- if .label }} 5 | 6 | {{- end }} 7 | {{- range .choices }} 8 |
9 | 13 |
14 | {{- end }} 15 |
16 | {{- end }} 17 | -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/options/select.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 |
3 | {{- if .label }} 4 | 5 | {{- end }} 6 | 20 | {{- if or .helptext .errors }} 21 | 22 | {{if .helptext}}{{ .helptext }}{{end}} 23 | {{- if .errors }} 24 |
    25 | {{- range .errors }} 26 |
  • {{.}}
  • 27 | {{- end }} 28 |
29 | {{- end }} 30 |
31 | {{- end }} 32 |
33 | {{- end }} 34 | -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/static.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 |
3 | {{- if .label }} 4 | {{.label}} 5 | {{- end }} 6 |

{{.text}}

7 |
8 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/text/passwordinput.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 |
3 | {{- if .label }} 4 | 5 | {{- end }} 6 | 7 | {{- if or .helptext .errors }} 8 | 9 | {{if .helptext}}{{ .helptext }}{{end}} 10 | {{- if .errors }} 11 |
    12 | {{- range .errors }} 13 |
  • {{.}}
  • 14 | {{-end }} 15 |
16 | {{- end }} 17 |
18 | {{- end }} 19 |
20 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/text/textareainput.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 |
3 | {{- if .label }} 4 | 5 | {{- end }} 6 | 8 | {{- if or .helptext .errors }} 9 | 10 | {{if .helptext}}{{ .helptext }}{{end}} 11 | {{- if .errors }} 12 |
    13 | {{- range .errors }} 14 |
  • {{.}}
  • 15 | {{- end }} 16 |
17 | {{- end }} 18 |
19 | {{- end }} 20 |
21 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrap3/text/textinput.html: -------------------------------------------------------------------------------- 1 | {{- define "main" }} 2 |
3 | {{- if .label }} 4 | 5 | {{- end }} 6 | 7 | {{- if or .helptext .errors }} 8 | 9 | {{if .helptext}}{{ .helptext }}{{end}} 10 | {{- if .errors }} 11 |
    12 | {{- range .errors }} 13 |
  • {{.}}
  • 14 | {{- end }} 15 |
16 | {{- end }} 17 |
18 | {{- end }} 19 |
20 | {{- end }} -------------------------------------------------------------------------------- /defaults/templates/bootstrapform.html: -------------------------------------------------------------------------------- 1 |
2 | {{- range .fields }} 3 | {{- .Render }} 4 | {{- end }} 5 |
-------------------------------------------------------------------------------- /example/forms.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme":"bootstrap3", 3 | "method":"POST", 4 | "action":"/index", 5 | "elements":[ 6 | { 7 | "type":"text", 8 | "name":"user", 9 | "label":"User Name", 10 | "value":"", 11 | "valid":"required;maxSize(3)", 12 | "attributes":[ 13 | ["class","textInput"], 14 | ["style","border:1px solid #ccc"] 15 | ] 16 | }, 17 | { 18 | "type":"fieldset", 19 | "label":"Alias Name", 20 | "elements":[ 21 | { 22 | "type":"text", 23 | "name":"user1", 24 | "lable":"User Name1", 25 | "value":"" 26 | }, 27 | { 28 | "type":"text", 29 | "name":"user2", 30 | "lable":"User Name2", 31 | "value":"" 32 | } 33 | ] 34 | }, 35 | { 36 | "type":"select", 37 | "name":"birthday", 38 | "label":"Birthday", 39 | "valid":"required", 40 | "choices":[ 41 | {"option":["1983","1983"],"checked":true}, 42 | {"option":["1984","1984"]}, 43 | {"option":["1985","1985"]} 44 | // {"option":["1985","1985"]} 45 | ] 46 | }, 47 | { 48 | "type":"langset", 49 | "name":"title", 50 | "label":"Title", 51 | "languages":[ 52 | { 53 | "id":"en", 54 | "label":"英语", 55 | "nameFormat":"~" 56 | }, 57 | { 58 | "id":"zh-CN", 59 | "label":"中文", 60 | "nameFormat":"~" 61 | } 62 | ], 63 | "elements":[ 64 | { 65 | "type":"text", 66 | "name":"title", 67 | "label":"标题" 68 | }, 69 | { 70 | "type":"textarea", 71 | "name":"content", 72 | "label":"内容" 73 | }, 74 | { 75 | "type":"fieldset", 76 | "name":"seo", 77 | "label":"meta", 78 | "elements":[ 79 | { 80 | "type":"text", 81 | "name":"meta_title", 82 | "label":"SEO标题" 83 | },{ 84 | "type":"text", 85 | "name":"meta_kewwords", 86 | "label":"SEO关键词" 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | . "github.com/coscms/forms" 10 | _ "github.com/coscms/forms/defaults" 11 | ) 12 | 13 | type Test struct { 14 | User string 15 | Birthday string 16 | } 17 | 18 | var expected = []string{ 19 | "user", 20 | "user1", 21 | "user2", 22 | "birthday", 23 | "Language[en][title]", 24 | "Language[zh-CN][title]", 25 | "Language[en][content]", 26 | "Language[zh-CN][content]", 27 | "Language[en][meta_title]", 28 | "Language[zh-CN][meta_title]", 29 | "Language[en][meta_kewwords]", 30 | "Language[zh-CN][meta_kewwords]", 31 | } 32 | 33 | func main() { 34 | //1.=================================== 35 | startTime := time.Now() 36 | config, err := UnmarshalFile(`forms.json`) 37 | if err != nil { 38 | log.Println(err) 39 | } 40 | t := Test{ 41 | User: `webx`, 42 | Birthday: `1985`, 43 | } 44 | b, _ := json.MarshalIndent(config.GetNames(), ``, " ") 45 | println(string(b)) 46 | for _, name := range expected { 47 | if !config.HasName(name) { 48 | panic(`not found "` + name + `"`) 49 | } 50 | } 51 | form := NewWithModelConfig(t, config) 52 | fmt.Println(form.Render()) 53 | //return 54 | fmt.Println(`1.________________________________________CostTime:`, time.Since(startTime).Seconds(), `s`) 55 | fmt.Println(``) 56 | 57 | //2.=================================== 58 | startTime = time.Now() 59 | form = New() 60 | fmt.Println(form.Init(config, t).ValidFromConfig().ParseFromConfig(true)) 61 | 62 | fmt.Println(`2.________________________________________CostTime:`, time.Since(startTime).Seconds(), `s`) 63 | fmt.Println(``) 64 | 65 | //3.=================================== 66 | startTime = time.Now() 67 | form = New() 68 | form.Generate(t, `forms.json`) 69 | fmt.Println(form) 70 | fmt.Println(`3.________________________________________CostTime:`, time.Since(startTime).Seconds(), `s`) 71 | 72 | //b, _ = json.MarshalIndent(form, ``, " ") 73 | //println(string(b)) 74 | } 75 | -------------------------------------------------------------------------------- /example/run.bat: -------------------------------------------------------------------------------- 1 | go run main.go 2 | pause -------------------------------------------------------------------------------- /fields/button.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package fields 20 | 21 | import ( 22 | "github.com/coscms/forms/common" 23 | ) 24 | 25 | // SubmitButton creates a default button with the provided name and text. 26 | func SubmitButton(name string, text string) *Field { 27 | ret := FieldWithType(name, common.SUBMIT) 28 | ret.SetText(text) 29 | return ret 30 | } 31 | 32 | // ResetButton creates a default reset button with the provided name and text. 33 | func ResetButton(name string, text string) *Field { 34 | ret := FieldWithType(name, common.RESET) 35 | ret.SetText(text) 36 | return ret 37 | } 38 | 39 | // Button creates a default generic button 40 | func Button(name string, text string) *Field { 41 | ret := FieldWithType(name, common.BUTTON) 42 | ret.SetText(text) 43 | return ret 44 | } 45 | -------------------------------------------------------------------------------- /fields/datetime.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package fields 20 | 21 | import ( 22 | "reflect" 23 | "time" 24 | 25 | "github.com/coscms/forms/common" 26 | ) 27 | 28 | // Datetime format string to convert from time.Time objects to HTML fields and viceversa. 29 | const ( 30 | DATETIME_FORMAT = "2006-01-02 15:05" 31 | DATE_FORMAT = "2006-01-02" 32 | TIME_FORMAT = "15:05" 33 | ) 34 | 35 | func ConvertTime(v interface{}) (time.Time, bool) { 36 | t, ok := v.(time.Time) 37 | var isEmpty bool 38 | if !ok { 39 | var timestamp int64 40 | switch i := v.(type) { 41 | case int: 42 | timestamp = int64(i) 43 | case int64: 44 | timestamp = i 45 | } 46 | if timestamp > 0 { 47 | t = time.Unix(timestamp, 0) 48 | } else { 49 | isEmpty = true 50 | } 51 | } 52 | return t, isEmpty 53 | } 54 | 55 | // DatetimeField creates a default datetime input field with the given name. 56 | func DatetimeField(name string) *Field { 57 | return FieldWithType(name, common.DATETIME) 58 | } 59 | 60 | // DateField creates a default date input field with the given name. 61 | func DateField(name string) *Field { 62 | return FieldWithType(name, common.DATE) 63 | } 64 | 65 | // TimeField creates a default time input field with the given name. 66 | func TimeField(name string) *Field { 67 | return FieldWithType(name, common.TIME) 68 | } 69 | 70 | // DatetimeFieldFromInstance creates and initializes a datetime field based on its name, the reference object instance and field number. 71 | // This method looks for "form_min", "form_max" and "form_value" tags to add additional parameters to the field. 72 | func DatetimeFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 73 | ret := DatetimeField(name) 74 | dateFormat := DATETIME_FORMAT 75 | if v := common.TagVal(t, fieldNo, "form_format"); len(v) > 0 { 76 | dateFormat = v 77 | } 78 | ret.Format = dateFormat 79 | // check tags 80 | if v := common.TagVal(t, fieldNo, "form_min"); len(v) > 0 { 81 | if !validateDateformat(v, dateFormat) { 82 | panic("Invalid date value (min) for field: " + name) 83 | } 84 | ret.SetParam("min", v) 85 | } 86 | if v := common.TagVal(t, fieldNo, "form_max"); len(v) > 0 { 87 | if !validateDateformat(v, dateFormat) { 88 | panic("Invalid date value (max) for field: " + name) 89 | } 90 | ret.SetParam("max", v) 91 | } 92 | 93 | if useFieldValue { 94 | if vt, isEmpty := ConvertTime(val.Field(fieldNo).Interface()); !vt.IsZero() { 95 | ret.SetValue(vt.Format(dateFormat)) 96 | } else if isEmpty { 97 | ret.SetValue(``) 98 | } 99 | } else if v := common.TagVal(t, fieldNo, "form_value"); len(v) > 0 { 100 | ret.SetValue(v) 101 | } 102 | return ret 103 | } 104 | 105 | // DateFieldFromInstance creates and initializes a date field based on its name, the reference object instance and field number. 106 | // This method looks for "form_min", "form_max" and "form_value" tags to add additional parameters to the field. 107 | func DateFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 108 | ret := DateField(name) 109 | dateFormat := DATE_FORMAT 110 | if v := common.TagVal(t, fieldNo, "form_format"); len(v) > 0 { 111 | dateFormat = v 112 | } 113 | ret.Format = dateFormat 114 | // check tags 115 | if v := common.TagVal(t, fieldNo, "form_min"); len(v) > 0 { 116 | if !validateDateformat(v, dateFormat) { 117 | panic("Invalid date value (min) for field: " + name) 118 | } 119 | ret.SetParam("min", v) 120 | } 121 | if v := common.TagVal(t, fieldNo, "form_max"); len(v) > 0 { 122 | if !validateDateformat(v, dateFormat) { 123 | panic("Invalid date value (max) for field: " + name) 124 | } 125 | ret.SetParam("max", v) 126 | } 127 | 128 | if useFieldValue { 129 | if vt, isEmpty := ConvertTime(val.Field(fieldNo).Interface()); !vt.IsZero() { 130 | ret.SetValue(vt.Format(dateFormat)) 131 | } else if isEmpty { 132 | ret.SetValue(``) 133 | } 134 | } else if v := common.TagVal(t, fieldNo, "form_value"); len(v) > 0 { 135 | ret.SetValue(v) 136 | } 137 | return ret 138 | } 139 | 140 | // TimeFieldFromInstance creates and initializes a time field based on its name, the reference object instance and field number. 141 | // This method looks for "form_min", "form_max" and "form_value" tags to add additional parameters to the field. 142 | func TimeFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 143 | ret := TimeField(name) 144 | dateFormat := TIME_FORMAT 145 | if v := common.TagVal(t, fieldNo, "form_format"); len(v) > 0 { 146 | dateFormat = v 147 | } 148 | ret.Format = dateFormat 149 | // check tags 150 | if v := common.TagVal(t, fieldNo, "form_min"); len(v) > 0 { 151 | if !validateDateformat(v, dateFormat) { 152 | panic("Invalid time value (min) for field: " + name) 153 | } 154 | ret.SetParam("min", v) 155 | } 156 | if v := common.TagVal(t, fieldNo, "form_max"); len(v) > 0 { 157 | if !validateDateformat(v, dateFormat) { 158 | panic("Invalid time value (max) for field: " + name) 159 | } 160 | ret.SetParam("max", v) 161 | } 162 | if useFieldValue { 163 | if v, isEmpty := ConvertTime(val.Field(fieldNo).Interface()); !v.IsZero() { 164 | ret.SetValue(v.Format(dateFormat)) 165 | } else if isEmpty { 166 | ret.SetValue(``) 167 | } 168 | } else if v := common.TagVal(t, fieldNo, "form_value"); len(v) > 0 { 169 | ret.SetValue(v) 170 | } 171 | return ret 172 | } 173 | 174 | func validateDateformat(v string, format string) bool { 175 | _, err := time.Parse(format, v) 176 | return err == nil 177 | } 178 | 179 | func validateDatetime(v string) bool { 180 | _, err := time.Parse(DATETIME_FORMAT, v) 181 | return err == nil 182 | } 183 | 184 | func validateDate(v string) bool { 185 | _, err := time.Parse(DATE_FORMAT, v) 186 | return err == nil 187 | } 188 | 189 | func validateTime(v string) bool { 190 | _, err := time.Parse(TIME_FORMAT, v) 191 | return err == nil 192 | } 193 | -------------------------------------------------------------------------------- /fields/field.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | // Package fields This package provides all the input fields logic and customization methods. 20 | package fields 21 | 22 | import ( 23 | "fmt" 24 | "html/template" 25 | "strconv" 26 | "strings" 27 | 28 | "github.com/coscms/forms/common" 29 | "github.com/coscms/forms/config" 30 | "github.com/coscms/forms/widgets" 31 | ) 32 | 33 | var _ FieldInterface = (*Field)(nil) 34 | var _ config.FormElement = (*Field)(nil) 35 | 36 | // Field is a generic type containing all data associated to an input field. 37 | type Field struct { 38 | Type string `json:"type" xml:"type"` 39 | Template string `json:"template" xml:"template"` 40 | CurrName string `json:"currName" xml:"currName"` 41 | OrigName string `json:"oriName" xml:"oriName"` 42 | Classes common.HTMLAttrValues `json:"classes" xml:"classes"` 43 | ID string `json:"id" xml:"id"` 44 | Params map[string]interface{} `json:"params" xml:"params"` 45 | CSS map[string]string `json:"css" xml:"css"` 46 | Label string `json:"label" xml:"label"` 47 | LabelCols int `json:"labelCols" xml:"labelCols"` 48 | FieldCols int `json:"fieldCols" xml:"fieldCols"` 49 | LabelClasses common.HTMLAttrValues `json:"labelClasses" xml:"labelClasses"` 50 | Tags common.HTMLAttrValues `json:"tags" xml:"tags"` 51 | Value string `json:"value" xml:"value"` 52 | Helptext string `json:"helpText" xml:"helpText"` 53 | Errors []string `json:"errors,omitempty" xml:"errors,omitempty"` 54 | Additional map[string]interface{} `json:"additional,omitempty" xml:"additional,omitempty"` 55 | Choices interface{} `json:"choices,omitempty" xml:"choices,omitempty"` 56 | ChoiceKeys map[string]ChoiceIndex `json:"choiceKeys,omitempty" xml:"choiceKeys,omitempty"` 57 | AppendData map[string]interface{} `json:"appendData,omitempty" xml:"appendData,omitempty"` 58 | Theme string `json:"theme" xml:"theme"` 59 | Format string `json:"format,omitempty" xml:"format,omitempty"` 60 | Language string `json:"language,omitempty" xml:"language,omitempty"` 61 | widget widgets.WidgetInterface // Public Widget field for widget customization 62 | data map[string]interface{} 63 | } 64 | 65 | // FieldWithType creates an empty field of the given type and identified by name. 66 | func FieldWithType(name, t string) *Field { 67 | return &Field{ 68 | Type: t, 69 | CurrName: name, 70 | OrigName: name, 71 | Classes: common.HTMLAttrValues{}, 72 | ID: "", 73 | Params: map[string]interface{}{}, 74 | CSS: map[string]string{}, 75 | Label: "", 76 | LabelClasses: common.HTMLAttrValues{}, 77 | Tags: common.HTMLAttrValues{}, 78 | Value: "", 79 | Helptext: "", 80 | Errors: []string{}, 81 | Additional: map[string]interface{}{}, 82 | Choices: nil, 83 | ChoiceKeys: map[string]ChoiceIndex{}, 84 | AppendData: map[string]interface{}{}, 85 | } 86 | } 87 | 88 | func (f *Field) Cols() int { 89 | return config.GetCols(f.LabelCols, f.FieldCols) 90 | } 91 | 92 | func (f *Field) SetTemplate(tmpl string, theme ...string) FieldInterface { 93 | f.Template = tmpl 94 | if len(f.Template) > 0 && f.widget != nil && f.Template != tmpl { 95 | var s string 96 | if len(theme) > 0 { 97 | s = theme[0] 98 | } else { 99 | s = f.Theme 100 | } 101 | f.widget = widgets.BaseWidget(s, f.Type, f.Template) 102 | } 103 | return f 104 | } 105 | 106 | func (f *Field) SetName(name string) { 107 | f.CurrName = name 108 | } 109 | 110 | func (f *Field) OriginalName() string { 111 | return f.OrigName 112 | } 113 | 114 | func (f *Field) Clone() config.FormElement { 115 | fc := *f 116 | return &fc 117 | } 118 | 119 | func (f *Field) SetLang(lang string) { 120 | f.Language = lang 121 | } 122 | 123 | func (f *Field) Lang() string { 124 | return f.Language 125 | } 126 | 127 | // SetTheme sets the theme (e.g.: BASE, BOOTSTRAP) of the field, correctly populating the Widget field. 128 | func (f *Field) SetTheme(theme string) FieldInterface { 129 | f.Theme = theme 130 | f.widget = widgets.BaseWidget(theme, f.Type, f.Template) 131 | return f 132 | } 133 | 134 | func (f *Field) SetData(key string, value interface{}) { 135 | f.AppendData[key] = value 136 | } 137 | 138 | func (f *Field) SetLabelCols(cols int) { 139 | f.LabelCols = cols 140 | } 141 | 142 | func (f *Field) SetFieldCols(cols int) { 143 | f.FieldCols = cols 144 | } 145 | 146 | // Name returns the name of the field. 147 | func (f *Field) Name() string { 148 | return strings.TrimSuffix(f.CurrName, "[]") 149 | } 150 | 151 | func (f *Field) Data() map[string]interface{} { 152 | if len(f.data) > 0 { 153 | return f.data 154 | } 155 | safeParams := make(common.HTMLAttributes) 156 | safeParams.FillFrom(f.Params) 157 | f.data = map[string]interface{}{ 158 | "classes": f.Classes, 159 | "id": f.ID, 160 | "name": f.CurrName, 161 | "params": safeParams, 162 | "css": f.CSS, 163 | "type": f.Type, 164 | "label": f.Label, 165 | "labelCols": f.LabelCols, 166 | "fieldCols": f.FieldCols, 167 | "labelClasses": f.LabelClasses, 168 | "tags": f.Tags, 169 | "value": f.Value, 170 | "helptext": f.Helptext, 171 | "errors": f.Errors, 172 | "container": "form", 173 | "choices": f.Choices, 174 | } 175 | for k, v := range f.Additional { 176 | f.data[k] = v 177 | } 178 | for k, v := range f.AppendData { 179 | f.data[k] = v 180 | } 181 | return f.data 182 | } 183 | 184 | // Render packs all data and executes widget render method. 185 | func (f *Field) Render() template.HTML { 186 | if f.widget != nil { 187 | return template.HTML(f.widget.Render(f.Data())) 188 | } 189 | return template.HTML("") 190 | } 191 | 192 | func (f *Field) String() string { 193 | if f.widget != nil { 194 | return f.widget.Render(f.Data()) 195 | } 196 | return "" 197 | } 198 | 199 | // AddClass adds a class to the field. 200 | func (f *Field) AddClass(class string) FieldInterface { 201 | f.Classes.Add(class) 202 | return f 203 | } 204 | 205 | // RemoveClass removes a class from the field, if it was present. 206 | func (f *Field) RemoveClass(class string) FieldInterface { 207 | f.Classes.Remove(class) 208 | return f 209 | } 210 | 211 | // SetID associates the given id to the field, overwriting any previous id. 212 | func (f *Field) SetID(id string) FieldInterface { 213 | f.ID = id 214 | return f 215 | } 216 | 217 | // SetLabel saves the label to be rendered along with the field. 218 | func (f *Field) SetLabel(label string) FieldInterface { 219 | f.Label = label 220 | return f 221 | } 222 | 223 | // AddLabelClass allows to define custom classes for the label. 224 | func (f *Field) AddLabelClass(class string) FieldInterface { 225 | f.LabelClasses.Add(class) 226 | return f 227 | } 228 | 229 | // RemoveLabelClass removes the given class from the field label. 230 | func (f *Field) RemoveLabelClass(class string) FieldInterface { 231 | f.LabelClasses.Remove(class) 232 | return f 233 | } 234 | 235 | // SetParam adds a parameter (defined as key-value pair) in the field. 236 | func (f *Field) SetParam(key string, value interface{}) FieldInterface { 237 | switch key { 238 | case `class`: 239 | f.AddClass(value.(string)) 240 | default: 241 | f.Params[key] = value 242 | } 243 | return f 244 | } 245 | 246 | // DeleteParam removes a parameter identified by key from the field. 247 | func (f *Field) DeleteParam(key string) FieldInterface { 248 | delete(f.Params, key) 249 | return f 250 | } 251 | 252 | // AddCSS adds a custom CSS style the field. 253 | func (f *Field) AddCSS(key, value string) FieldInterface { 254 | f.CSS[key] = value 255 | return f 256 | } 257 | 258 | // RemoveCSS removes CSS options identified by key from the field. 259 | func (f *Field) RemoveCSS(key string) FieldInterface { 260 | delete(f.CSS, key) 261 | return f 262 | } 263 | 264 | // Disabled add the "disabled" tag to the field, making it unresponsive in some environments (e.g. Bootstrap). 265 | func (f *Field) Disabled() FieldInterface { 266 | f.AddTag("disabled") 267 | return f 268 | } 269 | 270 | // Enabled removes the "disabled" tag from the field, making it responsive. 271 | func (f *Field) Enabled() FieldInterface { 272 | f.RemoveTag("disabled") 273 | return f 274 | } 275 | 276 | // AddTag adds a no-value parameter (e.g.: checked, disabled) to the field. 277 | func (f *Field) AddTag(tag string) FieldInterface { 278 | f.Tags.Add(tag) 279 | return f 280 | } 281 | 282 | // RemoveTag removes a no-value parameter from the field. 283 | func (f *Field) RemoveTag(tag string) FieldInterface { 284 | f.Tags.Remove(tag) 285 | return f 286 | } 287 | 288 | // SetValue sets the value parameter for the field. 289 | func (f *Field) SetValue(value string) FieldInterface { 290 | f.Value = value 291 | f.SetSelected(f.Value) 292 | return f 293 | } 294 | 295 | // SetHelptext saves the field helptext. 296 | func (f *Field) SetHelptext(text string) FieldInterface { 297 | f.Helptext = text 298 | return f 299 | } 300 | 301 | // AddError adds an error string to the field. It's valid only for Bootstrap forms. 302 | func (f *Field) AddError(err string) FieldInterface { 303 | f.Errors = append(f.Errors, err) 304 | return f 305 | } 306 | 307 | // MultipleChoice configures the SelectField to accept and display multiple choices. 308 | // It has no effect if type is not SELECT. 309 | func (f *Field) MultipleChoice() FieldInterface { 310 | switch f.Type { 311 | case common.SELECT: 312 | f.AddTag("multiple") 313 | fallthrough 314 | case common.CHECKBOX: 315 | // fix name if necessary 316 | if !strings.HasSuffix(f.CurrName, "[]") { 317 | f.CurrName = f.CurrName + "[]" 318 | } 319 | } 320 | return f 321 | } 322 | 323 | // SingleChoice configures the Field to accept and display only one choice (valid for SelectFields only). 324 | // It has no effect if type is not SELECT. 325 | func (f *Field) SingleChoice() FieldInterface { 326 | switch f.Type { 327 | case common.SELECT: 328 | f.RemoveTag("multiple") 329 | fallthrough 330 | case common.CHECKBOX: 331 | if strings.HasSuffix(f.CurrName, "[]") { 332 | f.CurrName = strings.TrimSuffix(f.CurrName, "[]") 333 | } 334 | } 335 | return f 336 | } 337 | 338 | // AddSelected If the field is configured as "multiple", AddSelected adds a selected value to the field (valid for SelectFields only). 339 | // It has no effect if type is not SELECT. 340 | func (f *Field) AddSelected(opt ...string) FieldInterface { 341 | switch f.Type { 342 | case common.SELECT: 343 | for _, v := range opt { 344 | i, ok := f.ChoiceKeys[v] 345 | if !ok { 346 | continue 347 | } 348 | choice := f.Choices.(map[string][]InputChoice) 349 | if vc, ok := choice[i.Group]; ok { 350 | if len(vc) > i.Index { 351 | choice[i.Group][i.Index].Checked = true 352 | } 353 | } 354 | } 355 | case common.RADIO, common.CHECKBOX: 356 | choice := f.Choices.([]InputChoice) 357 | size := len(choice) 358 | for _, v := range opt { 359 | i, ok := f.ChoiceKeys[v] 360 | if !ok { 361 | continue 362 | } 363 | if size > i.Index { 364 | choice[i.Index].Checked = true 365 | } 366 | } 367 | } 368 | return f 369 | } 370 | 371 | func (f *Field) SetSelected(opt ...string) FieldInterface { 372 | switch f.Type { 373 | case common.SELECT: 374 | choice := f.Choices.(map[string][]InputChoice) 375 | for key, i := range f.ChoiceKeys { 376 | vc, ok := choice[i.Group] 377 | if !ok || len(vc) <= i.Index { 378 | continue 379 | } 380 | checked := false 381 | for _, v := range opt { 382 | if key == v { 383 | checked = true 384 | break 385 | } 386 | } 387 | choice[i.Group][i.Index].Checked = checked 388 | } 389 | case common.RADIO, common.CHECKBOX: 390 | choice := f.Choices.([]InputChoice) 391 | size := len(choice) 392 | for key, i := range f.ChoiceKeys { 393 | if size <= i.Index { 394 | continue 395 | } 396 | checked := false 397 | for _, v := range opt { 398 | if key == v { 399 | checked = true 400 | break 401 | } 402 | } 403 | choice[i.Index].Checked = checked 404 | } 405 | } 406 | return f 407 | } 408 | 409 | // RemoveSelected If the field is configured as "multiple", AddSelected removes the selected value from the field (valid for SelectFields only). 410 | // It has no effect if type is not SELECT. 411 | func (f *Field) RemoveSelected(opt string) FieldInterface { 412 | switch f.Type { 413 | case common.SELECT: 414 | i := f.ChoiceKeys[opt] 415 | if vc, ok := f.Choices.(map[string][]InputChoice)[i.Group]; ok { 416 | if len(vc) > i.Index { 417 | f.Choices.(map[string][]InputChoice)[i.Group][i.Index].Checked = false 418 | } 419 | } 420 | 421 | case common.RADIO, common.CHECKBOX: 422 | size := len(f.Choices.([]InputChoice)) 423 | i := f.ChoiceKeys[opt] 424 | if size > i.Index { 425 | f.Choices.([]InputChoice)[i.Index].Checked = false 426 | } 427 | } 428 | return f 429 | } 430 | 431 | func (f *Field) AddChoice(key, value interface{}, checked ...bool) FieldInterface { 432 | var _checked bool 433 | if len(checked) > 0 && checked[0] { 434 | _checked = true 435 | } 436 | switch f.Type { 437 | case common.SELECT: 438 | if f.Choices == nil { 439 | f.Choices = map[string][]InputChoice{ 440 | "": []InputChoice{ 441 | { 442 | ID: fmt.Sprint(key), 443 | Val: fmt.Sprint(value), 444 | Checked: _checked, 445 | }, 446 | }, 447 | } 448 | } else { 449 | v, _ := f.Choices.(map[string][]InputChoice) 450 | v[""] = append(v[""], InputChoice{ 451 | ID: fmt.Sprint(key), 452 | Val: fmt.Sprint(value), 453 | Checked: _checked, 454 | }) 455 | f.Choices = v 456 | } 457 | 458 | case common.RADIO, common.CHECKBOX: 459 | if f.Choices == nil { 460 | f.Choices = []InputChoice{ 461 | { 462 | ID: fmt.Sprint(key), 463 | Val: fmt.Sprint(value), 464 | Checked: _checked, 465 | }, 466 | } 467 | } else { 468 | v, _ := f.Choices.([]InputChoice) 469 | v = append(v, InputChoice{ 470 | ID: fmt.Sprint(key), 471 | Val: fmt.Sprint(value), 472 | Checked: _checked, 473 | }) 474 | f.Choices = v 475 | } 476 | } 477 | return f 478 | } 479 | 480 | // SetChoices takes as input a dictionary whose key-value entries are defined as follows: key is the group name (the empty string 481 | // is the default group that is not explicitly rendered) and value is the list of choices belonging to that group. 482 | // Grouping is only useful for Select fields, while groups are ignored in Radio fields. 483 | // It has no effect if type is not SELECT. 484 | func (f *Field) SetChoices(choices interface{}, saveIndex ...bool) FieldInterface { 485 | if choices == nil { 486 | return f 487 | } 488 | switch f.Type { 489 | case common.SELECT: 490 | var ch map[string][]InputChoice 491 | if c, ok := choices.(map[string][]InputChoice); ok { 492 | ch = c 493 | } else { 494 | c, y := choices.([]InputChoice) 495 | if !y { 496 | if v, y := choices.([]string); y { 497 | c = []InputChoice{ 498 | InputChoice{}, 499 | } 500 | switch len(v) { 501 | case 3: 502 | c[0].Checked, _ = strconv.ParseBool(v[2]) 503 | fallthrough 504 | case 2: 505 | c[0].Val = v[1] 506 | fallthrough 507 | case 1: 508 | c[0].ID = v[0] 509 | } 510 | } 511 | } 512 | ch = map[string][]InputChoice{"": c} 513 | } 514 | f.Choices = ch 515 | if len(saveIndex) < 1 || saveIndex[0] { 516 | for k, v := range ch { 517 | for idx, ipt := range v { 518 | f.ChoiceKeys[ipt.ID] = ChoiceIndex{Group: k, Index: idx} 519 | } 520 | } 521 | } 522 | 523 | case common.RADIO, common.CHECKBOX: 524 | c, y := choices.([]InputChoice) 525 | if !y { 526 | if v, y := choices.([]string); y { 527 | c = []InputChoice{ 528 | InputChoice{}, 529 | } 530 | switch len(v) { 531 | case 3: 532 | c[0].Checked, _ = strconv.ParseBool(v[2]) 533 | fallthrough 534 | case 2: 535 | c[0].Val = v[1] 536 | fallthrough 537 | case 1: 538 | c[0].ID = v[0] 539 | } 540 | } 541 | } 542 | f.Choices = c 543 | if len(saveIndex) < 1 || saveIndex[0] { 544 | for idx, ipt := range c { 545 | f.ChoiceKeys[ipt.ID] = ChoiceIndex{Group: "", Index: idx} 546 | } 547 | } 548 | } 549 | return f 550 | } 551 | 552 | // SetText saves the provided text as content of the field, usually a TextAreaField. 553 | func (f *Field) SetText(text string) FieldInterface { 554 | if f.Type == common.BUTTON || 555 | f.Type == common.SUBMIT || 556 | f.Type == common.RESET || 557 | f.Type == common.STATIC || 558 | f.Type == common.TEXTAREA { 559 | f.Additional["text"] = text 560 | } 561 | return f 562 | } 563 | 564 | func (f *Field) Element() *config.Element { 565 | elem := &config.Element{ 566 | ID: f.ID, 567 | Type: f.Type, 568 | Name: f.CurrName, 569 | Label: f.Label, 570 | LabelCols: f.LabelCols, 571 | FieldCols: f.FieldCols, 572 | Value: f.Value, 573 | HelpText: f.Helptext, 574 | Template: f.Template, 575 | Valid: ``, 576 | Attributes: make([][]string, 0), 577 | Choices: make([]*config.Choice, 0), 578 | Elements: make([]*config.Element, 0), 579 | Format: f.Format, 580 | } 581 | if f.AppendData != nil && len(f.AppendData) > 0 { 582 | elem.Data = f.AppendData 583 | } 584 | var ( 585 | temp string 586 | join string 587 | ) 588 | for _, c := range f.Classes { 589 | temp += join + c 590 | join = ` ` 591 | } 592 | if len(temp) > 0 { 593 | elem.Attributes = append(elem.Attributes, []string{`class`, temp}) 594 | temp = `` 595 | join = `` 596 | } 597 | for _, c := range f.Tags { 598 | elem.Attributes = append(elem.Attributes, []string{c}) 599 | } 600 | for c, v := range f.Params { 601 | elem.Attributes = append(elem.Attributes, []string{c, fmt.Sprintf(`%v`, v)}) 602 | } 603 | for _, c := range f.CSS { 604 | temp += join + c 605 | join = `;` 606 | } 607 | if len(temp) > 0 { 608 | elem.Attributes = append(elem.Attributes, []string{`style`, temp}) 609 | temp = `` 610 | join = `` 611 | } 612 | switch choices := f.Choices.(type) { 613 | case map[string][]InputChoice: 614 | for k, items := range choices { 615 | for _, v := range items { 616 | elem.Choices = append(elem.Choices, &config.Choice{ 617 | Group: k, 618 | Option: []string{v.ID, v.Val}, 619 | Checked: v.Checked, 620 | }) 621 | } 622 | } 623 | case []InputChoice: 624 | for _, v := range choices { 625 | elem.Choices = append(elem.Choices, &config.Choice{ 626 | Group: ``, 627 | Option: []string{v.ID, v.Val}, 628 | Checked: v.Checked, 629 | }) 630 | } 631 | } 632 | return elem 633 | } 634 | 635 | func (f *Field) HasError() bool { 636 | return len(f.Errors) > 0 637 | } 638 | -------------------------------------------------------------------------------- /fields/field_interface.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | import ( 4 | "html/template" 5 | 6 | "github.com/coscms/forms/config" 7 | ) 8 | 9 | // FieldInterface defines the interface an object must implement to be used in a form. Every method returns a FieldInterface object 10 | // to allow methods chaining. 11 | type FieldInterface interface { 12 | Name() string 13 | OriginalName() string 14 | Cols() int 15 | SetName(string) 16 | SetLabelCols(cols int) 17 | SetFieldCols(cols int) 18 | Render() template.HTML 19 | AddClass(class string) FieldInterface 20 | RemoveClass(class string) FieldInterface 21 | AddTag(class string) FieldInterface 22 | RemoveTag(class string) FieldInterface 23 | SetID(id string) FieldInterface 24 | SetParam(key string, value interface{}) FieldInterface 25 | DeleteParam(key string) FieldInterface 26 | AddCSS(key, value string) FieldInterface 27 | RemoveCSS(key string) FieldInterface 28 | SetTheme(theme string) FieldInterface 29 | SetLabel(label string) FieldInterface 30 | AddLabelClass(class string) FieldInterface 31 | RemoveLabelClass(class string) FieldInterface 32 | SetValue(value string) FieldInterface 33 | Disabled() FieldInterface 34 | Enabled() FieldInterface 35 | SetTemplate(tmpl string, theme ...string) FieldInterface 36 | SetHelptext(text string) FieldInterface 37 | AddError(err string) FieldInterface 38 | MultipleChoice() FieldInterface 39 | SingleChoice() FieldInterface 40 | AddSelected(opt ...string) FieldInterface 41 | SetSelected(opt ...string) FieldInterface 42 | RemoveSelected(opt string) FieldInterface 43 | AddChoice(key, value interface{}, checked ...bool) FieldInterface 44 | SetChoices(choices interface{}, saveIndex ...bool) FieldInterface 45 | SetText(text string) FieldInterface 46 | SetData(key string, value interface{}) 47 | Data() map[string]interface{} 48 | String() string 49 | SetLang(lang string) 50 | Lang() string 51 | Clone() config.FormElement 52 | 53 | Element() *config.Element 54 | } 55 | -------------------------------------------------------------------------------- /fields/field_test.go: -------------------------------------------------------------------------------- 1 | package fields 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/coscms/forms/common" 8 | _ "github.com/coscms/forms/defaults" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestTextField(t *testing.T) { 13 | f := FieldWithType(`title`, common.TEXT) 14 | f.SetTheme(`base`) 15 | f.AddClass(`form-control`).AddClass(`row`) 16 | 17 | assert.Equal(t, common.HTMLAttrValues([]string{`form-control`, `row`}), f.Classes) 18 | assert.Equal(t, ``, strings.TrimSpace(f.String())) 19 | f.data = nil 20 | 21 | f.RemoveClass(`row`) 22 | assert.Equal(t, common.HTMLAttrValues([]string{`form-control`}), f.Classes) 23 | assert.Equal(t, ``, strings.TrimSpace(f.String())) 24 | f.data = nil 25 | 26 | f.RemoveClass(`form-control`) 27 | assert.Equal(t, common.HTMLAttrValues([]string{}), f.Classes) 28 | 29 | assert.Equal(t, ``, strings.TrimSpace(f.String())) 30 | } 31 | 32 | func TestCheckboxField(t *testing.T) { 33 | f := FieldWithType(`title`, common.CHECKBOX) 34 | f.SetTheme(`base`) 35 | f.AddChoice(`value1`, `text1`) 36 | f.AddChoice(`value2`, `text2`, true) 37 | f.AddChoice(`value3`, `text3`) 38 | f.AddChoice(`value4`, `text4`) 39 | 40 | assert.Equal(t, "\n\n\n\n\n\n\n\n", f.String()) 41 | } 42 | 43 | func TestSelectField(t *testing.T) { 44 | f := FieldWithType(`title`, common.SELECT) 45 | f.SetTheme(`base`) 46 | f.AddChoice(`value1`, `text1`) 47 | f.AddChoice(`value2`, `text2`, true) 48 | f.AddChoice(`value3`, `text3`) 49 | f.AddChoice(`value4`, `text4`) 50 | 51 | assert.Equal(t, "\n", f.String()) 52 | } 53 | -------------------------------------------------------------------------------- /fields/number.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package fields 20 | 21 | import ( 22 | "fmt" 23 | "reflect" 24 | 25 | "github.com/coscms/forms/common" 26 | ) 27 | 28 | // RangeField creates a default range field with the provided name. Min, max and step parameters define the expected behavior 29 | // of the HTML field. 30 | func RangeField(name string, min, max, step int) *Field { 31 | ret := FieldWithType(name, common.RANGE) 32 | ret.SetParam("min", fmt.Sprintf("%d", min)) 33 | ret.SetParam("max", fmt.Sprintf("%d", max)) 34 | ret.SetParam("step", fmt.Sprintf("%d", step)) 35 | return ret 36 | } 37 | 38 | // NumberField craetes a default number field with the provided name. 39 | func NumberField(name string) *Field { 40 | ret := FieldWithType(name, common.NUMBER) 41 | return ret 42 | } 43 | 44 | // NumberFieldFromInstance creates and initializes a number field based on its name, the reference object instance and field number. 45 | // This method looks for "form_min", "form_max" and "form_value" tags to add additional parameters to the field. 46 | func NumberFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 47 | ret := NumberField(name) 48 | // check tags 49 | if v := common.TagVal(t, fieldNo, "form_min"); v != "" { 50 | ret.SetParam("min", v) 51 | } 52 | if v := common.TagVal(t, fieldNo, "form_max"); v != "" { 53 | ret.SetParam("max", v) 54 | } 55 | if v := common.TagVal(t, fieldNo, "form_step"); v != "" { 56 | ret.SetParam("step", v) 57 | } 58 | ret.SetValue(defaultValue(val, t, fieldNo, useFieldValue)) 59 | return ret 60 | } 61 | 62 | // RangeFieldFromInstance creates and initializes a range field based on its name, the reference object instance and field number. 63 | // This method looks for "form_min", "form_max", "form_step" and "form_value" tags to add additional parameters to the field. 64 | func RangeFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 65 | ret := RangeField(name, 0, 10, 1) 66 | // check tags 67 | if v := common.TagVal(t, fieldNo, "form_min"); v != "" { 68 | ret.SetParam("min", v) 69 | } 70 | if v := common.TagVal(t, fieldNo, "form_max"); v != "" { 71 | ret.SetParam("max", v) 72 | } 73 | if v := common.TagVal(t, fieldNo, "form_step"); v != "" { 74 | ret.SetParam("step", v) 75 | } 76 | ret.SetValue(defaultValue(val, t, fieldNo, useFieldValue)) 77 | return ret 78 | } 79 | -------------------------------------------------------------------------------- /fields/options.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package fields 20 | 21 | import ( 22 | "fmt" 23 | "reflect" 24 | "strings" 25 | 26 | "github.com/coscms/forms/common" 27 | ) 28 | 29 | // InputChoice ID - Value pair used to define an option for select and redio input fields. 30 | type InputChoice struct { 31 | ID, Val string 32 | Checked bool 33 | } 34 | 35 | type ChoiceIndex struct { 36 | Group string 37 | Index int 38 | } 39 | 40 | func defaultValue(val reflect.Value, t reflect.Type, fieldNo int, useFieldValue bool) string { 41 | field := val.Field(fieldNo) 42 | var v string 43 | if useFieldValue { 44 | v = fmt.Sprintf("%v", field.Interface()) 45 | } else { 46 | v = common.TagVal(t, fieldNo, "form_value") 47 | } 48 | 49 | return v 50 | } 51 | 52 | // =============== RADIO 53 | 54 | // RadioField creates a default radio button input field with the provided name and list of choices. 55 | func RadioField(name string, choices []InputChoice) *Field { 56 | ret := FieldWithType(name, common.RADIO) 57 | ret.Choices = []InputChoice{} 58 | ret.SetChoices(choices) 59 | return ret 60 | } 61 | 62 | // RadioFieldFromInstance creates and initializes a radio field based on its name, the reference object instance and field number. 63 | // This method looks for "form_choices" and "form_value" tags to add additional parameters to the field. "form_choices" tag is a list 64 | // of | options, joined by "|" character; ex: "A|Option A|B|Option B" translates into 2 options: and . 65 | func RadioFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool, args ...func(string) string) *Field { 66 | fn := common.LabelFn 67 | if len(args) > 0 { 68 | fn = args[0] 69 | } 70 | choices := strings.Split(common.TagVal(t, fieldNo, "form_choices"), "|") 71 | chArr := make([]InputChoice, 0) 72 | ret := RadioField(name, chArr) 73 | chMap := make(map[string]string) 74 | for i := 0; i < len(choices)-1; i += 2 { 75 | ret.ChoiceKeys[choices[i]] = ChoiceIndex{Group: "", Index: len(chArr)} 76 | chArr = append(chArr, InputChoice{choices[i], fn(choices[i+1]), false}) 77 | chMap[choices[i]] = choices[i+1] 78 | } 79 | ret.SetChoices(chArr, false) 80 | v := defaultValue(val, t, fieldNo, useFieldValue) 81 | if _, ok := chMap[v]; ok { 82 | ret.SetValue(v) 83 | } 84 | return ret 85 | } 86 | 87 | // ================ SELECT 88 | 89 | // SelectField creates a default select input field with the provided name and map of choices. Choices for SelectField are grouped 90 | // by name (if is needed); "" group is the default one and does not trigger a rendering. 91 | func SelectField(name string, choices map[string][]InputChoice) *Field { 92 | ret := FieldWithType(name, common.SELECT) 93 | ret.Choices = map[string][]InputChoice{} 94 | ret.SetChoices(choices) 95 | return ret 96 | } 97 | 98 | // SelectFieldFromInstance creates and initializes a select field based on its name, the reference object instance and field number. 99 | // This method looks for "form_choices" and "form_value" tags to add additional parameters to the field. "form_choices" tag is a list 100 | // of | options, joined by "|" character; ex: "G1|A|Option A|G1|B|Option B" translates into 2 options in the same group G1: 101 | // and . "" group is the default one. 102 | func SelectFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool, options map[string]struct{}, args ...func(string) string) *Field { 103 | fn := common.LabelFn 104 | if len(args) > 0 { 105 | fn = args[0] 106 | } 107 | choices := strings.Split(common.TagVal(t, fieldNo, "form_choices"), "|") 108 | chArr := make(map[string][]InputChoice) 109 | ret := SelectField(name, chArr) 110 | chMap := make(map[string]string) 111 | for i := 0; i < len(choices)-2; i += 3 { 112 | optgroupLabel := fn(choices[i]) 113 | if _, ok := chArr[optgroupLabel]; !ok { 114 | chArr[optgroupLabel] = make([]InputChoice, 0) 115 | } 116 | id := choices[i+1] 117 | ret.ChoiceKeys[id] = ChoiceIndex{Group: optgroupLabel, Index: len(chArr[optgroupLabel])} 118 | chArr[optgroupLabel] = append(chArr[optgroupLabel], InputChoice{id, fn(choices[i+2]), false}) 119 | chMap[id] = choices[i+2] 120 | } 121 | ret.SetChoices(chArr, false) 122 | if _, ok := options["multiple"]; ok { 123 | ret.MultipleChoice() 124 | } 125 | v := defaultValue(val, t, fieldNo, useFieldValue) 126 | if _, ok := options["forceSetValue"]; ok { 127 | ret.SetValue(v) 128 | } else if _, ok := chMap[v]; ok { 129 | ret.SetValue(v) 130 | } 131 | return ret 132 | } 133 | 134 | // ================== CHECKBOX 135 | 136 | func CheckboxField(name string, choices []InputChoice) *Field { 137 | ret := FieldWithType(name, common.CHECKBOX) 138 | ret.Choices = []InputChoice{} 139 | ret.SetChoices(choices) 140 | if len(ret.Choices.([]InputChoice)) > 1 { 141 | ret.MultipleChoice() 142 | } 143 | return ret 144 | } 145 | 146 | func CheckboxFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool, args ...func(string) string) *Field { 147 | fn := common.LabelFn 148 | if len(args) > 0 { 149 | fn = args[0] 150 | } 151 | choices := strings.Split(common.TagVal(t, fieldNo, "form_choices"), "|") 152 | chArr := make([]InputChoice, 0) 153 | ret := CheckboxField(name, chArr) 154 | chMap := make(map[string]string) 155 | for i := 0; i < len(choices)-1; i += 2 { 156 | ret.ChoiceKeys[choices[i]] = ChoiceIndex{Group: "", Index: len(chArr)} 157 | chArr = append(chArr, InputChoice{choices[i], fn(choices[i+1]), false}) 158 | chMap[choices[i]] = choices[i+1] 159 | } 160 | ret.SetChoices(choices, false) 161 | if len(ret.Choices.([]InputChoice)) > 1 { 162 | ret.MultipleChoice() 163 | } 164 | v := defaultValue(val, t, fieldNo, useFieldValue) 165 | if _, ok := chMap[v]; ok { 166 | ret.SetValue(v) 167 | } 168 | return ret 169 | } 170 | 171 | // Checkbox creates a default checkbox field with the provided name. It also makes it checked by default based 172 | // on the checked parameter. 173 | func Checkbox(name string, checked bool) *Field { 174 | ret := FieldWithType(name, common.CHECKBOX) 175 | if checked { 176 | ret.AddTag("checked") 177 | } 178 | return ret 179 | } 180 | 181 | // CheckboxFromInstance creates and initializes a checkbox field based on its name, the reference object instance, field number and field options. 182 | func CheckboxFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool, options map[string]struct{}) *Field { 183 | ret := FieldWithType(name, common.CHECKBOX) 184 | ret.SetValue("true") 185 | checked := false 186 | if _, ok := options["checked"]; ok { 187 | checked = true 188 | } else { 189 | if useFieldValue { 190 | checked = val.Field(fieldNo).Bool() 191 | } 192 | } 193 | if checked { 194 | ret.AddTag("checked") 195 | } 196 | ret.Choices = []InputChoice{} 197 | ret.SetChoices(InputChoice{`true`, ``, checked}) 198 | return ret 199 | } 200 | -------------------------------------------------------------------------------- /fields/static.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package fields 20 | 21 | import ( 22 | "fmt" 23 | "reflect" 24 | 25 | "github.com/coscms/forms/common" 26 | ) 27 | 28 | // StaticField returns a static field with the provided name and content 29 | func StaticField(name, content string) *Field { 30 | ret := FieldWithType(name, common.STATIC) 31 | ret.SetText(content) 32 | return ret 33 | } 34 | 35 | // StaticFieldFromInstance creates and initializes a radio field based on its name, the reference object instance and field number. 36 | func StaticFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 37 | var ret *Field 38 | if useFieldValue { 39 | ret = StaticField(name, fmt.Sprintf("%s", val.Field(fieldNo).Interface())) 40 | } else { 41 | ret = StaticField(name, common.TagVal(t, fieldNo, "form_value")) 42 | } 43 | return ret 44 | } 45 | -------------------------------------------------------------------------------- /fields/text.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package fields 20 | 21 | import ( 22 | "fmt" 23 | "reflect" 24 | "strconv" 25 | 26 | "github.com/coscms/forms/common" 27 | ) 28 | 29 | func ColorField(name string) *Field { 30 | return FieldWithType(name, common.COLOR) 31 | } 32 | 33 | func EmailField(name string) *Field { 34 | return FieldWithType(name, common.EMAIL) 35 | } 36 | 37 | func FileField(name string) *Field { 38 | return FieldWithType(name, common.FILE) 39 | } 40 | 41 | func ImageField(name string) *Field { 42 | return FieldWithType(name, common.IMAGE) 43 | } 44 | 45 | func MonthField(name string) *Field { 46 | return FieldWithType(name, common.MONTH) 47 | } 48 | 49 | func SearchField(name string) *Field { 50 | return FieldWithType(name, common.SEARCH) 51 | } 52 | 53 | func TelField(name string) *Field { 54 | return FieldWithType(name, common.TEL) 55 | } 56 | 57 | func UrlField(name string) *Field { 58 | return FieldWithType(name, common.URL) 59 | } 60 | 61 | func WeekField(name string) *Field { 62 | return FieldWithType(name, common.WEEK) 63 | } 64 | 65 | // TextField creates a default text input field based on the provided name. 66 | func TextField(name string, typ ...string) *Field { 67 | var t = common.TEXT 68 | if len(typ) > 0 { 69 | t = typ[0] 70 | } 71 | return FieldWithType(name, t) 72 | } 73 | 74 | // PasswordField creates a default password text input field based on the provided name. 75 | func PasswordField(name string) *Field { 76 | return FieldWithType(name, common.PASSWORD) 77 | } 78 | 79 | // =========== TEXT AREA 80 | 81 | // TextAreaField creates a default textarea input field based on the provided name and dimensions. 82 | func TextAreaField(name string, rows, cols int) *Field { 83 | ret := FieldWithType(name, common.TEXTAREA) 84 | ret.SetParam("rows", fmt.Sprintf("%d", rows)) 85 | ret.SetParam("cols", fmt.Sprintf("%d", cols)) 86 | return ret 87 | } 88 | 89 | // ======================== 90 | 91 | // HiddenField creates a default hidden input field based on the provided name. 92 | func HiddenField(name string) *Field { 93 | return FieldWithType(name, common.HIDDEN) 94 | } 95 | 96 | // TextFieldFromInstance creates and initializes a text field based on its name, the reference object instance and field number. 97 | func TextFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool, typ ...string) *Field { 98 | ret := TextField(name, typ...) 99 | if useFieldValue { 100 | if dateFormat := common.TagVal(t, fieldNo, "form_format"); len(dateFormat) > 0 { 101 | if vt, isEmpty := ConvertTime(val.Field(fieldNo).Interface()); !vt.IsZero() { 102 | ret.SetValue(vt.Format(dateFormat)) 103 | } else if isEmpty { 104 | ret.SetValue(``) 105 | } 106 | } else { 107 | ret.SetValue(fmt.Sprintf("%v", val.Field(fieldNo).Interface())) 108 | } 109 | } else if v := common.TagVal(t, fieldNo, "form_value"); len(v) > 0 { 110 | ret.SetValue(v) 111 | } 112 | return ret 113 | } 114 | 115 | // PasswordFieldFromInstance creates and initializes a password field based on its name, the reference object instance and field number. 116 | func PasswordFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 117 | ret := PasswordField(name) 118 | if useFieldValue { 119 | ret.SetValue(fmt.Sprintf("%s", val.Field(fieldNo).String())) 120 | } else if v := common.TagVal(t, fieldNo, "form_value"); len(v) > 0 { 121 | ret.SetValue(v) 122 | } 123 | return ret 124 | } 125 | 126 | // TextFieldFromInstance creates and initializes a text field based on its name, the reference object instance and field number. 127 | // This method looks for "form_rows" and "form_cols" tags to add additional parameters to the field. 128 | func TextAreaFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 129 | var rows, cols int = 20, 50 130 | var err error 131 | if v := common.TagVal(t, fieldNo, "form_rows"); len(v) > 0 { 132 | rows, err = strconv.Atoi(v) 133 | if err != nil { 134 | return nil 135 | } 136 | } 137 | if v := common.TagVal(t, fieldNo, "form_cols"); len(v) > 0 { 138 | cols, err = strconv.Atoi(v) 139 | if err != nil { 140 | return nil 141 | } 142 | } 143 | ret := TextAreaField(name, rows, cols) 144 | if useFieldValue { 145 | ret.SetText(fmt.Sprintf("%s", val.Field(fieldNo).String())) 146 | } else if v := common.TagVal(t, fieldNo, "form_value"); len(v) > 0 { 147 | ret.SetText(v) 148 | } 149 | return ret 150 | } 151 | 152 | // HiddenFieldFromInstance creates and initializes a hidden field based on its name, the reference object instance and field number. 153 | func HiddenFieldFromInstance(val reflect.Value, t reflect.Type, fieldNo int, name string, useFieldValue bool) *Field { 154 | ret := HiddenField(name) 155 | if useFieldValue { 156 | ret.SetValue(fmt.Sprintf("%v", val.Field(fieldNo).Interface())) 157 | } else if v := common.TagVal(t, fieldNo, "form_value"); len(v) > 0 { 158 | ret.SetValue(v) 159 | } 160 | return ret 161 | } 162 | -------------------------------------------------------------------------------- /fieldset.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package forms 20 | 21 | import ( 22 | "bytes" 23 | "html/template" 24 | "strconv" 25 | "strings" 26 | 27 | "github.com/coscms/forms/common" 28 | "github.com/coscms/forms/config" 29 | "github.com/coscms/forms/fields" 30 | ) 31 | 32 | // FieldSetType is a collection of fields grouped within a form. 33 | type FieldSetType struct { 34 | OrigName string `json:"origName" xml:"origName"` 35 | CurrName string `json:"currName" xml:"currName"` 36 | Label string `json:"label" xml:"label"` 37 | LabelCols int `json:"labelCols" xml:"labelCols"` 38 | FieldCols int `json:"fieldCols" xml:"fieldCols"` 39 | Classes common.HTMLAttrValues `json:"classes" xml:"classes"` 40 | Tags common.HTMLAttrValues `json:"tags" xml:"tags"` 41 | Helptext string `json:"helpText" xml:"helpText"` 42 | FieldList []config.FormElement `json:"fieldList" xml:"fieldList"` 43 | AppendData map[string]interface{} `json:"appendData,omitempty" xml:"appendData,omitempty"` 44 | FormTheme string `json:"formTheme" xml:"formTheme"` 45 | Language string `json:"language,omitempty" xml:"language,omitempty"` 46 | Template string `json:"template" xml:"template"` 47 | fieldMap map[string]int 48 | containerMap map[string]string 49 | data map[string]interface{} 50 | } 51 | 52 | func (f *FieldSetType) Cols() int { 53 | return config.GetCols(f.LabelCols, f.FieldCols) 54 | } 55 | 56 | func (f *FieldSetType) SetData(key string, value interface{}) { 57 | f.AppendData[key] = value 58 | } 59 | 60 | // SetHelptext saves the field helptext. 61 | func (f *FieldSetType) SetHelptext(text string) *FieldSetType { 62 | f.Helptext = text 63 | return f 64 | } 65 | 66 | func (f *FieldSetType) SetLabelCols(cols int) { 67 | f.LabelCols = cols 68 | } 69 | 70 | func (f *FieldSetType) SetFieldCols(cols int) { 71 | f.FieldCols = cols 72 | } 73 | 74 | func (f *FieldSetType) SetName(name string) { 75 | f.CurrName = name 76 | } 77 | 78 | func (f *FieldSetType) OriginalName() string { 79 | return f.OrigName 80 | } 81 | 82 | func (f *FieldSetType) Data() map[string]interface{} { 83 | if len(f.data) > 0 { 84 | return f.data 85 | } 86 | f.data = map[string]interface{}{ 87 | "container": "fieldset", 88 | "name": f.CurrName, 89 | "label": f.Label, 90 | "labelCols": f.LabelCols, 91 | "fieldCols": f.FieldCols, 92 | "fields": f.FieldList, 93 | "groups": config.SplitGroup(f.FieldList), 94 | "classes": f.Classes, 95 | "tags": f.Tags, 96 | "helptext": f.Helptext, 97 | } 98 | for k, v := range f.AppendData { 99 | f.data[k] = v 100 | } 101 | return f.data 102 | } 103 | 104 | func (f *FieldSetType) render() string { 105 | buf := bytes.NewBuffer(nil) 106 | tpf := common.TmplDir(f.FormTheme) + "/" + f.FormTheme + "/" + f.Template + ".html" 107 | tpl, err := common.GetOrSetCachedTemplate(tpf, func() (*template.Template, error) { 108 | return common.ParseFiles(common.LookupPath(tpf)) 109 | }) 110 | if err != nil { 111 | return err.Error() 112 | } 113 | err = tpl.Execute(buf, f.Data()) 114 | if err != nil { 115 | return err.Error() 116 | } 117 | return buf.String() 118 | } 119 | 120 | // Render translates a FieldSetType into HTML code and returns it as a template.HTML object. 121 | func (f *FieldSetType) Render() template.HTML { 122 | return template.HTML(f.render()) 123 | } 124 | 125 | func (f *FieldSetType) String() string { 126 | return f.render() 127 | } 128 | 129 | func (f *FieldSetType) SetLang(lang string) { 130 | f.Language = lang 131 | } 132 | 133 | func (f *FieldSetType) Lang() string { 134 | return f.Language 135 | } 136 | 137 | func (f *FieldSetType) Clone() config.FormElement { 138 | fc := *f 139 | return &fc 140 | } 141 | 142 | func (f *FieldSetType) SetTemplate(tmpl string) *FieldSetType { 143 | f.Template = tmpl 144 | return f 145 | } 146 | 147 | // FieldSet creates and returns a new FieldSetType with the given name and list of fields. 148 | // Every method for FieldSetType objects returns the object itself, so that call can be chained. 149 | func FieldSet(name string, label string, theme string, elems ...config.FormElement) *FieldSetType { 150 | ret := &FieldSetType{ 151 | Template: "fieldset", 152 | CurrName: name, 153 | OrigName: name, 154 | Label: label, 155 | Classes: common.HTMLAttrValues{}, 156 | Tags: common.HTMLAttrValues{}, 157 | FieldList: elems, 158 | containerMap: make(map[string]string), 159 | fieldMap: map[string]int{}, 160 | AppendData: map[string]interface{}{}, 161 | FormTheme: theme, 162 | } 163 | for i, elem := range elems { 164 | ret.fieldMap[elem.OriginalName()] = i 165 | } 166 | return ret 167 | } 168 | 169 | // SortAll("field1,field2") or SortAll("field1","field2") 170 | func (f *FieldSetType) SortAll(sortList ...string) *FieldSetType { 171 | elem := f.FieldList 172 | size := len(elem) 173 | f.FieldList = make([]config.FormElement, size) 174 | var sortSlice []string 175 | if len(sortList) == 1 { 176 | sortSlice = strings.Split(sortList[0], ",") 177 | } else { 178 | sortSlice = sortList 179 | } 180 | for k, fieldName := range sortSlice { 181 | if oldIndex, ok := f.fieldMap[fieldName]; ok { 182 | f.FieldList[k] = elem[oldIndex] 183 | f.fieldMap[fieldName] = k 184 | } 185 | } 186 | return f 187 | } 188 | 189 | // Elements adds the provided elements to the fieldset. 190 | func (f *FieldSetType) Elements(elems ...config.FormElement) { 191 | for _, e := range elems { 192 | switch v := e.(type) { 193 | case fields.FieldInterface: 194 | f.addField(v) 195 | case *FieldSetType: 196 | f.addFieldSet(v) 197 | case *LangSetType: 198 | f.addLangSet(v) 199 | } 200 | } 201 | } 202 | 203 | func (f *FieldSetType) addFieldSet(fs *FieldSetType) *FieldSetType { 204 | for _, v := range fs.FieldList { 205 | v.SetData("container", "fieldset") 206 | f.containerMap[v.OriginalName()] = fs.OriginalName() 207 | } 208 | f.FieldList = append(f.FieldList, fs) 209 | f.fieldMap[fs.OriginalName()] = len(f.FieldList) - 1 210 | return f 211 | } 212 | 213 | func (f *FieldSetType) addLangSet(fs *LangSetType) *FieldSetType { 214 | for _, v := range fs.fieldMap { 215 | v.SetData("container", "langset") 216 | f.containerMap[v.OriginalName()] = fs.OriginalName() 217 | } 218 | f.FieldList = append(f.FieldList, fs) 219 | f.fieldMap[fs.OriginalName()] = len(f.FieldList) - 1 220 | return f 221 | } 222 | 223 | func (f *FieldSetType) addField(field fields.FieldInterface) *FieldSetType { 224 | field.SetTheme(f.FormTheme) 225 | field.SetData(`container`, `fieldset`) 226 | f.FieldList = append(f.FieldList, field) 227 | f.fieldMap[field.OriginalName()] = len(f.FieldList) - 1 228 | return f 229 | } 230 | 231 | // Sort("field1:1,field2:2") or Sort("field1:1","field2:2") 232 | func (f *FieldSetType) Sort(sortList ...string) *FieldSetType { 233 | size := len(f.FieldList) 234 | endIdx := size - 1 235 | var sortSlice []string 236 | if len(sortList) == 1 { 237 | sortSlice = strings.Split(sortList[0], ",") 238 | } else { 239 | sortSlice = sortList 240 | } 241 | var index int 242 | for _, nameIndex := range sortSlice { 243 | ni := strings.Split(nameIndex, ":") 244 | fieldName := ni[0] 245 | if len(ni) > 1 { 246 | if ni[1] == "last" { 247 | index = endIdx 248 | } else if idx, err := strconv.Atoi(ni[1]); err != nil { 249 | continue 250 | } else { 251 | if idx >= 0 { 252 | index = idx 253 | } else { 254 | index = endIdx + idx 255 | } 256 | 257 | } 258 | } 259 | if oldIndex, ok := f.fieldMap[fieldName]; ok { 260 | if oldIndex != index && size > index { 261 | f.sortFields(index, oldIndex, endIdx, size) 262 | } 263 | } 264 | index++ 265 | } 266 | return f 267 | } 268 | 269 | func (f *FieldSetType) Sort2Last(fieldsName ...string) *FieldSetType { 270 | size := len(f.FieldList) 271 | endIdx := size - 1 272 | index := endIdx 273 | for n := len(fieldsName) - 1; n >= 0; n-- { 274 | fieldName := fieldsName[n] 275 | if oldIndex, ok := f.fieldMap[fieldName]; ok { 276 | if oldIndex != index && index >= 0 { 277 | f.sortFields(index, oldIndex, endIdx, size) 278 | } 279 | } 280 | index-- 281 | } 282 | return f 283 | } 284 | 285 | // Field returns the field identified by name. It returns an empty field if it is missing. 286 | func (f *FieldSetType) Field(name string) fields.FieldInterface { 287 | ind, ok := f.fieldMap[name] 288 | if !ok { 289 | return &fields.Field{} 290 | } 291 | switch v := f.FieldList[ind].(type) { 292 | case fields.FieldInterface: 293 | return v 294 | case *FieldSetType: 295 | if v, ok := f.containerMap[name]; ok { 296 | return f.FieldSet(v).Field(name) 297 | } 298 | case *LangSetType: 299 | if v, ok := f.containerMap[name]; ok { 300 | return f.LangSet(v).Field(name) 301 | } 302 | } 303 | return &fields.Field{} 304 | } 305 | 306 | // FieldSet returns the fieldset identified by name. It returns an empty field if it is missing. 307 | func (f *FieldSetType) FieldSet(name string) *FieldSetType { 308 | ind, ok := f.fieldMap[name] 309 | if !ok { 310 | return &FieldSetType{} 311 | } 312 | switch v := f.FieldList[ind].(type) { 313 | case *FieldSetType: 314 | return v 315 | default: 316 | return &FieldSetType{} 317 | } 318 | } 319 | 320 | // LangSet returns the fieldset identified by name. It returns an empty field if it is missing. 321 | func (f *FieldSetType) LangSet(name string) *LangSetType { 322 | ind, ok := f.fieldMap[name] 323 | if !ok { 324 | return &LangSetType{} 325 | } 326 | switch v := f.FieldList[ind].(type) { 327 | case *LangSetType: 328 | return v 329 | default: 330 | return &LangSetType{} 331 | } 332 | } 333 | 334 | // Name returns the name of the fieldset. 335 | func (f *FieldSetType) Name() string { 336 | return f.CurrName 337 | } 338 | 339 | // AddClass saves the provided class for the fieldset. 340 | func (f *FieldSetType) AddClass(class string) *FieldSetType { 341 | f.Classes.Add(class) 342 | return f 343 | } 344 | 345 | // RemoveClass removes the provided class from the fieldset, if it was present. Nothing is done if it was not originally present. 346 | func (f *FieldSetType) RemoveClass(class string) *FieldSetType { 347 | f.Classes.Remove(class) 348 | return f 349 | } 350 | 351 | // AddTag adds a no-value parameter (e.g.: "disabled", "checked") to the fieldset. 352 | func (f *FieldSetType) AddTag(tag string) *FieldSetType { 353 | f.Tags.Add(tag) 354 | return f 355 | } 356 | 357 | // RemoveTag removes a tag from the fieldset, if it was present. 358 | func (f *FieldSetType) RemoveTag(tag string) *FieldSetType { 359 | f.Tags.Remove(tag) 360 | return f 361 | } 362 | 363 | // Disable adds tag "disabled" to the fieldset, making it unresponsive in some environment (e.g.: Bootstrap). 364 | func (f *FieldSetType) Disable() *FieldSetType { 365 | f.AddTag("disabled") 366 | return f 367 | } 368 | 369 | // Enable removes tag "disabled" from the fieldset, making it responsive. 370 | func (f *FieldSetType) Enable() *FieldSetType { 371 | f.RemoveTag("disabled") 372 | return f 373 | } 374 | 375 | func (f *FieldSetType) sortFields(index, oldIndex, endIdx, size int) { 376 | 377 | var newFields []config.FormElement 378 | oldFields := make([]config.FormElement, size) 379 | copy(oldFields, f.FieldList) 380 | var min, max int 381 | if index > oldIndex { 382 | //[ ][I][ ][ ][ ][ ] I:oldIndex=1 383 | //[ ][ ][ ][ ][I][ ] I:index=4 384 | if oldIndex > 0 { 385 | newFields = oldFields[0:oldIndex] 386 | } 387 | newFields = append(newFields, oldFields[oldIndex+1:index+1]...) 388 | newFields = append(newFields, f.FieldList[oldIndex]) 389 | if index+1 <= endIdx { 390 | newFields = append(newFields, f.FieldList[index+1:]...) 391 | } 392 | min = oldIndex 393 | max = index 394 | } else { 395 | //[ ][ ][ ][ ][I][ ] I:oldIndex=4 396 | //[ ][I][ ][ ][ ][ ] I:index=1 397 | if index > 0 { 398 | newFields = oldFields[0:index] 399 | } 400 | newFields = append(newFields, oldFields[oldIndex]) 401 | newFields = append(newFields, f.FieldList[index:oldIndex]...) 402 | if oldIndex+1 <= endIdx { 403 | newFields = append(newFields, f.FieldList[oldIndex+1:]...) 404 | } 405 | min = index 406 | max = oldIndex 407 | } 408 | for i := min; i <= max; i++ { 409 | f.fieldMap[newFields[i].OriginalName()] = i 410 | } 411 | f.FieldList = newFields 412 | } 413 | -------------------------------------------------------------------------------- /forms_marshal.go: -------------------------------------------------------------------------------- 1 | package forms 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/webx-top/com/encoding/json" 7 | ) 8 | 9 | func NewForms(f *Form) *Forms { 10 | return &Forms{ 11 | Form: f, 12 | } 13 | } 14 | 15 | type Forms struct { 16 | *Form 17 | } 18 | 19 | // MarshalJSON allows type Pagination to be used with json.Marshal 20 | func (f *Forms) MarshalJSON() ([]byte, error) { 21 | f.runBefore() 22 | return json.Marshal(f.Form) 23 | } 24 | 25 | // MarshalXML allows type Pagination to be used with xml.Marshal 26 | func (f *Forms) MarshalXML(e *xml.Encoder, start xml.StartElement) error { 27 | f.runBefore() 28 | return e.EncodeElement(f.Form, start) 29 | } 30 | -------------------------------------------------------------------------------- /forms_test.go: -------------------------------------------------------------------------------- 1 | package forms 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/coscms/forms/common" 8 | "github.com/coscms/forms/config" 9 | "github.com/webx-top/com" 10 | ) 11 | 12 | func TestForms(t *testing.T) { 13 | type Data struct { 14 | Test string 15 | } 16 | mp := map[string]interface{}{ 17 | `name`: `test`, 18 | `age`: 20, 19 | `items`: map[string]string{ 20 | `itemK1`: `itemV1`, 21 | }, 22 | `data`: &Data{ 23 | Test: `test-data`, 24 | }, 25 | `list`: []string{ 26 | `1`, `2`, 27 | }, 28 | `listData`: []*Data{ 29 | &Data{ 30 | Test: `test-listdata-1`, 31 | }, &Data{ 32 | Test: `test-listdata-2`, 33 | }, 34 | }, 35 | } 36 | cfg := NewConfig() 37 | cfg.AddElement(&config.Element{ 38 | ID: `input-name`, 39 | Type: `text`, 40 | Name: `name`, 41 | Label: `名称`, 42 | }, &config.Element{ 43 | ID: `input-items-k1`, 44 | Type: `text`, 45 | Name: `items[itemK1]`, 46 | Label: `Item K1`, 47 | }, &config.Element{ 48 | ID: `input-data-test`, 49 | Type: `text`, 50 | Name: `data.test`, 51 | Label: `Data`, 52 | }, &config.Element{ 53 | ID: `input-list-0`, 54 | Type: `text`, 55 | Name: `list.0`, 56 | Label: `List 0`, 57 | }, &config.Element{ 58 | ID: `input-list-2`, 59 | Type: `text`, 60 | Name: `list.2`, 61 | Label: `List 2`, 62 | }, &config.Element{ 63 | ID: `input-listdata-0`, 64 | Type: `text`, 65 | Name: `listData.0.test`, 66 | Label: `ListData 0`, 67 | }) 68 | form := NewWithModelConfig(mp, cfg) 69 | com.Dump(form.Data()) 70 | result := form.String() 71 | fmt.Println(result) 72 | } 73 | 74 | func TestParseConfig(t *testing.T) { 75 | cfg := config.Config{ 76 | Elements: []*config.Element{ 77 | { 78 | ID: ``, 79 | Type: `text`, 80 | Name: `test`, 81 | }, 82 | }, 83 | } 84 | f := NewForms(New()) 85 | f.Theme = common.BOOTSTRAP 86 | f.Init(&cfg) 87 | f.ParseFromConfig(true) 88 | com.Dump(cfg.Clone()) 89 | } 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/coscms/forms 2 | 3 | go 1.21 4 | toolchain go1.24.1 5 | 6 | require ( 7 | github.com/admpub/json5 v0.0.1 8 | github.com/stretchr/testify v1.9.0 9 | github.com/webx-top/com v1.3.19 10 | github.com/webx-top/tagfast v0.0.1 11 | github.com/webx-top/validation v0.0.3 12 | golang.org/x/sync v0.11.0 13 | ) 14 | 15 | require ( 16 | github.com/admpub/fsnotify v1.7.0 // indirect 17 | github.com/davecgh/go-spew v1.1.1 // indirect 18 | github.com/francoispqt/gojay v1.2.13 // indirect 19 | github.com/goccy/go-json v0.10.4 // indirect 20 | github.com/hashicorp/go-version v1.7.0 // indirect 21 | github.com/json-iterator/go v1.1.12 // indirect 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 23 | github.com/modern-go/reflect2 v1.0.2 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | golang.org/x/crypto v0.35.0 // indirect 26 | golang.org/x/sys v0.30.0 // indirect 27 | golang.org/x/text v0.22.0 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 4 | cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= 5 | dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= 6 | dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= 7 | dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= 8 | dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= 9 | git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= 10 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 11 | github.com/admpub/fsnotify v1.7.0 h1:pI04ANljHE5cS3fr+uXMgDG4/Cv3iye40nH/oZE8pB0= 12 | github.com/admpub/fsnotify v1.7.0/go.mod h1:D9ecq8Ksz5grAhvRRrbN7AdVXnwYx3OxM1+xfjru4+4= 13 | github.com/admpub/json5 v0.0.1 h1:ZgD9YKNEpOqjcg553hqi1Zv8f8tNWLjxZrFcoksCRCw= 14 | github.com/admpub/json5 v0.0.1/go.mod h1:spt0tC/we4imVn3yB/FUuENv2qDJRSi7cxnt+buqWVE= 15 | github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 16 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 17 | github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= 18 | github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= 19 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 20 | github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 25 | github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= 26 | github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= 27 | github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= 28 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 29 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 30 | github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= 31 | github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 32 | github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= 33 | github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 34 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 35 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 36 | github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= 37 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 38 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 39 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 40 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 42 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 43 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 44 | github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= 45 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 46 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 47 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 48 | github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= 49 | github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= 50 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 51 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 52 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 53 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 54 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 55 | github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= 56 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 57 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 58 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 59 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 60 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 61 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 62 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 63 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 64 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 65 | github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 66 | github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 67 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 68 | github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= 69 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 71 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 72 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 73 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 74 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 75 | github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= 76 | github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= 77 | github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= 78 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 79 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 80 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 81 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 82 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 83 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 84 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 85 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 86 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 87 | github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= 88 | github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= 89 | github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= 90 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 91 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 92 | github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= 93 | github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= 94 | github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= 95 | github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= 96 | github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= 97 | github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= 98 | github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= 99 | github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= 100 | github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= 101 | github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= 102 | github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= 103 | github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= 104 | github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= 105 | github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= 106 | github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 107 | github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= 108 | github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= 109 | github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= 110 | github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= 111 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 112 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 113 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 114 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 115 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 116 | github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= 117 | github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= 118 | github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= 119 | github.com/webx-top/com v1.3.19 h1:qaSCEYzHXvzk5RHoW5+gh2g66wIY9uQrTRRbwFt1wlU= 120 | github.com/webx-top/com v1.3.19/go.mod h1:Pw5Yr5UU0ZtwZU0xVyfyRdqf77wyzh+oRNYnOpX9Oyc= 121 | github.com/webx-top/tagfast v0.0.0-20161020041435-9a2065ce3dd2/go.mod h1:pMe3sJitHxbxX2EAI/v9HEAXjodP4c+yUVw3rbKcljI= 122 | github.com/webx-top/tagfast v0.0.1 h1:SNC2ui+ngSCwMaQgtfAsRKFLAt0GuiquMO9jwySfsGU= 123 | github.com/webx-top/tagfast v0.0.1/go.mod h1:vArAB9fuv8AVZ7NfWedERyyY1og7lEHkjuq+o5YuaP8= 124 | github.com/webx-top/validation v0.0.3 h1:6vBoAp5iqjIpfFA+XoCnIzBHcuLjQzxv7MRlshptUqk= 125 | github.com/webx-top/validation v0.0.3/go.mod h1:74lFGn3naxJl8FelK8RfCatVCKDB6G2ckG96tm3w1ug= 126 | go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= 127 | go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= 128 | golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= 129 | golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 130 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 131 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 132 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 133 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 134 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 135 | golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 136 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 137 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 138 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 139 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 140 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 141 | golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 142 | golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 143 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 144 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 145 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 146 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 147 | golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 148 | golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 149 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 150 | golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 151 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 152 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 153 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 154 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 155 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 156 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 157 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 158 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 159 | golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 160 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 161 | golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 163 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 164 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 165 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 166 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 167 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 168 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 169 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 170 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 171 | golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 172 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 173 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 174 | google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 175 | google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= 176 | google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= 177 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 178 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 179 | google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 180 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 181 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 182 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 183 | google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 184 | google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= 185 | google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 186 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 187 | google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= 188 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 189 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 190 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 191 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 192 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 193 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 194 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 195 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 196 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 197 | grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= 198 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 199 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 200 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 201 | sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= 202 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 203 | -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present Wenhui Shen 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package forms 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "net/url" 22 | "os" 23 | "path/filepath" 24 | "reflect" 25 | "strconv" 26 | "strings" 27 | 28 | "github.com/admpub/json5" 29 | 30 | "github.com/webx-top/tagfast" 31 | 32 | "github.com/coscms/forms/common" 33 | "github.com/coscms/forms/config" 34 | "github.com/coscms/forms/fields" 35 | "github.com/webx-top/validation" 36 | ) 37 | 38 | func UnmarshalFile(filename string) (r *config.Config, err error) { 39 | filename, err = filepath.Abs(filename) 40 | if err != nil { 41 | return 42 | } 43 | return common.GetOrSetCachedConfig(filename, func() (*config.Config, error) { 44 | b, err := os.ReadFile(filename) 45 | if err != nil { 46 | return nil, err 47 | } 48 | r = &config.Config{} 49 | err = json5.Unmarshal(b, r) 50 | if err != nil { 51 | return nil, err 52 | } 53 | fmt.Println(`cache form config:`, filename) 54 | return r, nil 55 | }) 56 | } 57 | 58 | func Unmarshal(b []byte, key string) (r *config.Config, err error) { 59 | return common.GetOrSetCachedConfig(key, func() (*config.Config, error) { 60 | r := &config.Config{} 61 | err = json5.Unmarshal(b, r) 62 | if err != nil { 63 | return nil, err 64 | } 65 | fmt.Println(`cache form config:`, key) 66 | return r, nil 67 | }) 68 | } 69 | 70 | func NewWithModelConfig(m interface{}, r *config.Config) *Form { 71 | form := NewWithConfig(r) 72 | form.SetModel(m).ParseFromConfig() 73 | return form 74 | } 75 | 76 | func (form *Form) Generate(m interface{}, jsonFile string) error { 77 | r, err := UnmarshalFile(jsonFile) 78 | if err != nil { 79 | return err 80 | } 81 | form.Init(r).SetModel(m) 82 | form.ParseFromConfig() 83 | return nil 84 | } 85 | 86 | func (form *Form) ParseFromJSONFile(jsonFile string) error { 87 | r, err := UnmarshalFile(jsonFile) 88 | if err != nil { 89 | return err 90 | } 91 | form.Init(r) 92 | form.ParseFromConfig() 93 | return nil 94 | } 95 | 96 | func (form *Form) ParseFromJSON(b []byte, key string) error { 97 | r, err := Unmarshal(b, key) 98 | if err != nil { 99 | return err 100 | } 101 | form.Init(r) 102 | form.ParseFromConfig() 103 | return nil 104 | } 105 | 106 | func (form *Form) ValidFromJSONFile(jsonFile string) error { 107 | r, err := UnmarshalFile(jsonFile) 108 | if err != nil { 109 | return err 110 | } 111 | form.Init(r) 112 | form.ValidFromConfig() 113 | return nil 114 | } 115 | 116 | func (form *Form) ValidFromJSON(b []byte, key string) error { 117 | r, err := Unmarshal(b, key) 118 | if err != nil { 119 | return err 120 | } 121 | form.Init(r) 122 | form.ValidFromConfig() 123 | return nil 124 | } 125 | 126 | func (form *Form) ValidFromConfig(model ...interface{}) *Form { 127 | form.Validate() 128 | var m interface{} 129 | if len(model) > 0 { 130 | m = model[0] 131 | } 132 | if m == nil { 133 | m = form.Model 134 | } 135 | if m == nil { 136 | return form 137 | } 138 | t := reflect.TypeOf(m) 139 | v := reflect.ValueOf(m) 140 | if t.Kind() == reflect.Ptr { 141 | t = t.Elem() 142 | v = v.Elem() 143 | } 144 | r := form.config 145 | form.ValidElements(r.Elements, t, v) 146 | return form 147 | } 148 | 149 | // Filter 过滤客户端提交的数据 150 | func (form *Form) Filter(values url.Values) (url.Values, *validation.ValidationError) { 151 | form.Validate() 152 | r := url.Values{} 153 | var err *validation.ValidationError 154 | for _, ele := range form.config.Elements { 155 | switch ele.Type { 156 | case `langset`, `fieldset`: 157 | for _, e := range ele.Elements { 158 | r, err = form.FilterByElement(values, r, e) 159 | if err != nil { 160 | return r, err 161 | } 162 | } 163 | default: 164 | r, err = form.FilterByElement(values, r, ele) 165 | if err != nil { 166 | return r, err 167 | } 168 | } 169 | } 170 | return r, err 171 | } 172 | 173 | // FilterByElement 过滤单个元素 174 | func (form *Form) FilterByElement(input url.Values, output url.Values, ele *config.Element) (url.Values, *validation.ValidationError) { 175 | if len(ele.Valid) == 0 { 176 | if vals, ok := input[ele.Name]; ok { 177 | output[ele.Name] = vals 178 | } 179 | } else { 180 | if vals, ok := input[ele.Name]; ok { 181 | for _, val := range vals { 182 | if !form.valid.ValidField(ele.Name, val, ele.Valid) { 183 | return output, form.Error() 184 | } 185 | } 186 | output[ele.Name] = vals 187 | } 188 | } 189 | return output, form.Error() 190 | } 191 | 192 | func (form *Form) ValidElements(elements []*config.Element, t reflect.Type, v reflect.Value) { 193 | for _, ele := range elements { 194 | switch ele.Type { 195 | case `langset`: 196 | form.ValidElements(ele.Elements, t, v) 197 | case `fieldset`: 198 | form.ValidElements(ele.Elements, t, v) 199 | default: 200 | if !form.IsIgnored(ele.Name) { 201 | form.validElement(ele, t, v) 202 | } 203 | } 204 | } 205 | } 206 | 207 | func (form *Form) IsIgnored(fieldName string) bool { 208 | for _, name := range form.ignoreValid { 209 | if fieldName == name { 210 | return true 211 | } 212 | } 213 | return false 214 | } 215 | 216 | func (form *Form) CloseValid(fieldName ...string) *Form { 217 | if form.ignoreValid == nil { 218 | form.ignoreValid = []string{} 219 | } 220 | form.ignoreValid = append(form.ignoreValid, fieldName...) 221 | return form 222 | } 223 | 224 | func (form *Form) ParseFromConfig(insertErrors ...bool) *Form { 225 | return form.ParseModelFromConfig(nil, insertErrors...) 226 | } 227 | 228 | func (form *Form) ParseModelFromConfig(model interface{}, insertErrors ...bool) *Form { 229 | if model == nil { 230 | model = form.Model 231 | } 232 | t := reflect.TypeOf(model) 233 | v := reflect.ValueOf(model) 234 | if t != nil && t.Kind() == reflect.Ptr { 235 | t = t.Elem() 236 | v = v.Elem() 237 | } 238 | r := form.config 239 | form.ParseModelElements(model, form, r.Elements, r.Languages, t, v, ``) 240 | if len(insertErrors) < 1 || insertErrors[0] { 241 | form.InsertErrors() 242 | } 243 | for _, attr := range r.Attributes { 244 | var k, v string 245 | switch len(attr) { 246 | case 2: 247 | v = attr[1] 248 | fallthrough 249 | case 1: 250 | k = attr[0] 251 | form.SetParam(k, v) 252 | } 253 | } 254 | if len(r.ID) > 0 { 255 | form.SetID(r.ID) 256 | } 257 | if r.WithButtons { 258 | if r.Buttons == nil { 259 | r.Buttons = []string{} 260 | } 261 | form.AddButton(r.BtnsTemplate, r.Buttons...) 262 | } 263 | for key, val := range r.Data { 264 | form.SetData(key, val) 265 | } 266 | return form 267 | } 268 | 269 | func (form *Form) ParseElements(es ElementSetter, 270 | elements []*config.Element, langs []*config.Language, 271 | t reflect.Type, v reflect.Value, lang string) { 272 | form.ParseModelElements(form.Model, es, elements, langs, t, v, lang) 273 | } 274 | 275 | func (form *Form) ParseModelElements(model interface{}, es ElementSetter, 276 | elements []*config.Element, langs []*config.Language, 277 | t reflect.Type, v reflect.Value, lang string) { 278 | for _, ele := range elements { 279 | switch ele.Type { 280 | case `langset`: 281 | if ele.Languages == nil { 282 | ele.Languages = langs 283 | } 284 | f := form.NewLangSet(ele.Name, ele.Languages) 285 | f.SetHelptext(form.labelFn(ele.HelpText)) 286 | if len(ele.Template) > 0 { 287 | f.SetTemplate(ele.Template) 288 | } 289 | f.SetData("container", "langset") 290 | for key, val := range ele.Data { 291 | f.SetData(key, val) 292 | } 293 | form.ParseModelElements(model, f, ele.Elements, ele.Languages, t, v, ``) 294 | for _, v := range ele.Attributes { 295 | switch len(v) { 296 | case 2: 297 | f.SetParam(v[0], v[1]) 298 | case 1: 299 | f.AddTag(v[0]) 300 | } 301 | } 302 | es.Elements(f) 303 | case `fieldset`: 304 | f := form.NewFieldSet(ele.Name, form.labelFn(ele.Label)) 305 | if len(ele.Template) > 0 { 306 | f.SetTemplate(ele.Template) 307 | } 308 | f.SetData("container", "fieldset") 309 | for key, val := range ele.Data { 310 | f.SetData(key, val) 311 | } 312 | form.ParseModelElements(model, f, ele.Elements, ele.Languages, t, v, ``) 313 | f.SetLabelCols(ele.LabelCols) 314 | f.SetFieldCols(ele.FieldCols) 315 | f.SetLang(lang) 316 | f.SetHelptext(form.labelFn(ele.HelpText)) 317 | es.Elements(f) 318 | default: 319 | f := form.parseElement(model, ele, t, v) 320 | if f != nil { 321 | f.SetLang(lang) 322 | es.Elements(f) 323 | } 324 | } 325 | } 326 | } 327 | 328 | func (form *Form) parseNameToStructFieldName(name string) []string { 329 | name = form.cleanName(name) 330 | if strings.HasSuffix(name, `]`) { 331 | return splitFormNames(name) 332 | } 333 | return strings.Split(name, `.`) 334 | } 335 | 336 | func (form *Form) cleanName(name string) string { 337 | if len(form.config.TrimNamePrefix) > 0 { 338 | name = strings.TrimPrefix(name, form.config.TrimNamePrefix) 339 | } 340 | if form.structFieldConverter != nil { 341 | name = form.structFieldConverter(name) 342 | } 343 | return name 344 | } 345 | 346 | func (form *Form) parseElement(model interface{}, ele *config.Element, typ reflect.Type, val reflect.Value) (f *fields.Field) { 347 | var sv string 348 | value := val 349 | if model != nil && !form.IsOmit(ele.Name) { 350 | parts := form.parseNameToStructFieldName(ele.Name) 351 | isValid := true 352 | for _, field := range parts { 353 | if value.Kind() == reflect.Ptr { 354 | if value.IsNil() { 355 | isValid = false 356 | break 357 | } 358 | value = value.Elem() 359 | } 360 | switch typ.Kind() { 361 | case reflect.Map: 362 | index := reflect.ValueOf(field) 363 | value = value.MapIndex(index) 364 | case reflect.Slice: 365 | index, _ := strconv.Atoi(field) 366 | if index >= value.Len() { 367 | isValid = false 368 | goto OUTLOOP 369 | } 370 | value = value.Index(index) 371 | case reflect.Struct: 372 | field = strings.Title(field) 373 | value = value.FieldByName(field) 374 | default: 375 | isValid = false 376 | goto OUTLOOP 377 | } 378 | if !value.IsValid() { 379 | isValid = false 380 | break 381 | } 382 | if value.Kind() == reflect.Interface { 383 | value = reflect.ValueOf(value.Interface()) 384 | } 385 | value = reflect.Indirect(value) 386 | kind := value.Kind() 387 | if kind != reflect.Struct && kind != reflect.Map && kind != reflect.Slice { 388 | break 389 | } 390 | typ = value.Type() 391 | } 392 | 393 | OUTLOOP: 394 | if isValid { 395 | sv = fmt.Sprintf("%v", value.Interface()) 396 | } 397 | } 398 | isStruct := typ != nil && typ.Kind() == reflect.Struct 399 | structFieldName := strings.Title(form.cleanName(ele.Name)) 400 | switch ele.Type { 401 | case common.DATE: 402 | dateFormat := fields.DATE_FORMAT 403 | if len(ele.Format) > 0 { 404 | dateFormat = ele.Format 405 | } else if isStruct { 406 | if structField, ok := typ.FieldByName(structFieldName); ok { 407 | if format := tagfast.Value(typ, structField, `form_format`); len(format) > 0 { 408 | dateFormat = format 409 | } 410 | } 411 | } 412 | f = fields.TextField(ele.Name, ele.Type) 413 | if v, isEmpty := fields.ConvertTime(value.Interface()); !v.IsZero() { 414 | f.SetValue(v.Format(dateFormat)) 415 | } else if isEmpty { 416 | f.SetValue(``) 417 | } else { 418 | f.SetValue(ele.Value) 419 | } 420 | 421 | case common.DATETIME: 422 | dateFormat := fields.DATETIME_FORMAT 423 | if len(ele.Format) > 0 { 424 | dateFormat = ele.Format 425 | } else if isStruct { 426 | if structField, ok := typ.FieldByName(structFieldName); ok { 427 | if format := tagfast.Value(typ, structField, `form_format`); len(format) > 0 { 428 | dateFormat = format 429 | } 430 | } 431 | } 432 | f = fields.TextField(ele.Name, ele.Type) 433 | if v, isEmpty := fields.ConvertTime(value.Interface()); !v.IsZero() { 434 | f.SetValue(v.Format(dateFormat)) 435 | } else if isEmpty { 436 | f.SetValue(``) 437 | } else { 438 | f.SetValue(ele.Value) 439 | } 440 | 441 | case common.DATETIME_LOCAL: 442 | dateFormat := fields.DATETIME_FORMAT 443 | if len(ele.Format) > 0 { 444 | dateFormat = ele.Format 445 | } else if isStruct { 446 | if structField, ok := typ.FieldByName(structFieldName); ok { 447 | if format := tagfast.Value(typ, structField, `form_format`); len(format) > 0 { 448 | dateFormat = format 449 | } 450 | } 451 | } 452 | f = fields.TextField(ele.Name, ele.Type) 453 | if v, isEmpty := fields.ConvertTime(value.Interface()); !v.IsZero() { 454 | f.SetValue(v.Local().Format(dateFormat)) 455 | } else if isEmpty { 456 | f.SetValue(``) 457 | } else { 458 | f.SetValue(ele.Value) 459 | } 460 | 461 | case common.TIME: 462 | dateFormat := fields.TIME_FORMAT 463 | if len(ele.Format) > 0 { 464 | dateFormat = ele.Format 465 | } else if isStruct { 466 | if structField, ok := typ.FieldByName(structFieldName); ok { 467 | if format := tagfast.Value(typ, structField, `form_format`); len(format) > 0 { 468 | dateFormat = format 469 | } 470 | } 471 | } 472 | f = fields.TextField(ele.Name, ele.Type) 473 | if v, isEmpty := fields.ConvertTime(value.Interface()); !v.IsZero() { 474 | f.SetValue(v.Format(dateFormat)) 475 | } else if isEmpty { 476 | f.SetValue(``) 477 | } else { 478 | f.SetValue(ele.Value) 479 | } 480 | 481 | case common.TEXT: 482 | f = fields.TextField(ele.Name, ele.Type) 483 | format := ele.Format 484 | if len(format) == 0 && isStruct { 485 | if structField, ok := typ.FieldByName(structFieldName); ok { 486 | format = tagfast.Value(typ, structField, `form_format`) 487 | } 488 | } 489 | if len(format) > 0 { //时间格式 490 | if vt, isEmpty := fields.ConvertTime(sv); !vt.IsZero() { 491 | f.SetValue(vt.Format(format)) 492 | } else if isEmpty { 493 | f.SetValue(``) 494 | } 495 | } else { 496 | if len(sv) == 0 { 497 | f.SetValue(ele.Value) 498 | } else { 499 | f.SetValue(sv) 500 | } 501 | } 502 | 503 | case common.COLOR, common.EMAIL, common.FILE, common.HIDDEN, common.IMAGE, common.MONTH, common.SEARCH, common.URL, common.TEL, common.WEEK, common.NUMBER, common.PASSWORD: 504 | f = fields.TextField(ele.Name, ele.Type) 505 | if len(sv) == 0 { 506 | f.SetValue(ele.Value) 507 | } else { 508 | f.SetValue(sv) 509 | } 510 | 511 | case common.CHECKBOX, common.RADIO: 512 | choices := []fields.InputChoice{} 513 | hasSet := len(sv) > 0 514 | for _, v := range ele.Choices { 515 | if v.Checked { 516 | if hasSet && sv != v.Option[0] { 517 | v.Checked = false 518 | } 519 | } else { 520 | if hasSet { 521 | v.Checked = sv == v.Option[0] 522 | } 523 | } 524 | ic := fields.InputChoice{ 525 | ID: v.Option[0], 526 | Val: form.labelFn(v.Option[1]), 527 | Checked: v.Checked, 528 | } 529 | choices = append(choices, ic) 530 | } 531 | if ele.Type == common.CHECKBOX { 532 | f = fields.CheckboxField(ele.Name, choices) 533 | } else { 534 | f = fields.RadioField(ele.Name, choices) 535 | } 536 | if !hasSet { 537 | f.SetValue(ele.Value) 538 | } else { 539 | f.SetValue(sv) 540 | } 541 | 542 | case common.RANGE: 543 | f = fields.FieldWithType(ele.Name, ele.Type) 544 | if len(sv) == 0 { 545 | f.SetValue(ele.Value) 546 | } else { 547 | f.SetValue(sv) 548 | } 549 | 550 | case common.BUTTON, common.RESET, common.SUBMIT, common.STATIC, common.TEXTAREA: 551 | f = fields.FieldWithType(ele.Name, ele.Type) 552 | if len(sv) == 0 { 553 | f.SetText(ele.Value) 554 | } else { 555 | f.SetText(sv) 556 | } 557 | 558 | case common.SELECT: 559 | choices := map[string][]fields.InputChoice{} 560 | hasSet := len(sv) > 0 561 | for _, v := range ele.Choices { 562 | if _, ok := choices[v.Group]; !ok { 563 | choices[v.Group] = []fields.InputChoice{} 564 | } 565 | if v.Checked { 566 | if hasSet && sv != v.Option[0] { 567 | v.Checked = false 568 | } 569 | } else { 570 | if hasSet { 571 | v.Checked = sv == v.Option[0] 572 | } 573 | } 574 | ic := fields.InputChoice{ 575 | ID: v.Option[0], 576 | Val: form.labelFn(v.Option[1]), 577 | Checked: v.Checked, 578 | } 579 | choices[v.Group] = append(choices[v.Group], ic) 580 | } 581 | f = fields.SelectField(ele.Name, choices) 582 | if !hasSet { 583 | f.SetValue(ele.Value) 584 | } else { 585 | f.SetValue(sv) 586 | } 587 | 588 | default: 589 | return nil 590 | } 591 | for _, v := range ele.Attributes { 592 | switch len(v) { 593 | case 2: 594 | f.SetParam(v[0], v[1]) 595 | case 1: 596 | f.AddTag(v[0]) 597 | } 598 | } 599 | f.SetHelptext(form.labelFn(ele.HelpText)) 600 | f.SetLabel(form.labelFn(ele.Label)) 601 | for _, labelClass := range ele.LabelClasses { 602 | f.AddLabelClass(labelClass) 603 | } 604 | f.SetTemplate(ele.Template) 605 | f.SetID(ele.ID) 606 | if len(ele.Valid) > 0 { 607 | form.validTagFn(ele.Valid, f) 608 | } 609 | for key, val := range ele.Data { 610 | f.SetData(key, val) 611 | } 612 | f.SetLabelCols(ele.LabelCols) 613 | f.SetFieldCols(ele.FieldCols) 614 | return f 615 | } 616 | 617 | func (form *Form) validElement(ele *config.Element, _ reflect.Type, val reflect.Value) bool { 618 | if len(ele.Valid) == 0 { 619 | return true 620 | } 621 | parts := form.parseNameToStructFieldName(ele.Name) 622 | value := val 623 | isValid := true 624 | for _, field := range parts { 625 | field = strings.Title(field) 626 | if value.Kind() == reflect.Ptr { 627 | if value.IsNil() { 628 | value.Set(reflect.New(value.Type().Elem())) 629 | } 630 | value = value.Elem() 631 | } 632 | value = value.FieldByName(field) 633 | if !value.IsValid() { 634 | isValid = false 635 | break 636 | } 637 | } 638 | if isValid { 639 | sv := fmt.Sprintf("%v", value.Interface()) 640 | isValid = form.valid.ValidField(ele.Name, sv, ele.Valid) 641 | } 642 | return isValid 643 | } 644 | 645 | func (form *Form) ToJSONBlob(args ...*config.Config) (r []byte, err error) { 646 | var config *config.Config 647 | if len(args) > 0 { 648 | config = args[0] 649 | } 650 | if config == nil { 651 | config = form.ToConfig() 652 | } 653 | r, err = json.MarshalIndent(config, ``, ` `) 654 | return 655 | } 656 | 657 | func (form *Form) NewConfig() *config.Config { 658 | return NewConfig() 659 | } 660 | 661 | func (form *Form) ToConfig() *config.Config { 662 | conf := form.NewConfig() 663 | form.ParseModel() 664 | for _, v := range form.FieldList { 665 | var element *config.Element 666 | switch f := v.(type) { 667 | case *FieldSetType: 668 | element = &config.Element{ 669 | ID: ``, 670 | Type: `fieldset`, 671 | Name: ``, 672 | Label: f.Name(), 673 | Value: ``, 674 | HelpText: ``, 675 | Template: ``, 676 | Valid: ``, 677 | Attributes: make([][]string, 0), 678 | Choices: make([]*config.Choice, 0), 679 | Elements: make([]*config.Element, 0), 680 | } 681 | var temp string 682 | var join string 683 | for _, c := range f.Classes { 684 | temp += join + c 685 | join = ` ` 686 | } 687 | if len(temp) > 0 { 688 | element.Attributes = append(element.Attributes, []string{`class`, temp}) 689 | temp = `` 690 | join = `` 691 | } 692 | for _, c := range f.Tags { 693 | element.Attributes = append(element.Attributes, []string{c}) 694 | } 695 | for _, ff := range f.FieldList { 696 | if fi, ok := ff.(fields.FieldInterface); ok { 697 | element.Elements = append(element.Elements, fi.Element()) 698 | } 699 | } 700 | case fields.FieldInterface: 701 | element = f.Element() 702 | } 703 | if element != nil { 704 | conf.Elements = append(conf.Elements, element) 705 | } 706 | } 707 | return conf 708 | } 709 | -------------------------------------------------------------------------------- /langset.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present Wenhui Shen 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package forms 17 | 18 | import ( 19 | "bytes" 20 | "strconv" 21 | "strings" 22 | 23 | "html/template" 24 | 25 | "github.com/coscms/forms/common" 26 | "github.com/coscms/forms/config" 27 | "github.com/coscms/forms/fields" 28 | ) 29 | 30 | // LangSetType is a collection of fields grouped within a form. 31 | type LangSetType struct { 32 | Languages []*config.Language `json:"languages" xml:"languages"` 33 | CurrName string `json:"currName" xml:"currName"` 34 | OrigName string `json:"origName" xml:"origName"` 35 | Template string `json:"template" xml:"template"` 36 | Params map[string]interface{} `json:"params" xml:"params"` 37 | Tags common.HTMLAttrValues `json:"tags" xml:"tags"` 38 | Helptext string `json:"helpText" xml:"helpText"` 39 | AppendData map[string]interface{} `json:"appendData,omitempty" xml:"appendData,omitempty"` 40 | Alone bool `json:"alone,omitempty" xml:"alone,omitempty"` 41 | FormTheme string `json:"formTheme" xml:"formTheme"` 42 | 43 | langMap map[string]int //{"zh-CN":1} 44 | fieldMap map[string]config.FormElement //{"zh-CN:title":0x344555} 45 | containerMap map[string]string //{"name":"fieldset's name"} 46 | data map[string]interface{} 47 | } 48 | 49 | func (f *LangSetType) Cols() int { 50 | return 0 51 | } 52 | 53 | func (f *LangSetType) SetName(name string) { 54 | f.CurrName = name 55 | } 56 | 57 | func (f *LangSetType) OriginalName() string { 58 | return f.OrigName 59 | } 60 | 61 | func (f *LangSetType) SetLang(lang string) { 62 | } 63 | 64 | func (f *LangSetType) Lang() string { 65 | return `` 66 | } 67 | 68 | func (f *LangSetType) Clone() config.FormElement { 69 | fc := *f 70 | return &fc 71 | } 72 | 73 | func (f *LangSetType) AddLanguage(language *config.Language) { 74 | f.langMap[language.ID] = len(f.Languages) 75 | f.Languages = append(f.Languages, language) 76 | } 77 | 78 | func (f *LangSetType) Language(lang string) *config.Language { 79 | if ind, ok := f.langMap[lang]; ok { 80 | return f.Languages[ind] 81 | } 82 | return nil 83 | } 84 | 85 | // SetHelptext saves the field helptext. 86 | func (f *LangSetType) SetHelptext(text string) *LangSetType { 87 | f.Helptext = text 88 | return f 89 | } 90 | 91 | func (f *LangSetType) SetData(key string, value interface{}) { 92 | f.AppendData[key] = value 93 | } 94 | 95 | func (f *LangSetType) Data() map[string]interface{} { 96 | if len(f.data) > 0 { 97 | return f.data 98 | } 99 | safeParams := make(common.HTMLAttributes) 100 | safeParams.FillFrom(f.Params) 101 | f.data = map[string]interface{}{ 102 | "container": "langset", 103 | "params": safeParams, 104 | "tags": f.Tags, 105 | "langs": f.Languages, 106 | "name": f.CurrName, 107 | "helptext": f.Helptext, 108 | } 109 | for k, v := range f.AppendData { 110 | f.data[k] = v 111 | } 112 | return f.data 113 | } 114 | 115 | func (f *LangSetType) render() string { 116 | buf := bytes.NewBuffer(nil) 117 | tpf := common.TmplDir(f.FormTheme) + "/" + f.FormTheme + "/" + f.Template + ".html" 118 | tpl, err := common.GetOrSetCachedTemplate(tpf, func() (*template.Template, error) { 119 | return common.ParseFiles(common.LookupPath(tpf)) 120 | }) 121 | if err != nil { 122 | return err.Error() 123 | } 124 | err = tpl.Execute(buf, f.Data()) 125 | if err != nil { 126 | return err.Error() 127 | } 128 | return buf.String() 129 | } 130 | 131 | // Render translates a FieldSetType into HTML code and returns it as a template.HTML object. 132 | func (f *LangSetType) Render() template.HTML { 133 | return template.HTML(f.render()) 134 | } 135 | 136 | func (f *LangSetType) String() string { 137 | return f.render() 138 | } 139 | 140 | func (f *LangSetType) SetTemplate(tmpl string) *LangSetType { 141 | f.Template = tmpl 142 | return f 143 | } 144 | 145 | // FieldSet creates and returns a new FieldSetType with the given name and list of fields. 146 | // Every method for FieldSetType objects returns the object itself, so that call can be chained. 147 | func LangSet(name string, theme string, languages ...*config.Language) *LangSetType { 148 | ret := &LangSetType{ 149 | Languages: languages, 150 | langMap: map[string]int{}, 151 | containerMap: make(map[string]string), 152 | fieldMap: make(map[string]config.FormElement), 153 | CurrName: name, 154 | OrigName: name, 155 | Template: "langset", 156 | Params: map[string]interface{}{}, 157 | Tags: common.HTMLAttrValues{}, 158 | AppendData: map[string]interface{}{}, 159 | FormTheme: theme, 160 | } 161 | for i, language := range languages { 162 | ret.langMap[language.ID] = i 163 | } 164 | return ret 165 | } 166 | 167 | // SortAll("field1,field2") or SortAll("field1","field2") 168 | func (f *LangSetType) SortAll(sortList ...string) *LangSetType { 169 | elem := f.Languages 170 | size := len(elem) 171 | f.Languages = make([]*config.Language, size) 172 | var sortSlice []string 173 | if len(sortList) == 1 { 174 | sortSlice = strings.Split(sortList[0], ",") 175 | } else { 176 | sortSlice = sortList 177 | } 178 | for k, fieldName := range sortSlice { 179 | if oldIndex, ok := f.langMap[fieldName]; ok { 180 | f.Languages[k] = elem[oldIndex] 181 | f.langMap[fieldName] = k 182 | } 183 | } 184 | return f 185 | } 186 | 187 | // Elements adds the provided elements to the langset. 188 | func (f *LangSetType) Elements(elems ...config.FormElement) { 189 | for _, e := range elems { 190 | switch v := e.(type) { 191 | case fields.FieldInterface: 192 | f.addField(v) 193 | case *FieldSetType: 194 | f.addFieldSet(v) 195 | } 196 | } 197 | } 198 | 199 | func (f *LangSetType) addField(field fields.FieldInterface) *LangSetType { 200 | field.SetTheme(f.FormTheme) 201 | if f.Alone { 202 | if ind, ok := f.langMap[field.Lang()]; ok { 203 | field.SetLang(f.Languages[ind].ID) 204 | field.SetName(f.Languages[ind].Name(field.OriginalName())) 205 | f.Languages[ind].AddField(field) 206 | f.fieldMap[field.Lang()+`:`+field.OriginalName()] = field 207 | } 208 | return f 209 | } 210 | for k, language := range f.Languages { 211 | f.langMap[language.ID] = k 212 | if k == 0 { 213 | field.SetLang(language.ID) 214 | field.SetName(language.Name(field.OriginalName())) 215 | language.AddField(field) 216 | f.fieldMap[field.Lang()+`:`+field.OriginalName()] = field 217 | continue 218 | } 219 | fieldCopy := field.Clone() 220 | fieldCopy.SetLang(language.ID) 221 | fieldCopy.SetName(language.Name(fieldCopy.OriginalName())) 222 | language.AddField(fieldCopy) 223 | f.fieldMap[fieldCopy.Lang()+`:`+fieldCopy.OriginalName()] = fieldCopy 224 | } 225 | return f 226 | } 227 | 228 | func (f *LangSetType) addFieldSet(fs *FieldSetType) *LangSetType { 229 | if f.Alone { 230 | if ind, ok := f.langMap[fs.Lang()]; ok { 231 | for _, v := range fs.FieldList { 232 | v.SetData("container", "langset") 233 | v.SetLang(f.Languages[ind].ID) 234 | v.SetName(f.Languages[ind].Name(v.OriginalName())) 235 | key := v.Lang() + `:` + v.OriginalName() 236 | f.fieldMap[key] = v 237 | f.containerMap[key] = fs.OriginalName() 238 | } 239 | fs.SetLang(f.Languages[ind].ID) 240 | fs.SetName(f.Languages[ind].Name(fs.OriginalName())) 241 | f.Languages[ind].AddField(fs) 242 | f.fieldMap[fs.Lang()+`:`+fs.OriginalName()] = fs 243 | } 244 | return f 245 | } 246 | for k, language := range f.Languages { 247 | f.langMap[language.ID] = k 248 | if k == 0 { 249 | for _, v := range fs.FieldList { 250 | v.SetLang(language.ID) 251 | v.SetData("container", "langset") 252 | key := v.Lang() + `:` + v.OriginalName() 253 | f.fieldMap[key] = v 254 | f.containerMap[key] = fs.OriginalName() 255 | v.SetName(language.Name(v.OriginalName())) 256 | } 257 | fs.SetLang(language.ID) 258 | fs.SetName(language.Name(fs.OriginalName())) 259 | language.AddField(fs) 260 | f.fieldMap[fs.Lang()+`:`+fs.OriginalName()] = fs 261 | continue 262 | } 263 | fsCopy := fs.Clone().(*FieldSetType) 264 | fsCopy.FieldList = make([]config.FormElement, len(fs.FieldList)) 265 | for kk, v := range fs.FieldList { 266 | fieldCopy := v.Clone() 267 | fieldCopy.SetLang(language.ID) 268 | fieldCopy.SetName(language.Name(fieldCopy.OriginalName())) 269 | key := fieldCopy.Lang() + `:` + fieldCopy.OriginalName() 270 | f.fieldMap[key] = fieldCopy 271 | f.containerMap[key] = fs.OriginalName() 272 | fsCopy.FieldList[kk] = fieldCopy 273 | } 274 | fsCopy.SetLang(language.ID) 275 | fsCopy.SetName(language.Name(fsCopy.OriginalName())) 276 | language.AddField(fsCopy) 277 | f.fieldMap[fsCopy.Lang()+`:`+fsCopy.OriginalName()] = fsCopy 278 | } 279 | return f 280 | } 281 | 282 | // Field returns the field identified by name. It returns an empty field if it is missing. 283 | // param format: "language:name" 284 | func (f *LangSetType) Field(name string) fields.FieldInterface { 285 | field, ok := f.fieldMap[name] 286 | if !ok { 287 | return &fields.Field{} 288 | } 289 | switch v := field.(type) { 290 | case fields.FieldInterface: 291 | return v 292 | case *FieldSetType: 293 | if v, ok := f.containerMap[name]; ok { 294 | r := strings.SplitN(name, `:`, 2) 295 | switch len(r) { 296 | case 2: 297 | return f.FieldSet(v).Field(r[1]) 298 | case 1: 299 | return f.FieldSet(v).Field(r[0]) 300 | } 301 | } 302 | } 303 | return &fields.Field{} 304 | } 305 | 306 | // FieldSet returns the fieldset identified by name. 307 | // param format: "language:name" 308 | func (f *LangSetType) FieldSet(name string) *FieldSetType { 309 | field, ok := f.fieldMap[name] 310 | if !ok { 311 | return &FieldSetType{} 312 | } 313 | switch v := field.(type) { 314 | case *FieldSetType: 315 | return v 316 | default: 317 | return &FieldSetType{} 318 | } 319 | } 320 | 321 | // NewFieldSet creates and returns a new FieldSetType with the given name and list of fields. 322 | // Every method for FieldSetType objects returns the object itself, so that call can be chained. 323 | func (f *LangSetType) NewFieldSet(name string, label string, elems ...config.FormElement) *FieldSetType { 324 | return FieldSet(name, label, f.FormTheme, elems...) 325 | } 326 | 327 | // Sort Sort("field1:1,field2:2") or Sort("field1:1","field2:2") 328 | func (f *LangSetType) Sort(sortList ...string) *LangSetType { 329 | size := len(f.Languages) 330 | endIdx := size - 1 331 | var sortSlice []string 332 | if len(sortList) == 1 { 333 | sortSlice = strings.Split(sortList[0], ",") 334 | } else { 335 | sortSlice = sortList 336 | } 337 | var index int 338 | for _, nameIndex := range sortSlice { 339 | ni := strings.Split(nameIndex, ":") 340 | fieldName := ni[0] 341 | if len(ni) > 1 { 342 | if ni[1] == "last" { 343 | index = endIdx 344 | } else if idx, err := strconv.Atoi(ni[1]); err != nil { 345 | continue 346 | } else { 347 | if idx >= 0 { 348 | index = idx 349 | } else { 350 | index = endIdx + idx 351 | } 352 | } 353 | } 354 | if oldIndex, ok := f.langMap[fieldName]; ok { 355 | if oldIndex != index && size > index { 356 | f.sortFields(index, oldIndex, endIdx, size) 357 | } 358 | } 359 | index++ 360 | } 361 | return f 362 | } 363 | 364 | func (f *LangSetType) Sort2Last(fieldsName ...string) *LangSetType { 365 | size := len(f.Languages) 366 | endIdx := size - 1 367 | index := endIdx 368 | for n := len(fieldsName) - 1; n >= 0; n-- { 369 | fieldName := fieldsName[n] 370 | if oldIndex, ok := f.langMap[fieldName]; ok { 371 | if oldIndex != index && index >= 0 { 372 | f.sortFields(index, oldIndex, endIdx, size) 373 | } 374 | } 375 | index-- 376 | } 377 | return f 378 | } 379 | 380 | // Name returns the name of the langset. 381 | func (f *LangSetType) Name() string { 382 | return f.CurrName 383 | } 384 | 385 | // SetParam saves the provided param for the langset. 386 | func (f *LangSetType) SetParam(k string, v interface{}) *LangSetType { 387 | f.Params[k] = v 388 | return f 389 | } 390 | 391 | // DeleteParam removes the provided param from the langset, if it was present. Nothing is done if it was not originally present. 392 | func (f *LangSetType) DeleteParam(k string) *LangSetType { 393 | delete(f.Params, k) 394 | return f 395 | } 396 | 397 | // AddTag adds a no-value parameter (e.g.: "disabled", "checked") to the langset. 398 | func (f *LangSetType) AddTag(tag string) *LangSetType { 399 | f.Tags.Add(tag) 400 | return f 401 | } 402 | 403 | // RemoveTag removes a tag from the langset, if it was present. 404 | func (f *LangSetType) RemoveTag(tag string) *LangSetType { 405 | f.Tags.Remove(tag) 406 | return f 407 | } 408 | 409 | // Disable adds tag "disabled" to the langset, making it unresponsive in some environment (e.g.: Bootstrap). 410 | func (f *LangSetType) Disable() *LangSetType { 411 | f.AddTag("disabled") 412 | return f 413 | } 414 | 415 | // Enable removes tag "disabled" from the langset, making it responsive. 416 | func (f *LangSetType) Enable() *LangSetType { 417 | f.RemoveTag("disabled") 418 | return f 419 | } 420 | 421 | func (f *LangSetType) sortFields(index, oldIndex, endIdx, size int) { 422 | var newFields []*config.Language 423 | oldFields := make([]*config.Language, size) 424 | copy(oldFields, f.Languages) 425 | var min, max int 426 | if index > oldIndex { 427 | //[ ][I][ ][ ][ ][ ] I:oldIndex=1 428 | //[ ][ ][ ][ ][I][ ] I:index=4 429 | if oldIndex > 0 { 430 | newFields = oldFields[0:oldIndex] 431 | } 432 | newFields = append(newFields, oldFields[oldIndex+1:index+1]...) 433 | newFields = append(newFields, f.Languages[oldIndex]) 434 | if index+1 <= endIdx { 435 | newFields = append(newFields, f.Languages[index+1:]...) 436 | } 437 | min = oldIndex 438 | max = index 439 | } else { 440 | //[ ][ ][ ][ ][I][ ] I:oldIndex=4 441 | //[ ][I][ ][ ][ ][ ] I:index=1 442 | if index > 0 { 443 | newFields = oldFields[0:index] 444 | } 445 | newFields = append(newFields, oldFields[oldIndex]) 446 | newFields = append(newFields, f.Languages[index:oldIndex]...) 447 | if oldIndex+1 <= endIdx { 448 | newFields = append(newFields, f.Languages[oldIndex+1:]...) 449 | } 450 | min = index 451 | max = oldIndex 452 | } 453 | for i := min; i <= max; i++ { 454 | f.langMap[newFields[i].ID] = i 455 | } 456 | f.Languages = newFields 457 | } 458 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016-present Wenhui Shen 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package forms 17 | 18 | import ( 19 | "github.com/coscms/forms/config" 20 | "github.com/coscms/forms/fields" 21 | ) 22 | 23 | type ElementSetter interface { 24 | Elements(...config.FormElement) 25 | } 26 | 27 | func NewConfig() *config.Config { 28 | return &config.Config{ 29 | ID: `Forms`, 30 | Theme: `bootstrap3`, 31 | Template: ``, 32 | Method: `POST`, 33 | Attributes: [][]string{ 34 | []string{"class", "form-horizontal"}, 35 | []string{"role", "form"}, 36 | }, 37 | WithButtons: true, 38 | Buttons: make([]string, 0), 39 | Elements: make([]*config.Element, 0), 40 | } 41 | } 42 | 43 | // GenChoices generate choices 44 | // 45 | // type Data struct{ 46 | // ID string 47 | // Name string 48 | // } 49 | // 50 | // data:=[]*Data{ 51 | // &Data{ID:"a",Name:"One"}, 52 | // &Data{ID:"b",Name:"Two"}, 53 | // } 54 | // 55 | // GenChoices(len(data), func(index int) (string, string, bool){ 56 | // return data[index].ID,data[index].Name,false 57 | // }) 58 | // 59 | // or 60 | // 61 | // GenChoices(map[string]int{ 62 | // "":len(data), 63 | // }, func(group string,index int) (string, string, bool){ 64 | // 65 | // return data[index].ID,data[index].Name,false 66 | // }) 67 | func GenChoices(lenType interface{}, fnType interface{}) interface{} { 68 | switch fn := fnType.(type) { 69 | case func(int) (string, string, bool): 70 | length, ok := lenType.(int) 71 | if !ok { 72 | return []fields.InputChoice{} 73 | } 74 | result := make([]fields.InputChoice, length) 75 | for key, r := range result { 76 | r.ID, r.Val, r.Checked = fn(key) 77 | result[key] = r 78 | } 79 | return result 80 | case func(string, int) (string, string, bool): 81 | result := make(map[string][]fields.InputChoice) 82 | values, ok := lenType.(map[string]int) 83 | if !ok { 84 | return result 85 | } 86 | for group, length := range values { 87 | if _, ok := result[group]; !ok { 88 | result[group] = make([]fields.InputChoice, length) 89 | } 90 | for key, r := range result[group] { 91 | r.ID, r.Val, r.Checked = fn(group, key) 92 | result[group][key] = r 93 | } 94 | } 95 | return result 96 | } 97 | return nil 98 | } 99 | 100 | // splitFormNames user[name][test] 101 | func splitFormNames(s string) []string { 102 | var res []string 103 | hasLeft := false 104 | hasRight := true 105 | var val []rune 106 | for i, r := range s { 107 | if r == '[' { 108 | if hasRight && i > 0 { 109 | res = append(res, string(val)) 110 | val = []rune{} 111 | } 112 | hasLeft = true 113 | hasRight = false 114 | continue 115 | } 116 | if r == ']' { 117 | if hasLeft { 118 | res = append(res, string(val)) 119 | val = []rune{} 120 | hasLeft = false 121 | } 122 | continue 123 | } 124 | val = append(val, r) 125 | } 126 | if len(val) > 0 { 127 | res = append(res, string(val)) 128 | } 129 | return res 130 | } 131 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | package forms 20 | 21 | import ( 22 | "html/template" 23 | "strings" 24 | 25 | "github.com/coscms/forms/fields" 26 | "github.com/webx-top/validation" 27 | ) 28 | 29 | func ValidationEngine(valid string, f fields.FieldInterface) { 30 | //for jQuery-Validation-Engine 31 | validFuncs := strings.Split(valid, ";") 32 | var validClass string 33 | for _, v := range validFuncs { 34 | pos := strings.Index(v, "(") 35 | var fn string 36 | if pos > -1 { 37 | fn = v[0:pos] 38 | } else { 39 | fn = v 40 | } 41 | switch fn { 42 | case "required": 43 | validClass += "," + strings.ToLower(fn) 44 | case "min", "max": 45 | val := v[pos+1:] 46 | val = strings.TrimSuffix(val, ")") 47 | validClass += "," + strings.ToLower(fn) + "[" + val + "]" 48 | case "range": 49 | val := v[pos+1:] 50 | val = strings.TrimSuffix(val, ")") 51 | rangeVals := strings.SplitN(val, ",", 2) 52 | validClass += ",min[" + strings.TrimSpace(rangeVals[0]) + "],max[" + strings.TrimSpace(rangeVals[1]) + "]" 53 | case "minSize": 54 | val := v[pos+1:] 55 | val = strings.TrimSuffix(val, ")") 56 | validClass += ",minSize[" + val + "]" 57 | case "maxSize": 58 | val := v[pos+1:] 59 | val = strings.TrimSuffix(val, ")") 60 | validClass += ",maxSize[" + val + "]" 61 | case "mumeric": 62 | validClass += ",number" 63 | case "alphaNumeric": 64 | validClass += ",custom[onlyLetterNumber]" 65 | /* 66 | case "Length": 67 | validClass += ",length" 68 | case "Match": 69 | val := v[pos+1:] 70 | val = strings.TrimSuffix(val, ")") 71 | val = strings.Trim(val, "/") 72 | validClass += ",match[]" 73 | */ 74 | case "alphaDash": 75 | validClass += ",custom[onlyLetterNumber]" 76 | case "ip": 77 | validClass += ",custom[ipv4]" 78 | case "alpha", "email", "base64", "mobile", "tel", "phone": 79 | validClass += ",custom[" + strings.ToLower(fn) + "]" 80 | case "zipCode": 81 | validClass += ",custom[zip]" 82 | } 83 | } 84 | if len(validClass) > 0 { 85 | validClass = strings.TrimPrefix(validClass, ",") 86 | validClass = "validate[" + validClass + "]" 87 | f.AddClass(validClass) 88 | } 89 | } 90 | 91 | func Html5Validate(valid string, f fields.FieldInterface) { 92 | validFuncs := strings.Split(valid, ";") 93 | for _, v := range validFuncs { 94 | pos := strings.Index(v, "(") 95 | var fn string 96 | if pos > -1 { 97 | fn = v[0:pos] 98 | } else { 99 | fn = v 100 | } 101 | switch fn { 102 | case "required": 103 | f.AddTag(strings.ToLower(fn)) 104 | case "min", "max": 105 | val := v[pos+1:] 106 | val = strings.TrimSuffix(val, ")") 107 | f.SetParam(strings.ToLower(fn), val) 108 | case "range": 109 | val := v[pos+1:] 110 | val = strings.TrimSuffix(val, ")") 111 | rangeVals := strings.SplitN(val, ",", 2) 112 | f.SetParam("min", strings.TrimSpace(rangeVals[0])) 113 | f.SetParam("max", strings.TrimSpace(rangeVals[1])) 114 | case "minSize": 115 | val := v[pos+1:] 116 | val = strings.TrimSuffix(val, ")") 117 | f.SetParam("data-min", val) 118 | case "maxSize": 119 | val := v[pos+1:] 120 | val = strings.TrimSuffix(val, ")") 121 | f.SetParam("maxlength", val) 122 | f.SetParam("data-max", val) 123 | case "numeric": 124 | f.SetParam("pattern", template.HTML("^\\-?\\d+(\\.\\d+)?$")) 125 | case "alphaNumeric": 126 | f.SetParam("pattern", template.HTML("^[a-zA-Z\\d]+$")) 127 | case "length": 128 | val := v[pos+1:] 129 | val = strings.TrimSuffix(val, ")") 130 | f.SetParam("pattern", ".{"+val+"}") 131 | case "match": 132 | val := v[pos+1:] 133 | val = strings.TrimSuffix(val, ")") 134 | val = strings.Trim(val, "/") 135 | f.SetParam("pattern", template.HTML(val)) 136 | 137 | case "alphaDash": 138 | f.SetParam("pattern", template.HTML(validation.DefaultRule.AlphaDash)) 139 | case "ip": 140 | f.SetParam("pattern", template.HTML(validation.DefaultRule.IPv4)) 141 | case "alpha": 142 | f.SetParam("pattern", template.HTML("^[a-zA-Z]+$")) 143 | case "email": 144 | f.SetParam("pattern", template.HTML(validation.DefaultRule.Email)) 145 | case "base64": 146 | f.SetParam("pattern", template.HTML(validation.DefaultRule.Base64)) 147 | case "mobile": 148 | f.SetParam("pattern", template.HTML(validation.DefaultRule.Mobile)) 149 | case "tel": 150 | f.SetParam("pattern", template.HTML(validation.DefaultRule.Telephone)) 151 | case "phone": 152 | f.SetParam("pattern", template.HTML(validation.DefaultRule.GetPhone())) 153 | case "zipCode": 154 | f.SetParam("pattern", template.HTML(validation.DefaultRule.ZipCode)) 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /widgets/widgets.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright 2016-present Wenhui Shen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | */ 18 | 19 | //Package widgets This package contains the base logic for the creation and rendering of field widgets. Base widgets are defined for most input fields, 20 | // both in classic and Bootstrap3 theme; custom widgets can be defined and associated to a field, provided that they implement the 21 | // WidgetInterface interface. 22 | package widgets 23 | 24 | import ( 25 | "bytes" 26 | "html/template" 27 | 28 | "github.com/coscms/forms/common" 29 | ) 30 | 31 | func New(t *template.Template) *Widget { 32 | return &Widget{template: t} 33 | } 34 | 35 | // Widget Simple widget object that gets executed at render time. 36 | type Widget struct { 37 | template *template.Template 38 | } 39 | 40 | // WidgetInterface defines the requirements for custom widgets. 41 | type WidgetInterface interface { 42 | Render(data interface{}) string 43 | } 44 | 45 | // Render executes the internal template and returns the result as a template.HTML object. 46 | func (w *Widget) Render(data interface{}) string { 47 | buf := bytes.NewBuffer(nil) 48 | err := w.template.ExecuteTemplate(buf, "main", data) 49 | if err != nil { 50 | return err.Error() 51 | } 52 | return buf.String() 53 | } 54 | 55 | // BaseWidget creates a Widget based on theme and inpuType parameters, both defined in the common package. 56 | func BaseWidget(theme, inputType, tmplName string) *Widget { 57 | cachedKey := theme + ", " + inputType + ", " + tmplName 58 | tmpl, err := common.GetOrSetCachedTemplate(cachedKey, func() (*template.Template, error) { 59 | fpath := common.TmplDir(theme) + "/" + theme + "/" 60 | urls := []string{common.LookupPath(fpath + "generic.html")} 61 | tpath := widgetTmpl(inputType, tmplName) 62 | urls = append(urls, common.LookupPath(fpath+tpath+".html")) 63 | return common.ParseFiles(urls...) 64 | }) 65 | if err != nil { 66 | panic(err) 67 | } 68 | tmpl.Funcs(common.TplFuncs()) 69 | return New(tmpl) 70 | } 71 | 72 | func widgetTmpl(inputType, tmpl string) (tpath string) { 73 | switch inputType { 74 | case common.BUTTON: 75 | tpath = "button" 76 | if len(tmpl) > 0 { 77 | tpath = tmpl 78 | } 79 | case common.TEXTAREA: 80 | tpath = "text/textareainput" 81 | if len(tmpl) > 0 { 82 | tpath = "text/" + tmpl 83 | } 84 | case common.PASSWORD: 85 | tpath = "text/passwordinput" 86 | if len(tmpl) > 0 { 87 | tpath = "text/" + tmpl 88 | } 89 | case common.TEXT: 90 | tpath = "text/textinput" 91 | if len(tmpl) > 0 { 92 | tpath = "text/" + tmpl 93 | } 94 | case common.CHECKBOX: 95 | tpath = "options/checkbox" 96 | if len(tmpl) > 0 { 97 | tpath = "options/" + tmpl 98 | } 99 | case common.SELECT: 100 | tpath = "options/select" 101 | if len(tmpl) > 0 { 102 | tpath = "options/" + tmpl 103 | } 104 | case common.RADIO: 105 | tpath = "options/radiobutton" 106 | if len(tmpl) > 0 { 107 | tpath = "options/" + tmpl 108 | } 109 | case common.RANGE: 110 | tpath = "number/range" 111 | if len(tmpl) > 0 { 112 | tpath = "number/" + tmpl 113 | } 114 | case common.NUMBER: 115 | tpath = "number/number" 116 | if len(tmpl) > 0 { 117 | tpath = "number/" + tmpl 118 | } 119 | case common.RESET, common.SUBMIT: 120 | tpath = "button" 121 | if len(tmpl) > 0 { 122 | tpath = tmpl 123 | } 124 | case common.DATE: 125 | tpath = "datetime/date" 126 | if len(tmpl) > 0 { 127 | tpath = "datetime/" + tmpl 128 | } 129 | case common.DATETIME: 130 | tpath = "datetime/datetime" 131 | if len(tmpl) > 0 { 132 | tpath = "datetime/" + tmpl 133 | } 134 | case common.TIME: 135 | tpath = "datetime/time" 136 | if len(tmpl) > 0 { 137 | tpath = "datetime/" + tmpl 138 | } 139 | case common.DATETIME_LOCAL: 140 | tpath = "datetime/datetime" 141 | if len(tmpl) > 0 { 142 | tpath = "datetime/" + tmpl 143 | } 144 | case common.STATIC: 145 | tpath = "static" 146 | if len(tmpl) > 0 { 147 | tpath = tmpl 148 | } 149 | case common.SEARCH, common.TEL, common.URL, common.WEEK, common.COLOR, common.EMAIL, common.FILE, common.HIDDEN, common.IMAGE, common.MONTH: 150 | fallthrough 151 | default: 152 | tpath = "input" 153 | if len(tmpl) > 0 { 154 | tpath = tmpl 155 | } 156 | } 157 | return 158 | } 159 | --------------------------------------------------------------------------------