├── .bowerrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.yaml
├── .gitattributes
├── .gitignore
├── .npmignore
├── LICENSE
├── Makefile
├── README.md
├── bower.json
├── circle.yml
├── dist
├── riot-form.js
├── riot-form.js.map
├── riot-form.min.js
└── riot-form.min.js.map
├── gulpfile.js
├── karma.conf.js
├── lib
├── .eslintrc.yaml
├── components
│ ├── index.js
│ ├── rf-form.tag
│ ├── rf-input.html
│ ├── rf-input.js
│ ├── rf-text-input.tag
│ └── rf-textarea-input.tag
├── config.js
├── form-builder.js
├── form.js
├── index.js
├── input-factory.js
├── inputs
│ ├── base.js
│ └── index.js
├── mixins
│ ├── index.js
│ ├── rf-base-input.js
│ └── rf-input-helpers.js
└── util.js
├── package.json
├── tests
├── fixtures
│ ├── js
│ └── simple.html
├── integration
│ ├── simple.feature
│ ├── steps
│ │ └── simple.js
│ └── support
│ │ └── webdriver-hooks.js
└── unit
│ ├── .eslintrc.yaml
│ ├── base-input_test.js
│ ├── components
│ ├── helpers.js
│ └── rf-form_test.js
│ ├── form-builder_test.js
│ ├── form_test.js
│ ├── input-factory_test.js
│ ├── inputs_test.js
│ └── util_test.js
├── tmp
└── .keep
├── webpack.config.js
└── yarn.lock
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "./tests/fixtures/components"
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.json]
12 | indent_size = 2
13 |
14 | [Makefile]
15 | indent_style = tab
16 | indent_size = 4
17 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | .gitignore
--------------------------------------------------------------------------------
/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | root: true
2 | env:
3 | node: true
4 | es6: true
5 |
6 | rules:
7 | no-debugger: 2
8 | no-dupe-args: 2
9 | no-dupe-keys: 2
10 | no-duplicate-case: 2
11 | no-ex-assign: 2
12 | no-unreachable: 2
13 | valid-typeof: 2
14 | no-fallthrough: 2
15 | quotes: [2, "single", "avoid-escape"]
16 | indent: [2, 2]
17 | comma-spacing: 2
18 | semi: [2, "never"]
19 | space-infix-ops: 2
20 | space-before-function-paren: [2, {named: "never"}]
21 | space-before-blocks: [2, "always"]
22 | new-parens: 2
23 | max-len: [2, 100, 2]
24 | no-multiple-empty-lines: [2, {max: 2}]
25 | eol-last: 2
26 | no-trailing-spaces: 2
27 | keyword-spacing: 2
28 | prefer-const: 2
29 | strict: [2, "global"]
30 | no-undef: 2
31 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | dist/* -diff
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /tests/fixtures/components
3 | /tests/fixtures/js
4 |
5 | *.jar
6 | /dist
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /tests
2 | *.jar
3 | /tmp
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Claude Tech
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SELENIUM_JAR = tmp/selenium-server-standalone.3.4.0.jar
2 | BOWER_COMPONENTS = tests/integration/pages/components
3 | NODE_MODULES = node_modules
4 | SELENIUM_URL = https://goo.gl/s4o9Vx
5 |
6 | all: test dist_build
7 |
8 | $(SELENIUM_JAR):
9 | wget $(SELENIUM_URL) -O $(SELENIUM_JAR)
10 |
11 | prepare: $(SELENIUM_JAR) $(BOWER_COMPONENTS) $(NODE_MODULES)
12 |
13 | $(NODE_MODULES):
14 | npm install
15 |
16 | $(BOWER_COMPONENTS):
17 | bower install
18 |
19 | unit:
20 | ./node_modules/.bin/gulp test
21 |
22 | lint:
23 | ./node_modules/.bin/eslint lib tests
24 |
25 | build: normal_build dist_build
26 |
27 | normal_build:
28 | ./node_modules/.bin/webpack
29 |
30 | dist_build:
31 | NODE_ENV=production ./node_modules/.bin/webpack -p
32 |
33 | selenium: prepare
34 | @echo 'Starting selenium...'
35 | @java -jar $(SELENIUM_JAR) >> tmp/selenium.log 2>&1 &
36 |
37 | kill_selenium: prepare
38 | @echo 'Stopping selenium...'
39 | @ps aux | grep $(SELENIUM_JAR) | grep -v grep | awk '{ print $$2 }' | xargs kill
40 |
41 | run_integration: normal_build
42 | ./node_modules/.bin/gulp features
43 |
44 | integration: selenium run_integration kill_selenium
45 |
46 | test: prepare lint unit integration
47 |
48 | watch:
49 | gulp watch
50 |
51 | .PHONY: test unit prepare
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # riot-form [](https://circleci.com/gh/claudetech/riot-form/tree/master)
2 |
3 | Easy forms for [riotjs](http://riotjs.com/).
4 |
5 | A set of classes and tags to generate and handle forms.
6 |
7 | ## Installation
8 |
9 | You can either include the files in [dist](./dist) or require.
10 |
11 | ### With npm
12 |
13 | ```
14 | npm install riot-form --save
15 | ```
16 |
17 | ### With bower
18 |
19 | ```
20 | bower install riot-form --save
21 | ```
22 |
23 | ## Usage
24 |
25 | ### Example with automatic rendering
26 |
27 | ```html
28 |
29 |
30 |
31 |
32 | simple.html
33 |
34 |
35 |
36 |
37 |
38 |
39 |
59 |
60 |
61 |
62 |
63 |
66 |
67 |
68 | ```
69 |
70 | ### Example with manual rendering
71 |
72 | ```html
73 |
74 |
75 |
76 |
77 | simple.html
78 |
79 |
80 |
81 |
82 |
83 |
84 |
111 |
112 |
113 |
114 |
115 |
118 |
119 |
120 | ```
121 |
122 | Note that in both cases, the `form.model` property will always be synchronized with the content of the form.
123 |
124 | ## API
125 |
126 | ### `riotForm.Form`
127 |
128 | #### `Form.prototype`
129 |
130 | * `name`: Returns the name of the form, included in the config
131 | * `inputs`: Returns all the inputs of the form as an object
132 | * `model`: Returns the model of the form
133 | * `errors`: Returns an object with the errors of the form
134 | * `valid`: Returns a boolean with `true` if the form is valid and `false` otherwise
135 |
136 | #### `Form.Builder`
137 |
138 | * `addInput`: Add an input to the form. It can be any object with a `name` and a `type` properties
139 | You can pass a `tag` as an option to change the tag that will be rendered for this input
140 | * `setModel`: Set the form model. form values will be populated with it
141 | * `build`: Construct the form and returns a `Form` instance.
142 |
143 | By default, the model will be cloned when building the form, to avoid overriding existing values.
144 | If you want the values model of the you pass to be changed, pass `{noClone: true}` to the `build`
145 | method.
146 |
147 |
148 | ## How to
149 |
150 | ### Registering new inputs
151 |
152 | You will probably want to create new inputs to fit your needs.
153 |
154 | You can easily do so by creating a subclass of `riotForm.BaseInput` and registering it as follow.
155 |
156 | With ES5
157 |
158 | ```javascript
159 | // ES5
160 | var riotForm = require('riot-form')
161 |
162 | var MyInput = riotForm.BaseInput.extend({
163 | myFunc: function () {
164 | return 'whatever you want'
165 | }
166 | })
167 |
168 | MyInput.type = 'my-type'
169 | MyInput.defaultTag = 'my-tag'
170 |
171 | riotForm.inputFactory.register(MyInput)
172 | ```
173 |
174 | or with ES6
175 |
176 | ```javascript
177 | // ES6
178 | import {BaseInput, inputFactory} from 'riot-form'
179 | class MyInput extends BaseInput {
180 | myFunc() {
181 | return 'whatever you want'
182 | }
183 | }
184 |
185 | MyInput.type = 'my-type'
186 | MyInput.defaultTag = 'my-tag'
187 |
188 | inputFactory.register(MyInput)
189 | ```
190 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "riot-form",
3 | "description": "Form inputs for RiotJS",
4 | "main": "lib/index.js",
5 | "authors": [
6 | "Daniel Perez "
7 | ],
8 | "license": "MIT",
9 | "keywords": [
10 | "riotjs",
11 | "form"
12 | ],
13 | "moduleType": [
14 | "amd",
15 | "es6",
16 | "globals",
17 | "node"
18 | ],
19 | "homepage": "https://github.com/claudetech/riotjs-form",
20 | "ignore": [
21 | "**/.*",
22 | "node_modules",
23 | "bower_components",
24 | "test",
25 | "tests"
26 | ],
27 | "dependencies": {
28 | "riot": "^3.3.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: v6.1.0
4 | environment:
5 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin"
6 |
7 | dependencies:
8 | pre:
9 | - wget -O geckodriver.tar.gz https://github.com/mozilla/geckodriver/releases/download/v0.16.1/geckodriver-v0.16.1-linux64.tar.gz
10 | - gunzip -c geckodriver.tar.gz | tar xopf -
11 | - chmod +x geckodriver && mv geckodriver /home/ubuntu/bin/
12 |
13 | override:
14 | - yarn
15 | cache_directories:
16 | - ~/.cache/yarn
17 |
18 | test:
19 | override:
20 | - yarn test
21 |
--------------------------------------------------------------------------------
/dist/riot-form.min.js:
--------------------------------------------------------------------------------
1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("riot")):"function"==typeof define&&define.amd?define(["riot"],e):"object"==typeof exports?exports.riotForm=e(require("riot")):t.riotForm=e(t.riot)}(this,function(t){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,e),o.l=!0,o.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,r){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:r})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=119)}([function(t,e){var n=t.exports={version:"2.4.0"};"number"==typeof __e&&(__e=n)},function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(t,e,n){var r=n(32)("wks"),o=n(22),u=n(1).Symbol,i="function"==typeof u;(t.exports=function(t){return r[t]||(r[t]=i&&u[t]||(i?u:o)("Symbol."+t))}).store=r},function(e,n){e.exports=t},function(t,e,n){t.exports=!n(13)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},function(t,e,n){var r=n(8),o=n(48),u=n(35),i=Object.defineProperty;e.f=n(4)?Object.defineProperty:function(t,e,n){if(r(t),e=u(e,!0),r(n),o)try{return i(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},function(t,e,n){var r=n(90),o=n(25);t.exports=function(t){return r(o(t))}},function(t,e,n){var r=n(14);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},function(t,e,n){var r=n(1),o=n(0),u=n(46),i=n(10),a=function(t,e,n){var f,s,c,l=t&a.F,p=t&a.G,d=t&a.S,h=t&a.P,y=t&a.B,m=t&a.W,v=p?o:o[e]||(o[e]={}),g=v.prototype,b=p?r:d?r[e]:(r[e]||{}).prototype;p&&(n=e);for(f in n)(s=!l&&b&&void 0!==b[f])&&f in v||(c=s?b[f]:n[f],v[f]=p&&"function"!=typeof b[f]?n[f]:y&&s?u(c,r):m&&b[f]==c?function(t){var e=function(e,n,r){if(this instanceof t){switch(arguments.length){case 0:return new t;case 1:return new t(e);case 2:return new t(e,n)}return new t(e,n,r)}return t.apply(this,arguments)};return e.prototype=t.prototype,e}(c):h&&"function"==typeof c?u(Function.call,c):c,h&&((v.virtual||(v.virtual={}))[f]=c,t&a.R&&g&&!g[f]&&i(g,f,c)))};a.F=1,a.G=2,a.S=4,a.P=8,a.B=16,a.W=32,a.U=64,a.R=128,t.exports=a},function(t,e,n){var r=n(6),o=n(21);t.exports=n(4)?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},function(t,e,n){"use strict";e.__esModule=!0,e.default=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}},function(t,e,n){"use strict";e.__esModule=!0;var r=n(72),o=function(t){return t&&t.__esModule?t:{default:t}}(r);e.default=function(){function t(t,e){for(var n=0;n
10 | * @license MIT
11 | */
12 | function r(t,e){if(t===e)return 0;for(var n=t.length,r=e.length,o=0,u=Math.min(n,r);o=0;a--)if(f[a]!==s[a])return!1;for(a=f.length-1;a>=0;a--)if(i=f[a],!d(t[i],e[i],n,r))return!1;return!0}function m(t,e,n){d(t,e,!0)&&l(t,e,n,"notDeepStrictEqual",m)}function v(t,e){if(!t||!e)return!1;if("[object RegExp]"==Object.prototype.toString.call(e))return e.test(t);try{if(t instanceof e)return!0}catch(t){}return!Error.isPrototypeOf(e)&&!0===e.call({},t)}function g(t){var e;try{t()}catch(t){e=t}return e}function b(t,e,n,r){var o;if("function"!=typeof e)throw new TypeError('"block" argument must be a function');"string"==typeof n&&(r=n,n=null),o=g(e),r=(n&&n.name?" ("+n.name+").":".")+(r?" "+r:"."),t&&!o&&l(o,n,"Missing expected exception"+r);var u="string"==typeof r,i=!t&&_.isError(o),a=!t&&o&&!n;if((i&&u&&v(o,n)||a)&&l(o,n,"Got unwanted exception"+r),t&&o&&n&&!v(o,n)||!t&&o)throw o}var _=n(118),x=Object.prototype.hasOwnProperty,O=Array.prototype.slice,w=function(){return"foo"===function(){}.name}(),j=t.exports=p,E=/\s*function\s+([^\(\s]*)\s*/;j.AssertionError=function(t){this.name="AssertionError",this.actual=t.actual,this.expected=t.expected,this.operator=t.operator,t.message?(this.message=t.message,this.generatedMessage=!1):(this.message=c(this),this.generatedMessage=!0);var e=t.stackStartFunction||l;if(Error.captureStackTrace)Error.captureStackTrace(this,e);else{var n=new Error;if(n.stack){var r=n.stack,o=a(e),u=r.indexOf("\n"+o);if(u>=0){var i=r.indexOf("\n",u+1);r=r.substring(i+1)}this.stack=r}}},_.inherits(j.AssertionError,Error),j.fail=l,j.ok=p,j.equal=function(t,e,n){t!=e&&l(t,e,n,"==",j.equal)},j.notEqual=function(t,e,n){t==e&&l(t,e,n,"!=",j.notEqual)},j.deepEqual=function(t,e,n){d(t,e,!1)||l(t,e,n,"deepEqual",j.deepEqual)},j.deepStrictEqual=function(t,e,n){d(t,e,!0)||l(t,e,n,"deepStrictEqual",j.deepStrictEqual)},j.notDeepEqual=function(t,e,n){d(t,e,!1)&&l(t,e,n,"notDeepEqual",j.notDeepEqual)},j.notDeepStrictEqual=m,j.strictEqual=function(t,e,n){t!==e&&l(t,e,n,"===",j.strictEqual)},j.notStrictEqual=function(t,e,n){t===e&&l(t,e,n,"!==",j.notStrictEqual)},j.throws=function(t,e,n){b(!0,t,e,n)},j.doesNotThrow=function(t,e,n){b(!1,t,e,n)},j.ifError=function(t){if(t)throw t};var S=Object.keys||function(t){var e=[];for(var n in t)x.call(t,n)&&e.push(n);return e}}).call(e,n(59))},function(t,e,n){"use strict";function r(){(0,i.default)(f,a)}Object.defineProperty(e,"__esModule",{value:!0}),e.defaultConfig=void 0,e.restore=r;var o=n(40),u=n(16),i=function(t){return t&&t.__esModule?t:{default:t}}(u),a={formatErrors:function(t){return t?Array.isArray(t)?t[0]:t.toString():""},processValue:function(t){return t},formatLabel:o.capitalize,formatPlaceholder:o.capitalize,makeID:function(t,e){return e+"_"+t},makeName:function(t,e){return e+"_"+t},labelClassName:"",groupClassName:"",errorClassName:"",inputContainerClassName:""},f=(0,i.default)({},a);e.defaultConfig=a,e.default=f},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}Object.defineProperty(e,"__esModule",{value:!0});var o=n(41),u=r(o),i=n(44),a=r(i),f=n(43),s=r(f),c=n(11),l=r(c),p=n(12),d=r(p),h=n(17),y=r(h),m=n(3),v=r(m),g=n(16),b=r(g),_=n(18),x=r(_),O=function(){function t(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};(0,l.default)(this,t),v.default.observable(this),(0,y.default)(e.name,"An input must have a name"),this.config=e,this.setValue(e.value||this.defaultValue,{silent:!0}),e.formName&&(this.formName=e.formName)}return(0,d.default)(t,[{key:"setValue",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=this.process(this.preProcessValue(t));n!==this._value&&(this._rawValue=t,this._value=n,this.validate(),e.silent||this.trigger("change",n),e.update&&this.trigger("change:update",n))}},{key:"validate",value:function(){this.config.validate&&(this.errors=this.config.validate(this._value))}},{key:"preProcessValue",value:function(t){return t}},{key:"isEmpty",value:function(){return!this.value}},{key:"name",get:function(){return this.config.name}},{key:"tag",get:function(){return this.config.tag||this.constructor.defaultTag}},{key:"rawValue",get:function(){return this._rawValue}},{key:"value",set:function(t){this.setValue(t)},get:function(){return this._value}},{key:"formName",set:function(t){(0,y.default)(t,"the form name cannot be empty"),this._formName=t},get:function(){return this._formName}},{key:"valid",get:function(){return this.validate(),!this.errors}},{key:"type",get:function(){return this.config.type||this.constructor.type}},{key:"defaultValue",get:function(){}},{key:"formattedErrors",get:function(){return this.config.formatErrors?this.config.formatErrors(this.errors):this.defaultFormatErrors(this.errors)}},{key:"process",get:function(){return this.config.process||this.defaultProcess}},{key:"defaultProcess",get:function(){return x.default.processValue}},{key:"defaultFormatErrors",get:function(){return x.default.formatErrors}}]),t}();e.default=O,O.extend=function(t){var e=function(t){function e(){return(0,l.default)(this,e),(0,a.default)(this,(e.__proto__||(0,u.default)(e)).apply(this,arguments))}return(0,s.default)(e,t),e}(O);return(0,b.default)(e.prototype,t),e}},function(t,e){t.exports={}},function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},function(t,e,n){t.exports={default:n(76),__esModule:!0}},function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},function(t,e){t.exports=function(t){if(void 0==t)throw TypeError("Can't call method on "+t);return t}},function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,e){t.exports=!0},function(t,e,n){var r=n(8),o=n(96),u=n(26),i=n(31)("IE_PROTO"),a=function(){},f=function(){var t,e=n(47)("iframe"),r=u.length;for(e.style.display="none",n(89).appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write("
15 |
16 |
--------------------------------------------------------------------------------
/lib/components/rf-textarea-input.tag:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | import {capitalize} from './util'
2 | import assign from 'object-assign'
3 |
4 | const defaultConfig = {
5 | formatErrors: (errors) => {
6 | if (!errors) {
7 | return ''
8 | }
9 | if (Array.isArray(errors)) {
10 | return errors[0]
11 | }
12 | return errors.toString()
13 | },
14 |
15 | processValue: (value) => value,
16 |
17 | formatLabel: capitalize,
18 | formatPlaceholder: capitalize,
19 |
20 | makeID: (inputName, formName) => `${formName}_${inputName}`,
21 | makeName: (inputName, formName) => `${formName}_${inputName}`,
22 |
23 | labelClassName: '',
24 | groupClassName: '',
25 | errorClassName: '',
26 | inputContainerClassName: ''
27 | }
28 |
29 | const config = assign({}, defaultConfig)
30 |
31 | export function restore() {
32 | assign(config, defaultConfig)
33 | }
34 |
35 | export {defaultConfig as defaultConfig}
36 |
37 | export default config
38 |
--------------------------------------------------------------------------------
/lib/form-builder.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import Form from './form'
3 | import BaseInput from './inputs/base'
4 | import inputFactory from './input-factory'
5 | import assign from 'object-assign'
6 |
7 | export default class FormBuilder {
8 | constructor(name) {
9 | assert(name, 'You must provide a name for the form')
10 | this._model = {}
11 | this._inputs = {}
12 | this._forms = {}
13 | this._name = name
14 | }
15 |
16 | addInput(input) {
17 | if (!(input instanceof BaseInput)) {
18 | input = inputFactory.create(input)
19 | }
20 | assert(input.name, 'You must provide an input name')
21 | this._inputs[input.name] = input
22 | return this
23 | }
24 |
25 | addInputs(inputs) {
26 | for (const input of inputs) {
27 | this.addInput(input)
28 | }
29 | return this
30 | }
31 |
32 | addNestedForm(form) {
33 | assert(form instanceof Form, 'A form must be instance of Form')
34 | form.name = this._name + '.' + form.name
35 | this._forms[form.name] = form
36 | return this
37 | }
38 |
39 | setModel(model) {
40 | this._model = model
41 | return this
42 | }
43 |
44 | build(config = {}) {
45 | return new Form(assign({
46 | model: this._model,
47 | inputs: this._inputs,
48 | forms: this._forms,
49 | name: this._name
50 | }, config))
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/form.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 | import assert from 'assert'
3 | import assign from 'object-assign'
4 |
5 | export default class Form {
6 | constructor(config = {}) {
7 | assert(config.name, 'A form must have a name')
8 | riot.observable(this)
9 | this._config = config
10 | this._inputs = config.inputs || {}
11 | this._forms = config.forms || {}
12 | this.model = config.model || {}
13 | this.name = config.name
14 | this._errors = {}
15 | }
16 |
17 | get name() {
18 | const nameList = this._name.split('.')
19 | return nameList[nameList.length - 1]
20 | }
21 |
22 | get fullName() {
23 | return this._name
24 | }
25 |
26 | get config() {
27 | return this._config
28 | }
29 |
30 | get model() {
31 | return this._model
32 | }
33 |
34 | get inputs() {
35 | return this._inputs
36 | }
37 |
38 | filterInputs(inputs, p) {
39 | const filtered = {}
40 | for (const name in inputs) {
41 | if (p(inputs[name])) {
42 | filtered[name] = inputs[name]
43 | }
44 | }
45 | return filtered
46 | }
47 |
48 | get visibleInputs() {
49 | return this.filterInputs(this.inputs, (input) => input.type !== 'hidden')
50 | }
51 |
52 | get hiddenInputs() {
53 | return this.filterInputs(this.inputs, (input) => input.type === 'hidden')
54 | }
55 |
56 | get forms() {
57 | return this._forms
58 | }
59 |
60 | get errors() {
61 | return this._errors
62 | }
63 |
64 | set model(model) {
65 | if (this.config.noClone) {
66 | this._model = model
67 | } else {
68 | this._model = assign({}, model)
69 | }
70 | this._setInputValues()
71 | this._setFormValues()
72 | }
73 |
74 | set name(name) {
75 | this._name = name
76 | for (name of Object.keys(this.inputs)) {
77 | const input = this.inputs[name]
78 | input.formName = this.fullName
79 | }
80 | }
81 |
82 | get valid() {
83 | let valid = true
84 | for (const name of Object.keys(this.inputs)) {
85 | const input = this.inputs[name]
86 | input.validate()
87 | this.errors[name] = input.errors
88 | if (input.errors) {
89 | valid = false
90 | }
91 | }
92 | return valid
93 | }
94 |
95 | get inputsCount() {
96 | return Object.keys(this.inputs).length
97 | }
98 |
99 | _setInputValues() {
100 | for (const name of Object.keys(this.inputs)) {
101 | const input = this.inputs[name]
102 | input.off('change')
103 | input.setValue(this.model[input.name], {update: true, silent: true})
104 | input.on('change', this._makeChangeHandler(input))
105 | }
106 | }
107 |
108 | _setFormValues() {
109 | for (const name of Object.keys(this.forms)) {
110 | const form = this.forms[name]
111 | form.off('change')
112 | form.model = this.model[form.name]
113 | form.on('change', this._makeFormChangeHandler(form))
114 | }
115 | }
116 |
117 | _makeChangeHandler(input) {
118 | return (value) => {
119 | this.model[input.name] = value
120 | this.errors[input.name] = input.errors
121 | this.trigger('change', input.name, value)
122 | }
123 | }
124 |
125 | _makeFormChangeHandler(form) {
126 | return (inputName, value) => {
127 | this.model[form.name] = form.model
128 | this.errors[form.name] = form.errors
129 | this.trigger('change', form.name + '.' + inputName, form.model)
130 | }
131 | }
132 |
133 | eachInput(f) {
134 | for (const name of Object.keys(this.inputs)) {
135 | f(this.inputs[name], name)
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | import assign from 'object-assign'
2 |
3 | import config from './config'
4 | import Form from './form'
5 | import FormBuilder from './form-builder'
6 | import inputs from './inputs'
7 | import inputFactory from './input-factory'
8 | import BaseInput from './inputs/base'
9 |
10 | Form.Builder = FormBuilder
11 |
12 | for (const input of Object.keys(inputs)) {
13 | inputFactory.register(inputs[input])
14 | }
15 |
16 | export function configure(conf) {
17 | assign(config, conf)
18 | }
19 |
20 | export {Form as Form}
21 | export {inputFactory as inputFactory}
22 | export {inputs as inputs}
23 | export {BaseInput as BaseInput}
24 | export {config as config}
25 |
26 | import './components'
27 | import './mixins'
28 |
29 | export default {
30 | configure: configure,
31 | Form: Form,
32 | inputFactory: inputFactory,
33 | inputs: inputs,
34 | BaseInput: BaseInput,
35 | config: config
36 | }
37 |
--------------------------------------------------------------------------------
/lib/input-factory.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import BaseInput from './inputs/base'
3 |
4 | class InputFactory {
5 | constructor() {
6 | this._inputs = {}
7 | }
8 |
9 | get inputs() {
10 | return this._inputs
11 | }
12 |
13 | create(config = {}) {
14 | assert(config.type, 'An input needs a type')
15 | const Input = this.inputs[config.type]
16 | assert(Input, `No input available for type ${config.type}`)
17 | return new Input(config)
18 | }
19 |
20 | register(input = {}) {
21 | assert(input.type, `no type found for input ${input}`)
22 | assert(input.defaultTag, 'Input should have a defaultTag property')
23 | assert(input.prototype instanceof BaseInput, 'Input should be a subclass of BaseInput')
24 | this.inputs[input.type] = input
25 | }
26 |
27 | unregisterAll() {
28 | this._inputs = {}
29 | }
30 | }
31 |
32 | export default new InputFactory()
33 |
--------------------------------------------------------------------------------
/lib/inputs/base.js:
--------------------------------------------------------------------------------
1 | import assert from 'assert'
2 | import riot from 'riot'
3 | import assign from 'object-assign'
4 | import globalConfig from '../config'
5 |
6 | export default class BaseInput {
7 | constructor(config = {}) {
8 | riot.observable(this)
9 | assert(config.name, 'An input must have a name')
10 | this.config = config
11 | this.setValue(config.value || this.defaultValue, {silent: true})
12 | if (config.formName) {
13 | this.formName = config.formName
14 | }
15 | }
16 |
17 | get name() {
18 | return this.config.name
19 | }
20 |
21 | get tag() {
22 | return this.config.tag || this.constructor.defaultTag
23 | }
24 |
25 | get rawValue() {
26 | return this._rawValue
27 | }
28 |
29 | set value(value) {
30 | this.setValue(value)
31 | }
32 |
33 | setValue(rawValue, options = {}) {
34 | const value = this.process(this.preProcessValue(rawValue))
35 | if (value === this._value) {
36 | return
37 | }
38 | this._rawValue = rawValue
39 | this._value = value
40 | this.validate()
41 | if (!options.silent) {
42 | this.trigger('change', value)
43 | }
44 | if (options.update) {
45 | this.trigger('change:update', value)
46 | }
47 | }
48 |
49 | set formName(name) {
50 | assert(name, 'the form name cannot be empty')
51 | this._formName = name
52 | }
53 |
54 | get formName() {
55 | return this._formName
56 | }
57 |
58 | get value() {
59 | return this._value
60 | }
61 |
62 | get valid() {
63 | this.validate()
64 | return !this.errors
65 | }
66 |
67 | get type() {
68 | return this.config.type || this.constructor.type
69 | }
70 |
71 | get defaultValue() {
72 | return undefined
73 | }
74 |
75 | // TODO: pre pack some validators to avoid having to pass a callback
76 | validate() {
77 | if (this.config.validate) {
78 | this.errors = this.config.validate(this._value)
79 | }
80 | }
81 |
82 | get formattedErrors() {
83 | if (this.config.formatErrors) {
84 | return this.config.formatErrors(this.errors)
85 | }
86 | return this.defaultFormatErrors(this.errors)
87 | }
88 |
89 | preProcessValue(value) {
90 | return value
91 | }
92 |
93 | // TODO: pre pack some processors to avoid having to pass a callback
94 | get process() {
95 | return this.config.process || this.defaultProcess
96 | }
97 |
98 | get defaultProcess() {
99 | return globalConfig.processValue
100 | }
101 |
102 | get defaultFormatErrors() {
103 | return globalConfig.formatErrors
104 | }
105 |
106 | isEmpty() {
107 | return !this.value
108 | }
109 | }
110 |
111 | BaseInput.extend = function (props) {
112 | class Input extends BaseInput {}
113 | assign(Input.prototype, props)
114 | return Input
115 | }
116 |
--------------------------------------------------------------------------------
/lib/inputs/index.js:
--------------------------------------------------------------------------------
1 | import BaseInput from './base'
2 | import {padLeft} from '../util'
3 |
4 | class TextInput extends BaseInput {
5 | }
6 | TextInput.defaultTag = 'rf-text-input'
7 | TextInput.type = 'text'
8 |
9 | class EmailInput extends BaseInput {
10 | }
11 | EmailInput.defaultTag = 'rf-text-input'
12 | EmailInput.type = 'email'
13 |
14 | class PasswordInput extends BaseInput {
15 | }
16 | PasswordInput.defaultTag = 'rf-text-input'
17 | PasswordInput.type = 'password'
18 |
19 | class NumberInput extends BaseInput {
20 | preProcessValue(value) {
21 | return parseFloat(value)
22 | }
23 |
24 | isEmpty() {
25 | return isNaN(this.value)
26 | }
27 | }
28 | NumberInput.defaultTag = 'rf-text-input'
29 | NumberInput.type = 'number'
30 |
31 | class URLInput extends BaseInput {
32 | }
33 | URLInput.defaultTag = 'rf-text-input'
34 | URLInput.type = 'url'
35 |
36 | class TelInput extends BaseInput {
37 | }
38 | TelInput.defaultTag = 'rf-text-input'
39 | TelInput.type = 'tel'
40 |
41 |
42 | class DateInput extends BaseInput {
43 | preProcessValue(value) {
44 | const timestamp = Date.parse(value)
45 | if (!timestamp) {
46 | return value
47 | }
48 | const date = new Date(timestamp)
49 | return [
50 | date.getFullYear(),
51 | padLeft((date.getMonth() + 1).toString(), 2, '0'),
52 | padLeft(date.getDate().toString(), 2, '0')
53 | ].join('-')
54 | }
55 | }
56 | DateInput.defaultTag = 'rf-text-input'
57 | DateInput.type = 'date'
58 |
59 | class TextareaInput extends BaseInput {
60 | }
61 | TextareaInput.defaultTag = 'rf-textarea-input'
62 | TextareaInput.type = 'textarea'
63 |
64 | class HiddenInput extends BaseInput {
65 | }
66 | HiddenInput.defaultTag = 'rf-text-input'
67 | HiddenInput.type = 'hidden'
68 |
69 |
70 | export default {
71 | TextInput : TextInput,
72 | EmailInput : EmailInput,
73 | PasswordInput : PasswordInput,
74 | NumberInput : NumberInput,
75 | URLInput : URLInput,
76 | TelInput : TelInput,
77 | DateInput : DateInput,
78 | TextareaInput : TextareaInput,
79 | HiddenInput : HiddenInput
80 | }
81 |
--------------------------------------------------------------------------------
/lib/mixins/index.js:
--------------------------------------------------------------------------------
1 | import './rf-input-helpers'
2 | import './rf-base-input'
3 |
--------------------------------------------------------------------------------
/lib/mixins/rf-base-input.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 | import config from '../config'
3 |
4 | riot.mixin('rf-base-input', {
5 | init: function () {
6 | let tag = null
7 | let currentValue = null
8 |
9 | const makeData = () => {
10 | return { model: this.opts.model, formName: this.opts.formName }
11 | }
12 |
13 | this.on('mount', () => {
14 | const input = this.root.querySelector('[rf-input-elem]')
15 | if (!input) {
16 | throw new Error('element with attribute rf-input-elem not found in rf-input html')
17 | }
18 | tag = riot.mount(input, this.opts.model.tag, makeData())[0]
19 | })
20 |
21 | this.on('update', () => {
22 | if (tag && this.opts.model.value !== currentValue) {
23 | currentValue = this.opts.model.value
24 | tag.update(makeData())
25 | }
26 | })
27 | }
28 | })
29 |
--------------------------------------------------------------------------------
/lib/mixins/rf-input-helpers.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 | import config from '../config'
3 |
4 | riot.mixin('rf-input-helpers', {
5 | init: function () {
6 | this.currentValue = this.opts.model.value
7 | },
8 | getID: function () {
9 | return this.getProperty('inputId') ||
10 | config.makeID(this.opts.model.name, this.getFormName())
11 | },
12 | getName: function () {
13 | return this.getProperty('inputName') ||
14 | config.makeName(this.opts.model.name, this.getFormName())
15 | },
16 | getLabel: function () {
17 | return this.getProperty('inputLabel') ||
18 | config.formatLabel(this.opts.model.name, this.getFormName())
19 | },
20 | getPlaceholder: function () {
21 | return this.getProperty('inputPlaceholder') ||
22 | config.formatPlaceholder(this.opts.model.name, this.getFormName())
23 | },
24 | formatErrors: function (errors) {
25 | return config.formatErrors(errors, this.opts.model.name, this.getFormName())
26 | },
27 | getLabelClassName: function () {
28 | return this.getProperty('labelClassName') || config.labelClassName
29 | },
30 | getGroupClassName: function () {
31 | return this.getProperty('groupClassName') || config.groupClassName
32 | },
33 | getErrorClassName: function () {
34 | return this.getProperty('errorClassName') || config.errorClassName
35 | },
36 | getInputContainerClassName: function () {
37 | return this.getProperty('inputContainerClassName') || config.inputContainerClassName
38 | },
39 | getProperty: function (propertyName) {
40 | return this.opts[propertyName] || this.opts.model.config[propertyName]
41 | },
42 | assignValue: function (value) {
43 | this.opts.model.setValue(value)
44 | },
45 | getFormName: function () {
46 | return this.opts.formName || this.opts.model.formName
47 | },
48 | valueIs: function (value) {
49 | return this.opts.model.value === value
50 | },
51 | handleChange: function (e) {
52 | this.assignValue(e.target.value)
53 | },
54 | initializeValue: function () {
55 | this.on('mount', () => {
56 | const input = this.root.querySelector(`[name="${this.getName()}"]`)
57 | if (input) {
58 | input.value = this.opts.model.value || ''
59 | }
60 | this.opts.model.on('change:update', () => {
61 | input.value = this.opts.model.value || ''
62 | })
63 | })
64 | }
65 | })
66 |
--------------------------------------------------------------------------------
/lib/util.js:
--------------------------------------------------------------------------------
1 | export function capitalize(str) {
2 | if (!str) {
3 | return ''
4 | }
5 | return str[0].toUpperCase() + str.substring(1)
6 | }
7 |
8 | export function padLeft(str, length, char = ' ') {
9 | while (str.length < length) {
10 | str = char + str
11 | }
12 | return str
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "riot-form",
3 | "version": "0.2.0",
4 | "description": "Form inputs for RiotJS",
5 | "main": "dist/riot-form.js",
6 | "scripts": {
7 | "test": "make -k test",
8 | "prepublish": "make build"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/claudetech/riot-form.git"
13 | },
14 | "keywords": [
15 | "riotjs",
16 | "form"
17 | ],
18 | "author": "Daniel Perez ",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/claudetech/riot-form/issues"
22 | },
23 | "homepage": "https://github.com/claudetech/riot-form#readme",
24 | "devDependencies": {
25 | "babel": "^6.5.2",
26 | "babel-core": "^6.5.2",
27 | "babel-loader": "^7.0.0",
28 | "babel-plugin-transform-runtime": "^6.6.0",
29 | "babel-preset-es2015": "^6.5.0",
30 | "chai": "^3.5.0",
31 | "chai-as-promised": "^6.0.0",
32 | "connect": "^3.4.1",
33 | "cucumber": "^2.0.0-rc.7",
34 | "eslint": "^3.16.1",
35 | "gulp": "^3.9.1",
36 | "gulp-cucumber": "0.0.22",
37 | "html-loader": "^0.4.3",
38 | "karma": "^1.5.0",
39 | "karma-chrome-launcher": "^2.0.0",
40 | "karma-firefox-launcher": "^1.0.0",
41 | "karma-mocha": "^1.3.0",
42 | "karma-mocha-reporter": "^2.2.2",
43 | "karma-phantomjs-launcher": "^1.0.0",
44 | "karma-sourcemap-loader": "^0.3.7",
45 | "karma-webpack": "^2.0.2",
46 | "mocha": "^3.2.0",
47 | "nightwatch": "^0.9.12",
48 | "phantomjs-prebuilt": "^2.1.4",
49 | "riot": "^3.3.1",
50 | "riotjs-loader": "^4.0.0",
51 | "serve-static": "^1.10.2",
52 | "webdriverio": "^4.0.9",
53 | "webpack": "^2.2.1",
54 | "webpack-stream": "^3.1.0"
55 | },
56 | "dependencies": {
57 | "object-assign": "^4.0.1"
58 | },
59 | "peerDependencies": {
60 | "riot": "3.x"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/fixtures/js:
--------------------------------------------------------------------------------
1 | ../../dist/
--------------------------------------------------------------------------------
/tests/fixtures/simple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | simple.html
6 |
7 |
8 |
9 |
10 |
11 |
12 |
36 |
37 |
38 |
39 |
40 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/tests/integration/simple.feature:
--------------------------------------------------------------------------------
1 | Feature: Simple Feature
2 |
3 | Background:
4 | Given I visit the simple test page
5 |
6 | Scenario: I view the page
7 | Then I should see username "Daniel"
8 | And I should see profile "My name is Daniel"
9 |
10 | Scenario: I enter information
11 | When I enter username "Bob"
12 | And I enter profile "I like to fish"
13 | Then I should see username "Bob"
14 | And I should see profile "I like to fish"
15 | When I press reset button
16 | Then I should see empty form
17 |
--------------------------------------------------------------------------------
/tests/integration/steps/simple.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chai = require('chai')
4 | const chaiAsPromised = require('chai-as-promised')
5 | chai.use(chaiAsPromised)
6 | const expect = chai.expect
7 |
8 | var cucumber = require('cucumber')
9 |
10 | cucumber.defineSupportCode(function (support) {
11 | support.Given(/^I visit the simple test page$/, function () {
12 | return this.browser.url('http://localhost:5050/simple.html')
13 | })
14 |
15 | support.When(/^I enter ([^\"]*) \"([^\"]*)\"$/, function (property, value) {
16 | return this.browser.setValue('[name="simple_' + property + '"]', value)
17 | })
18 |
19 | support.Then(/^I should see ([^\"]*) \"([^\"]*)\"$/, function (property, expected) {
20 | return Promise.all([
21 | expect(this.browser.getValue('[name="simple_' + property + '"]')).to.eventually.eq(expected),
22 | expect(this.browser.getText('.' + property)).to.eventually.eq(expected)
23 | ])
24 | })
25 |
26 | support.When(/^I press reset button$/, function () {
27 | return this.browser.click('#reset-button')
28 | })
29 |
30 | support.Then(/^I should see empty form$/, function () {
31 | return expect(this.browser.getValue('[name="simple_username"]')).to.eventually.eq('')
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/tests/integration/support/webdriver-hooks.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const WebdriverIO = require('webdriverio')
4 | const cucumber = require('cucumber')
5 |
6 | const client = WebdriverIO.remote({
7 | host: 'localhost',
8 | desiredCapabilities: {
9 | browserName: 'firefox'
10 | }
11 | })
12 |
13 | const promised = client.init()
14 |
15 | cucumber.defineSupportCode(function (support) {
16 | support.Before(function () {
17 | this.browser = client
18 | return promised
19 | })
20 |
21 | support.registerHandler('AfterFeatures', function (event, callback) {
22 | client.end().then(() => callback()).catch(callback)
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/tests/unit/.eslintrc.yaml:
--------------------------------------------------------------------------------
1 | extends: ../../.eslintrc.yaml
2 | env:
3 | mocha: true
4 | browser: true
5 |
6 | parserOptions:
7 | sourceType: module
8 |
--------------------------------------------------------------------------------
/tests/unit/base-input_test.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai'
2 | import {BaseInput} from 'riot-form'
3 |
4 | class DummyInput extends BaseInput {
5 | get defaultValue() {
6 | return 'dummy'
7 | }
8 | }
9 | DummyInput.defaultTag = 'dummy-tag'
10 |
11 | describe('BaseInput', () => {
12 | it('should throw error when no name present', () => {
13 | expect(() => new DummyInput({})).to.throw(Error)
14 | })
15 |
16 | describe('new instances', () => {
17 | it('should accept value', () => {
18 | const input = new DummyInput({name: 'hello', value: 'foobar'})
19 | expect(input.value).to.eq('foobar')
20 | })
21 |
22 | it('should set value to default value', () => {
23 | const input = new DummyInput({name: 'hello'})
24 | expect(input.value).to.eq('dummy')
25 | })
26 | })
27 |
28 | describe('name', () => {
29 | it('should return config name', () => {
30 | const input = new DummyInput({name: 'hello'})
31 | expect(input.name).to.eq('hello')
32 | })
33 | })
34 |
35 | describe('value', () => {
36 | it('should trigger event on value changed', () => {
37 | const input = new DummyInput({name: 'hello'})
38 | let called = false
39 | const value = 'new value'
40 | input.on('change', (v) => {
41 | called = true
42 | expect(v).to.eq(value)
43 | })
44 | input.value = value
45 | expect(called).to.be.true
46 | })
47 | })
48 |
49 | describe('tag', () => {
50 | it('should return default tag when no tag given', () => {
51 | const input = new DummyInput({name: 'hello'})
52 | expect(input.tag).to.eq('dummy-tag')
53 | })
54 |
55 | it('should return config tag when given', () => {
56 | const input = new DummyInput({name: 'hello', tag: 'custom-tag'})
57 | expect(input.tag).to.eq('custom-tag')
58 | })
59 | })
60 |
61 | describe('process', () => {
62 | it('should be called when present', () => {
63 | const input = new DummyInput({
64 | name: 'hello',
65 | process: (v) => v.trim()
66 | })
67 | input.value = ' hello '
68 | expect(input.value).to.eq('hello')
69 | })
70 | })
71 |
72 | describe('rawValue', () => {
73 | it('should not be processed', () => {
74 | const input = new DummyInput({
75 | name: 'hello',
76 | process: (v) => v.trim()
77 | })
78 | const v = ' hello '
79 | input.value = v
80 | expect(input.rawValue).to.eq(v)
81 | })
82 | })
83 |
84 | describe('validate', () => {
85 | it('should be called when present', () => {
86 | const errors = ['should contain only numbers']
87 | const input = new DummyInput({
88 | name: 'hello',
89 | validate: (v) => /^[0-9]+/.exec(v) || errors
90 | })
91 | input.value = 'hello'
92 | expect(input.errors).to.deep.eq(errors)
93 | })
94 | })
95 |
96 | describe('formattedErrors', () => {
97 | it('should return error when it is string', () => {
98 | const errors = 'should contain only numbers'
99 | const input = new DummyInput({
100 | name: 'hello',
101 | validate: (_v) => errors
102 | })
103 | input.validate()
104 | expect(input.formattedErrors).to.eq(errors)
105 | })
106 |
107 | it('should return the first error when it is an array', () => {
108 | const errors = ['first', 'second']
109 | const input = new DummyInput({
110 | name: 'hello',
111 | validate: (_v) => errors
112 | })
113 | input.validate()
114 | expect(input.formattedErrors).to.eq(errors[0])
115 | })
116 |
117 | it('should use the formatErrors function when present', () => {
118 | const errors = ['first', 'second']
119 | const input = new DummyInput({
120 | name: 'hello',
121 | formatErrors: (err) => `** ${err[0]} **`,
122 | validate: (_v) => errors
123 | })
124 | input.validate()
125 | expect(input.formattedErrors).to.eq(`** ${errors[0]} **`)
126 | })
127 | })
128 |
129 | describe('.extends', () => {
130 | const MyInput = BaseInput.extend({
131 | myFunc: function () {
132 | return 'myResult'
133 | }
134 | })
135 | MyInput.type = 'mine'
136 | MyInput.defaultTag = 'the-best-tag-ever'
137 |
138 | it('should enforce a name', () => {
139 | expect(() => new MyInput({})).to.throw(Error)
140 | })
141 |
142 | it('should have input properties', () => {
143 | const input = new MyInput({name: 'hello'})
144 | expect(input.name).to.eq('hello')
145 | expect(input.tag).to.eq('the-best-tag-ever')
146 | })
147 |
148 | it('should have defined functions', () => {
149 | const input = new MyInput({name: 'hello'})
150 | expect(input.myFunc()).to.eq('myResult')
151 | })
152 | })
153 | })
154 |
--------------------------------------------------------------------------------
/tests/unit/components/helpers.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 |
3 | export function mountTag(name, api = {}) {
4 | const elem = document.createElement(name)
5 | document.body.appendChild(elem)
6 | return riot.mount(name, api)[0]
7 | }
8 |
--------------------------------------------------------------------------------
/tests/unit/components/rf-form_test.js:
--------------------------------------------------------------------------------
1 | import riot from 'riot'
2 | import {expect} from 'chai'
3 | import {Form} from 'riot-form'
4 | import {mountTag} from './helpers'
5 |
6 | describe('rf-form', () => {
7 | const form = new Form.Builder('hello')
8 | .addInput({name: 'username', type: 'text'})
9 | .addInput({name: 'other', type: 'text'})
10 | .addInput({
11 | name: 'customized',
12 | type: 'text',
13 | inputLabel: 'Customized label',
14 | inputPlaceholder: 'Customized placeholder'
15 | })
16 | .addInput({name: 'hideme', type: 'hidden'})
17 | .setModel({username: 'world', hideme: 'i-am-hidden'})
18 | .build()
19 |
20 | before(() => {
21 | mountTag('rf-form', {
22 | model: form,
23 | className: 'custom-class'
24 | })
25 | })
26 |
27 | it('should display form', () => {
28 | const form = document.querySelector('form[name="hello"]')
29 | expect(form).not.to.be.null
30 | expect(form.className).to.eq('custom-class')
31 | })
32 |
33 | it('should render the label', () => {
34 | const label = document.querySelector('label[for="hello_username"]')
35 | expect(label).not.to.be.null
36 | expect(label.innerText || label.textContent).to.eq('Username')
37 | })
38 |
39 | it('should allow to customize labels', () => {
40 | const label = document.querySelector('label[for="hello_customized"]')
41 | expect(label).not.to.be.null
42 | expect(label.innerText || label.textContent).to.eq('Customized label')
43 | })
44 |
45 | it('should render the input', () => {
46 | const input = document.querySelector('input[name="hello_username"]')
47 | expect(input).not.to.be.null
48 | expect(input.placeholder).to.eq('Username')
49 | expect(input.value).to.eq('world')
50 | })
51 |
52 | it('should allow to customize placeholders', () => {
53 | const input = document.querySelector('input[name="hello_customized"]')
54 | expect(input).not.to.be.null
55 | expect(input.placeholder).to.eq('Customized placeholder')
56 | })
57 |
58 | it('should render hidden inputs without label', () => {
59 | const input = document.querySelector('input[name="hello_hideme"]')
60 | expect(input).not.to.be.null
61 | expect(input.value).to.eq('i-am-hidden')
62 | const label = document.querySelector('label[for="hello_hideme"]')
63 | expect(label).to.be.null
64 | })
65 |
66 | it('should not set undefined initial values', () => {
67 | const input = document.querySelector('input[name="hello_other"]')
68 | expect(input).not.to.be.null
69 | expect(input.value).to.eq('')
70 | })
71 | })
72 |
--------------------------------------------------------------------------------
/tests/unit/form-builder_test.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai'
2 | import {Form, inputs} from 'riot-form'
3 |
4 | describe('FormBuilder', () => {
5 | it('should build a form', () => {
6 | expect(new Form.Builder('hello').build()).to.be.instanceof(Form)
7 | })
8 |
9 | it('should throw when no named passed', () => {
10 | expect(() => new Form.Builder()).to.throw(Error)
11 | })
12 |
13 | it('should throw when input do not have a name', () => {
14 | const builder = new Form.Builder('whatever')
15 | expect(() => builder.addInput({type: 'text'})).to.throw(Error)
16 | })
17 |
18 | it('should raise on inexisting input type', () => {
19 | const builder = new Form.Builder('whatever')
20 | expect(() => builder.addInput({type: 'wtf', name: 'foo'})).to.throw(Error)
21 | })
22 |
23 | describe('setModel', () => {
24 | it('should set model', () => {
25 | const form = new Form.Builder('foo')
26 | .setModel({name: 'foo'})
27 | .build()
28 | expect(form.model.name).to.eq('foo')
29 | })
30 | })
31 |
32 | describe('addInput', () => {
33 | it('should add input from plain objects', () => {
34 | const name = 'hello'
35 | const form = new Form.Builder('foo')
36 | .addInput({name: name, type: 'text'})
37 | .build()
38 | expect(form.inputsCount).to.eq(1)
39 | expect(form.inputs.hello.name).to.eq(name)
40 | expect(form.inputs.hello.formName).to.eq('foo')
41 | })
42 |
43 | it('should add input from input objects', () => {
44 | const form = new Form.Builder('foo')
45 | .addInput(new inputs.TextInput({name: 'hello'}))
46 | .build()
47 | expect(form.inputsCount).to.eq(1)
48 | expect(form.inputs.hello.name).to.eq('hello')
49 | expect(form.inputs.hello.formName).to.eq('foo')
50 | })
51 | })
52 |
53 | describe('addInputs', () => {
54 | it('should add multiple inputs', () => {
55 | const form = new Form.Builder('foo')
56 | .addInputs([
57 | {name: 'foo', type: 'text'},
58 | {name: 'bar', type: 'text'}
59 | ])
60 | .build()
61 |
62 | expect(form.inputsCount).to.eq(2)
63 | expect(form.inputs.foo.name).to.eq('foo')
64 | })
65 | })
66 |
67 | describe('addNestedForm', () => {
68 | it('should throw when form is not Form instance', () => {
69 | const builder = new Form.Builder('foo')
70 | expect(() => builder.addNestedForm({name: 'hello'})).to.throw(Error)
71 | })
72 |
73 |
74 | it('should add nested form', () => {
75 | const form = new Form.Builder('foo')
76 | .addNestedForm(
77 | new Form.Builder('bar')
78 | .addInput({name: 'baz', type: 'text'})
79 | .build())
80 | .build()
81 | expect(form.forms.bar.inputsCount).to.eq(1)
82 | expect(form.forms.bar.name).to.eq('bar')
83 | expect(form.forms.bar.fullName).to.eq('foo.bar')
84 | expect(form.forms.bar.inputs.baz.name).to.eq('baz')
85 | expect(form.forms.bar.inputs.baz.formName).to.eq('foo.bar')
86 | })
87 | })
88 |
89 | it('should create a form with value prefilled', () => {
90 | const form = new Form.Builder('foo')
91 | .addInput({name: 'name', type: 'text'})
92 | .setModel({name: 'Daniel'})
93 | .build()
94 |
95 | expect(form.inputs.name.value).to.eq('Daniel')
96 | expect(form.model).to.deep.eq({name: 'Daniel'})
97 | })
98 | })
99 |
--------------------------------------------------------------------------------
/tests/unit/form_test.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai'
2 | import {Form, inputs} from 'riot-form'
3 |
4 | describe('Form', () => {
5 | it('should be observable', () => {
6 | expect(new Form({name: 'hello'}).on).to.be.a('function')
7 | })
8 |
9 | describe('inputs', () => {
10 | it('should be undefined when input does not exist', () => {
11 | const form = new Form.Builder('foo').build()
12 | expect(form.inputs.whatever).to.be.undefined
13 | })
14 |
15 | it('should return input when present', () => {
16 | const input = new inputs.TextInput({name: 'foo'})
17 | const form = new Form.Builder('foo').addInput(input).build()
18 | expect(form.inputs.foo).to.eq(input)
19 | })
20 | })
21 |
22 | describe('forms', () => {
23 | it('should be undefined when form does not exist', () => {
24 | const form = new Form.Builder('foo').build()
25 | expect(form.forms.whatever).to.be.undefined
26 | })
27 |
28 | it('should return form when present', () => {
29 | const nestedForm = new Form.Builder('bar').build()
30 | const form = new Form.Builder('foo').addNestedForm(nestedForm).build()
31 | expect(form.forms.bar).to.be.eq(nestedForm)
32 | })
33 | })
34 |
35 | it('should synchronize inputs', () => {
36 | const input = new inputs.TextInput({name: 'foo'})
37 | const form = new Form.Builder('foo').addInput(input).build()
38 | expect(form.model.foo).to.be.undefined
39 | input.value = 'bar'
40 | expect(form.model.foo).to.eq('bar')
41 | })
42 |
43 | it('should set input initial value', () => {
44 | const input = new inputs.TextInput({name: 'foo'})
45 | const form = new Form.Builder('foo').setModel({foo: 'bar'}).addInput(input).build()
46 | expect(form.model.foo).to.eq('bar')
47 | expect(input.value).to.eq('bar')
48 | })
49 |
50 | it('should set form initial value', () => {
51 | const nestedForm = new Form.Builder('bar').build()
52 | const form = new Form.Builder('foo').setModel({bar: {name: 'baz'}})
53 | .addNestedForm(nestedForm).build()
54 | expect(form.model.bar).to.deep.eq({name: 'baz'})
55 | expect(nestedForm.model).to.deep.eq({name:'baz'})
56 | expect(nestedForm.model.name).to.eq('baz')
57 | })
58 |
59 | describe('model setter', () => {
60 | it('should update model', () => {
61 | const form = new Form.Builder('foo').setModel({foo: 'bar'}).build()
62 | expect(form.model.foo).to.eq('bar')
63 | form.model = { foo: 'baz' }
64 | expect(form.model.foo).to.eq('baz')
65 | })
66 |
67 | it('should update input values', () => {
68 | const input = new inputs.TextInput({name: 'foo'})
69 | const form = new Form.Builder('foo').setModel({foo: 'bar'}).addInput(input).build()
70 | expect(input.value).to.eq('bar')
71 | form.model = { foo: 'baz' }
72 | expect(input.value).to.eq('baz')
73 | })
74 |
75 | it('should update form models', () => {
76 | const nestedForm = new Form.Builder('bar').build()
77 | const form = new Form.Builder('foo').setModel({bar: {name: 'bar'}})
78 | .addNestedForm(nestedForm).build()
79 | expect(nestedForm.model).to.deep.eq({name:'bar'})
80 | form.model = {bar: {name: 'baz'}}
81 | expect(nestedForm.model).to.deep.eq({name:'baz'})
82 | })
83 | })
84 |
85 | describe('errors', () => {
86 | it('should return input errors', () => {
87 | const errors = ['I have an error']
88 | const input = new inputs.TextInput({
89 | name: 'foo',
90 | validate: (v) => errors
91 | })
92 | const form = new Form.Builder('foo').addInput(input).build()
93 | input.value = 'bar'
94 | expect(form.model.foo).to.eq('bar')
95 | expect(form.errors.foo).to.deep.eq(errors)
96 | })
97 | })
98 |
99 | describe('valid', () => {
100 | it('should run input validators', () => {
101 | const errors = ['I have an error']
102 | const input = new inputs.TextInput({
103 | name: 'foo',
104 | validate: (v) => errors
105 | })
106 | const form = new Form.Builder('foo').addInput(input).build()
107 | expect(form.errors.foo).to.be.undefined
108 | expect(form.valid).to.be.false
109 | expect(form.errors.foo).to.deep.eq(errors)
110 | })
111 | })
112 | })
113 |
--------------------------------------------------------------------------------
/tests/unit/input-factory_test.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai'
2 | import {inputFactory, BaseInput} from 'riot-form'
3 |
4 | class DummyInput extends BaseInput {}
5 | DummyInput.type = 'dummy'
6 | DummyInput.defaultTag = 'dummy'
7 |
8 | describe('inputFactory', () => {
9 | beforeEach(() => inputFactory.unregisterAll())
10 |
11 | describe('register', () => {
12 | it('should throw when no type given', () => {
13 | expect(() => inputFactory.register()).to.throw(Error)
14 | })
15 |
16 | it('should throw when no defaultTag given', () => {
17 | expect(() => inputFactory.register({ type: 'whatever' })).to.throw(Error)
18 | })
19 |
20 | it('should throw when not instance of BaseInput', () => {
21 | const props = { type: 'whatever', defaultTag: 'something' }
22 | expect(() => inputFactory.register(props)).to.throw(Error)
23 | })
24 |
25 | it('should register input', () => {
26 | expect(inputFactory.inputs.dummy).to.be.undefined
27 | inputFactory.register(DummyInput)
28 | expect(inputFactory.inputs.dummy).not.to.be.undefined
29 | })
30 | })
31 |
32 | describe('create', () => {
33 | it('should throw when no type given', () => {
34 | expect(() => inputFactory.create({name: 'hello'})).to.throw(Error)
35 | })
36 |
37 | it('should throw when input not available', () => {
38 | expect(() => inputFactory.create({name: 'hello', type: 'whatever'})).to.throw(Error)
39 | })
40 |
41 | it('should return input instance when it exists', () => {
42 | inputFactory.register(DummyInput)
43 | const input = inputFactory.create({type: 'dummy', name: 'hello'})
44 | expect(input).to.be.instanceof(DummyInput)
45 | expect(input.name).to.eq('hello')
46 | })
47 | })
48 | })
49 |
--------------------------------------------------------------------------------
/tests/unit/inputs_test.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai'
2 | import {inputFactory} from 'riot-form'
3 |
4 | describe('inputs', () => {
5 | const inputTypes = [
6 | 'text',
7 | 'password',
8 | 'email',
9 | 'tel',
10 | 'number',
11 | 'url',
12 | 'date',
13 | 'textarea'
14 | ]
15 |
16 | it('should contain all inputs', () => {
17 | for (const inputType of inputTypes) {
18 | expect(() => inputFactory.create({name: 'hello', type: inputType})).not.to.throw(Error)
19 | }
20 | })
21 |
22 | describe('DateInput', () => {
23 | describe('preProcessValue', () => {
24 | it('should return a formatted date', () => {
25 | const input = inputFactory.create({name: 'whatever', type: 'date'})
26 | expect(input.preProcessValue('2016-08-06T06:15:38.864Z')).to.eq('2016-08-06')
27 | expect(input.preProcessValue('2016-08-31T06:15:38.864Z')).to.eq('2016-08-31')
28 | })
29 | })
30 | })
31 |
32 | describe('NumberInput', () => {
33 | describe('preProcessValue', () => {
34 | it('should return a number', () => {
35 | const input = inputFactory.create({name: 'whatever', type: 'number'})
36 | expect(input.preProcessValue('1')).to.eq(1)
37 | expect(input.preProcessValue('1.1')).to.eq(1.1)
38 | })
39 | })
40 | })
41 | })
42 |
--------------------------------------------------------------------------------
/tests/unit/util_test.js:
--------------------------------------------------------------------------------
1 | import {expect} from 'chai'
2 | import * as util from 'riot-form/util'
3 |
4 | describe('util', () => {
5 | describe('capitalize', () => {
6 | it('should capitalize the string', () => {
7 | expect(util.capitalize('foobar')).to.eq('Foobar')
8 | expect(util.capitalize('Foobar')).to.eq('Foobar')
9 | expect(util.capitalize('hellOO')).to.eq('HellOO')
10 | })
11 | })
12 |
13 | describe('padLeft', () => {
14 | it('should pad string', () => {
15 | expect(util.padLeft('1', 2, '0')).to.eq('01')
16 | expect(util.padLeft('ab', 4, 'x')).to.eq('xxab')
17 | })
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/claudetech/riot-form/1296ce64061050aae3598a076833e4714f4b5fa4/tmp/.keep
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const webpack = require('webpack')
5 |
6 | module.exports = {
7 | entry: {
8 | lib: ['./lib/index.js']
9 | },
10 | output: {
11 | path: path.join(__dirname, 'dist'),
12 | filename: process.env.NODE_ENV === 'production' ? 'riot-form.min.js' : 'riot-form.js',
13 | library: 'riotForm',
14 | libraryTarget: 'umd'
15 | },
16 | devtool: 'source-map',
17 | module: {
18 | rules: [{
19 | enforce: 'pre',
20 | test: /\.tag$/,
21 | loader: 'riotjs-loader',
22 | exclude: /node_modules/,
23 | options: { type: 'none' }
24 | }, {
25 | test: /\.js|\.tag/,
26 | exclude: /node_modules/,
27 | loader: 'babel-loader',
28 | options: {
29 | presets: ['es2015'],
30 | plugins: ['transform-runtime']
31 | }
32 | }, {test: /\.html$/, loader: 'html-loader'}]
33 | },
34 | resolve: {
35 | alias: {
36 | 'riot-form': path.join(__dirname, './lib')
37 | }
38 | },
39 | externals: {
40 | riot: 'riot'
41 | },
42 | plugins: [
43 | new webpack.ProvidePlugin({
44 | riot: 'riot'
45 | })
46 | ]
47 | }
48 |
--------------------------------------------------------------------------------