├── .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 [![CircleCI](https://circleci.com/gh/claudetech/riot-form/tree/master.svg?style=svg)](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 | --------------------------------------------------------------------------------