├── .gitignore ├── .npmignore ├── History.md ├── Makefile ├── Readme.md ├── component.json ├── dist ├── ive.js └── ive.min.js ├── index.js ├── lib ├── form.js ├── only.js └── validate.js ├── package.json └── test ├── index.html └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | components 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | support 2 | test 3 | examples 4 | *.sock 5 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 0.2.4 / 2015-01-11 3 | ================== 4 | 5 | * clear attributes immediately 6 | 7 | 0.2.3 / 2015-01-09 8 | ================== 9 | 10 | * bump squares 11 | 12 | 0.1.1 / 2015-01-08 13 | ================== 14 | 15 | * remove todo 16 | * support nested attributes with keys 17 | 18 | 0.1.0 / 2015-01-08 19 | ================== 20 | 21 | * rework form API 22 | 23 | 0.0.3 / 2015-01-07 24 | ================== 25 | 26 | * add valid attribute 27 | 28 | 0.0.2 / 2015-01-07 29 | ================== 30 | 31 | * return => continue 32 | * update readme 33 | * update readme 34 | * update readme 35 | * update readme 36 | * fix readme 37 | * readme cleanup 38 | 39 | 0.0.1 / 2015-01-07 40 | ================== 41 | 42 | * Initial release 43 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @node_modules/.bin/mocha 3 | 4 | browser: 5 | @node_modules/.bin/duo-serve -h test/index.html -g Schema index.js 6 | 7 | dist: components dist-build dist-minify 8 | 9 | dist-build: 10 | @mkdir -p dist/ 11 | @duo -g Ive < index.js > dist/ive.js 12 | 13 | dist-minify: dist/ive.js 14 | @curl -s \ 15 | -d compilation_level=SIMPLE_OPTIMIZATIONS \ 16 | -d output_format=text \ 17 | -d output_info=compiled_code \ 18 | --data-urlencode "js_code@$<" \ 19 | http://marijnhaverbeke.nl/uglifyjs \ 20 | > $<.tmp 21 | @mv $<.tmp dist/ive.min.js 22 | 23 | clean: 24 | @rm -rf node_modules 25 | @rm -rf components 26 | 27 | .PHONY: test browser dist 28 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # ive 3 | 4 | Isomorphic validation. Validate your forms in the browser and the form data on the server using the same schemas. 5 | 6 | ## Installation 7 | 8 | **Server:** 9 | 10 | ```bash 11 | npm install ive 12 | ``` 13 | 14 | **Browser (duo):** 15 | 16 | ```js 17 | var Ive = require('ive'); 18 | ``` 19 | 20 | **Browser (standalone):** 21 | 22 | - [ive.js](dist/ive.js) 23 | - [ive.min.js](dist/ive.min.js) 24 | 25 | **Browser (browserify):** 26 | 27 | - accepting PRs 28 | 29 | ## Features 30 | 31 | - Node & browser support 32 | - Generator-friendly 33 | - Thoughtful form validation 34 | - Data cleansing, formatting & casting 35 | - Asynchronous & custom validation 36 | - Validate against partial schemas 37 | - Informative errors 38 | - Composable schemas 39 | 40 | ## Example 41 | 42 | **user-schema.js** 43 | 44 | ```js 45 | var ive = module.exports = Ive(); 46 | 47 | ive.attr('name') 48 | .type(String) 49 | .between(2, 30) 50 | .required(true); 51 | 52 | ive.attr('phone') 53 | .assert(/^\d{10}$/) 54 | .format(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3') 55 | .required(true) 56 | 57 | ive.attr('email') 58 | .type(String) 59 | .assert(/\w+\@\w+\.\w+/) 60 | .required(true); 61 | 62 | ive.attr('age') 63 | .cast(Number) 64 | .between(18, 110) 65 | .required(true); 66 | ``` 67 | 68 | ### Browser 69 | 70 | **index.html** 71 | 72 | ```html 73 |
81 | ``` 82 | 83 | **index.js** 84 | 85 | ```js 86 | var schema = require('./user-schema'); 87 | schema(document.querySelector('.create-user')); 88 | // there is no next step! form looks for `[validate]` and 89 | // then validates the `input[name]` against Ive's schema 90 | ``` 91 | 92 | ### Server 93 | 94 | #### Express 95 | 96 | **server.js** 97 | 98 | ```js 99 | var schema = require('./user-schema'); 100 | 101 | app.post('/users', function(req, res, next) { 102 | var body = req.body; 103 | 104 | // validate 105 | schema(body, function(err, obj) { 106 | if (err) return res.send(400, { error: err }); 107 | user.create(obj, next); 108 | }); 109 | }); 110 | ``` 111 | 112 | #### Koa 113 | 114 | **server.js** 115 | 116 | ```js 117 | var schema = require('./user-schema'); 118 | 119 | app.use(_.post('/users', function *(next) { 120 | var body = this.request.body; 121 | 122 | // validate 123 | var obj = yield schema(body); 124 | yield user.create(obj); 125 | 126 | yield next; 127 | })); 128 | ``` 129 | 130 | ## API 131 | 132 | ### `Ive([attrs])` 133 | 134 | Initialize an `Ive` instance with an optional set of `attrs`. 135 | 136 | ### `ive([str], obj, [fn])` 137 | 138 | Validate `obj` against the schema, calling `fn` when the validation is complete. `fn` has the signature 139 | `function(error, val) { ... }`. `val` is the new object that may be cleansed, formatted, and cast. 140 | 141 | ```js 142 | var ive = Ive({ 143 | name: rube().type(String), 144 | email: rube().assert(/@/).type(String), 145 | age: rube().cast(Number).type(Number) 146 | }); 147 | 148 | ive({ 149 | name: 'matt', 150 | email: 'matt@lapwinglabs.com', 151 | age: '25' 152 | }, function(err, v) { 153 | assert(!err); 154 | assert('matt' == v.name); 155 | assert('matt@lapwinglabs.com' == v.email); 156 | assert(25 === v.age); 157 | done(); 158 | }); 159 | ``` 160 | 161 | If you're working with generators you can omit `fn` and Ive will return a thunk: 162 | 163 | ```js 164 | var ive = Ive({ 165 | name: rube().type(String), 166 | email: rube().assert(/@/).type(String), 167 | age: rube().cast(Number).type(Number) 168 | }); 169 | 170 | var val = yield ive({ 171 | name: 'matt', 172 | email: 'matt@lapwinglabs.com', 173 | age: '25' 174 | }); 175 | ``` 176 | 177 | You can also choose to validate against certain properties. This is useful if you're making updates 178 | to an existing document and you don't have all the properties: 179 | 180 | ```js 181 | var ive = Ive({ 182 | name: rube().type(String).required(true), 183 | email: rube().assert(/@/).type(String).required(true), 184 | age: rube().cast(Number).type(Number).required(true) 185 | }); 186 | 187 | // only validate on name and email 188 | ive('name email', { 189 | name: 'matt', 190 | email: 'matt@lapwinglabs.com' 191 | }, fn); 192 | ``` 193 | 194 | ### `ive.attr(name|ive|obj, [rube])` 195 | 196 | Add an attribute, ive instance, or object to `ive`. Optionally you may pass a `rube` instance as the key. 197 | 198 | ```js 199 | ive.attr('name', rube().required(true).type(String)); 200 | ``` 201 | 202 | If you just specify a name, ive returns a [rube](https://github.com/lapwinglabs/rube) instance. 203 | 204 | ```js 205 | ive.attr('phone') 206 | .assert(/^\d{10}$/) 207 | .format(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3') 208 | .required(true) 209 | ``` 210 | 211 | Ive schemas are composable: 212 | 213 | ```js 214 | var basic = Schema(); 215 | 216 | basic.attr('name') 217 | .type(String) 218 | .between(2, 30) 219 | .required(true); 220 | 221 | basic.attr('email') 222 | .type(String) 223 | .assert(/\w+\@\w+\.\w+/) 224 | .required(true); 225 | 226 | var admin = Schema(basic); 227 | 228 | admin.attr('password') 229 | .type(String) 230 | .between(8, 10) 231 | .assert(/[0-9]/) 232 | .required(true); 233 | ``` 234 | 235 | ## Differences to other libraries 236 | 237 | ### parsley.js 238 | 239 | - Nice for simple form validation, but it's declarative nature tends to get verbose very quickly. 240 | - No server-side support 241 | - jQuery dependency 242 | 243 | ## TODO 244 | 245 | * Better support for different form elements (radio, checkbox, etc.) 246 | * Customize the error formatting based on the environment 247 | * Subclass the generic Error 248 | 249 | ## Test 250 | 251 | Server: 252 | 253 | make test 254 | 255 | Browser: 256 | 257 | make browser 258 | 259 | ## License 260 | 261 | (The MIT License) 262 | 263 | Copyright (c) 2014 Matthew Mueller <matt@lapwinglabs.com> 264 | 265 | Permission is hereby granted, free of charge, to any person obtaining 266 | a copy of this software and associated documentation files (the 267 | 'Software'), to deal in the Software without restriction, including 268 | without limitation the rights to use, copy, modify, merge, publish, 269 | distribute, sublicense, and/or sell copies of the Software, and to 270 | permit persons to whom the Software is furnished to do so, subject to 271 | the following conditions: 272 | 273 | The above copyright notice and this permission notice shall be 274 | included in all copies or substantial portions of the Software. 275 | 276 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 277 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 278 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 279 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 280 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 281 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 282 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 283 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ive", 3 | "version": "0.2.4", 4 | "description": "isomorphic validation", 5 | "keywords": [ 6 | "schema", 7 | "object", 8 | "rube" 9 | ], 10 | "dependencies": { 11 | "tj/batch": "^0.5.1", 12 | "lapwinglabs/rube": "0.0.x", 13 | "matthewmueller/extend.js": "0.0.x", 14 | "dominicbarnes/form": "0.2.x", 15 | "component/event": "0.1.x", 16 | "component/value": "1.1.0", 17 | "matthewmueller/squares": "0.2.x" 18 | } 19 | } -------------------------------------------------------------------------------- /dist/ive.js: -------------------------------------------------------------------------------- 1 | (function outer(modules, cache, entries){ 2 | 3 | /** 4 | * Global 5 | */ 6 | 7 | var global = (function(){ return this; })(); 8 | 9 | /** 10 | * Require `name`. 11 | * 12 | * @param {String} name 13 | * @param {Boolean} jumped 14 | * @api public 15 | */ 16 | 17 | function require(name, jumped){ 18 | if (cache[name]) return cache[name].exports; 19 | if (modules[name]) return call(name, require); 20 | throw new Error('cannot find module "' + name + '"'); 21 | } 22 | 23 | /** 24 | * Call module `id` and cache it. 25 | * 26 | * @param {Number} id 27 | * @param {Function} require 28 | * @return {Function} 29 | * @api private 30 | */ 31 | 32 | function call(id, require){ 33 | var m = cache[id] = { exports: {} }; 34 | var mod = modules[id]; 35 | var name = mod[2]; 36 | var fn = mod[0]; 37 | 38 | fn.call(m.exports, function(req){ 39 | var dep = modules[id][1][req]; 40 | return require(dep ? dep : req); 41 | }, m, m.exports, outer, modules, cache, entries); 42 | 43 | // expose as `name`. 44 | if (name) cache[name] = cache[id]; 45 | 46 | return cache[id].exports; 47 | } 48 | 49 | /** 50 | * Require all entries exposing them on global if needed. 51 | */ 52 | 53 | for (var id in entries) { 54 | if (entries[id]) { 55 | global[entries[id]] = require(id); 56 | } else { 57 | require(id); 58 | } 59 | } 60 | 61 | /** 62 | * Duo flag. 63 | */ 64 | 65 | require.duo = true; 66 | 67 | /** 68 | * Expose cache. 69 | */ 70 | 71 | require.cache = cache; 72 | 73 | /** 74 | * Expose modules 75 | */ 76 | 77 | require.modules = modules; 78 | 79 | /** 80 | * Return newest require. 81 | */ 82 | 83 | return require; 84 | })({ 85 | 1: [function(require, module, exports) { 86 | /** 87 | * Module Dependencies 88 | */ 89 | 90 | var validate = require('./lib/validate'); 91 | var extend = require('extend.js'); 92 | var form = require('./lib/form'); 93 | var Batch = require('batch'); 94 | var isArray = Array.isArray; 95 | var Rube = require('rube'); 96 | 97 | /** 98 | * Export `Ive` 99 | */ 100 | 101 | module.exports = Ive; 102 | 103 | /** 104 | * Validate an object against a schema 105 | * 106 | * @return {Function} 107 | * @api public 108 | */ 109 | 110 | function Ive(props) { 111 | if (!(this instanceof Ive)) return new Ive(props); 112 | 113 | /** 114 | * Create a ive instance 115 | * 116 | * @param {Object|String|FormElement|Array|NodeList} 117 | * @param {Function} fn 118 | * @return {Ive} self 119 | */ 120 | 121 | function ive(obj, fn) { 122 | if ('string' == typeof obj) return filter(obj, ive.attrs); 123 | 124 | // Browser element 125 | if (obj.nodeName) { 126 | form(obj, ive); 127 | } else if (isArray(obj) || isNodeList(obj)) { 128 | for (var i = 0, el; el = obj[i]; i++) form(el, ive); 129 | } else if (fn) { 130 | // validate 131 | validate(obj, ive.attrs, fn); 132 | } else { 133 | // thunkify 134 | return function (done) { 135 | validate(obj, ive.attrs, done); 136 | } 137 | } 138 | 139 | return ive; 140 | } 141 | 142 | ive.attrs = {}; 143 | 144 | // add the methods 145 | for (var k in Ive.prototype) { 146 | ive[k] = Ive.prototype[k]; 147 | } 148 | 149 | // add the attributes 150 | ive.attr(props); 151 | 152 | return ive; 153 | } 154 | 155 | /** 156 | * Add an attribute 157 | * 158 | * @param {String} attr 159 | * @param {Rube} rube (optional) 160 | */ 161 | 162 | Ive.prototype.attr = function(key, rube) { 163 | if (!key) { 164 | return this.attrs; 165 | } else if ('ive' == key.name || 'object' == typeof key) { 166 | this.attrs = extend(this.attrs, key.attrs || key) 167 | return this; 168 | } else if (rube) { 169 | if (!this.attrs[key]) this.attrs[key] = Rube(); 170 | this.attrs[key].use(rube); 171 | return this; 172 | } else { 173 | return this.attrs[key] || (this.attrs[key] = Rube()); 174 | } 175 | }; 176 | 177 | /** 178 | * Filter out fields 179 | * 180 | * @param {String|Array} fields 181 | * @param {Object} attrs 182 | * @return {Function} 183 | */ 184 | 185 | function filter(fields, attrs) { 186 | return function(obj, fn) { 187 | return validate(obj, only(attrs, fields), fn); 188 | } 189 | } 190 | 191 | /** 192 | * Check if the element is a nodelist 193 | * 194 | * @param {Mixed} el 195 | * @return {Boolean} 196 | */ 197 | 198 | function isNodeList(el) { 199 | return typeof el.length == 'number' 200 | && typeof el.item == 'function' 201 | && typeof el.nextNode == 'function' 202 | && typeof el.reset == 'function' 203 | ? true 204 | : false; 205 | } 206 | 207 | /** 208 | * Filter `obj` by `keys` 209 | * 210 | * @param {Object} obj 211 | * @return {Object} 212 | */ 213 | 214 | function only(obj, keys){ 215 | obj = obj || {}; 216 | if ('string' == typeof keys) keys = keys.split(/ +/); 217 | return keys.reduce(function(ret, key){ 218 | if (null == obj[key]) return ret; 219 | ret[key] = obj[key]; 220 | return ret; 221 | }, {}); 222 | }; 223 | 224 | }, {"./lib/validate":2,"extend.js":3,"./lib/form":4,"batch":5,"rube":6}], 225 | 2: [function(require, module, exports) { 226 | /** 227 | * Module Dependencies 228 | */ 229 | 230 | var Batch = require('batch'); 231 | var keys = Object.keys; 232 | 233 | /** 234 | * Export `validate` 235 | */ 236 | 237 | module.exports = validate; 238 | 239 | /** 240 | * Validate 241 | * 242 | * @param {Object} obj 243 | * @param {Object} schema 244 | * @return {Function} fn 245 | */ 246 | 247 | function validate(obj, schema, fn) { 248 | var batch = Batch().throws(false); 249 | var attrs = keys(schema); 250 | var errors = {}; 251 | var values = {}; 252 | 253 | // loop through each of the schema attributes 254 | attrs.forEach(function(attr) { 255 | batch.push(function (next) { 256 | schema[attr](obj[attr], function(err, v) { 257 | if (err) { 258 | errors[attr] = err; 259 | return next(err); 260 | } else { 261 | values[attr] = v; 262 | return next(); 263 | } 264 | }); 265 | }); 266 | }); 267 | 268 | batch.end(function() { 269 | return keys(errors).length 270 | ? fn(format(errors, obj)) 271 | : fn(null, values); 272 | }) 273 | }; 274 | 275 | /** 276 | * Format the errors into a single error 277 | * 278 | * TODO: create a custom error 279 | * 280 | * @param {Array} arr 281 | * @return {Error} 282 | */ 283 | 284 | function format(errors, actual) { 285 | // format the object 286 | actual = JSON.stringify(actual, true, 2).split('\n').map(function(line) { 287 | return ' | ' + line; 288 | }).join('\n'); 289 | 290 | // format the errors 291 | var msg = keys(errors).map(function(error, i) { 292 | return ' | ' + (i + 1) + '. ' + error + ': ' + errors[error].message; 293 | }).join('\n'); 294 | 295 | var err = new Error('\n |\n | Rube Schema Validation Error\n |\n' + actual + '\n |\n' + msg + '\n |\n'); 296 | err.fields = errors; 297 | return err; 298 | } 299 | 300 | }, {"batch":5}], 301 | 5: [function(require, module, exports) { 302 | /** 303 | * Module dependencies. 304 | */ 305 | 306 | try { 307 | var EventEmitter = require('events').EventEmitter; 308 | } catch (err) { 309 | var Emitter = require('emitter'); 310 | } 311 | 312 | /** 313 | * Noop. 314 | */ 315 | 316 | function noop(){} 317 | 318 | /** 319 | * Expose `Batch`. 320 | */ 321 | 322 | module.exports = Batch; 323 | 324 | /** 325 | * Create a new Batch. 326 | */ 327 | 328 | function Batch() { 329 | if (!(this instanceof Batch)) return new Batch; 330 | this.fns = []; 331 | this.concurrency(Infinity); 332 | this.throws(true); 333 | for (var i = 0, len = arguments.length; i < len; ++i) { 334 | this.push(arguments[i]); 335 | } 336 | } 337 | 338 | /** 339 | * Inherit from `EventEmitter.prototype`. 340 | */ 341 | 342 | if (EventEmitter) { 343 | Batch.prototype.__proto__ = EventEmitter.prototype; 344 | } else { 345 | Emitter(Batch.prototype); 346 | } 347 | 348 | /** 349 | * Set concurrency to `n`. 350 | * 351 | * @param {Number} n 352 | * @return {Batch} 353 | * @api public 354 | */ 355 | 356 | Batch.prototype.concurrency = function(n){ 357 | this.n = n; 358 | return this; 359 | }; 360 | 361 | /** 362 | * Queue a function. 363 | * 364 | * @param {Function} fn 365 | * @return {Batch} 366 | * @api public 367 | */ 368 | 369 | Batch.prototype.push = function(fn){ 370 | this.fns.push(fn); 371 | return this; 372 | }; 373 | 374 | /** 375 | * Set wether Batch will or will not throw up. 376 | * 377 | * @param {Boolean} throws 378 | * @return {Batch} 379 | * @api public 380 | */ 381 | Batch.prototype.throws = function(throws) { 382 | this.e = !!throws; 383 | return this; 384 | }; 385 | 386 | /** 387 | * Execute all queued functions in parallel, 388 | * executing `cb(err, results)`. 389 | * 390 | * @param {Function} cb 391 | * @return {Batch} 392 | * @api public 393 | */ 394 | 395 | Batch.prototype.end = function(cb){ 396 | var self = this 397 | , total = this.fns.length 398 | , pending = total 399 | , results = [] 400 | , errors = [] 401 | , cb = cb || noop 402 | , fns = this.fns 403 | , max = this.n 404 | , throws = this.e 405 | , index = 0 406 | , done; 407 | 408 | // empty 409 | if (!fns.length) return cb(null, results); 410 | 411 | // process 412 | function next() { 413 | var i = index++; 414 | var fn = fns[i]; 415 | if (!fn) return; 416 | var start = new Date; 417 | 418 | try { 419 | fn(callback); 420 | } catch (err) { 421 | callback(err); 422 | } 423 | 424 | function callback(err, res){ 425 | if (done) return; 426 | if (err && throws) return done = true, cb(err); 427 | var complete = total - pending + 1; 428 | var end = new Date; 429 | 430 | results[i] = res; 431 | errors[i] = err; 432 | 433 | self.emit('progress', { 434 | index: i, 435 | value: res, 436 | error: err, 437 | pending: pending, 438 | total: total, 439 | complete: complete, 440 | percent: complete / total * 100 | 0, 441 | start: start, 442 | end: end, 443 | duration: end - start 444 | }); 445 | 446 | if (--pending) next(); 447 | else if(!throws) cb(errors, results); 448 | else cb(null, results); 449 | } 450 | } 451 | 452 | // concurrency 453 | for (var i = 0; i < fns.length; i++) { 454 | if (i == max) break; 455 | next(); 456 | } 457 | 458 | return this; 459 | }; 460 | 461 | }, {"emitter":7}], 462 | 7: [function(require, module, exports) { 463 | 464 | /** 465 | * Expose `Emitter`. 466 | */ 467 | 468 | module.exports = Emitter; 469 | 470 | /** 471 | * Initialize a new `Emitter`. 472 | * 473 | * @api public 474 | */ 475 | 476 | function Emitter(obj) { 477 | if (obj) return mixin(obj); 478 | }; 479 | 480 | /** 481 | * Mixin the emitter properties. 482 | * 483 | * @param {Object} obj 484 | * @return {Object} 485 | * @api private 486 | */ 487 | 488 | function mixin(obj) { 489 | for (var key in Emitter.prototype) { 490 | obj[key] = Emitter.prototype[key]; 491 | } 492 | return obj; 493 | } 494 | 495 | /** 496 | * Listen on the given `event` with `fn`. 497 | * 498 | * @param {String} event 499 | * @param {Function} fn 500 | * @return {Emitter} 501 | * @api public 502 | */ 503 | 504 | Emitter.prototype.on = 505 | Emitter.prototype.addEventListener = function(event, fn){ 506 | this._callbacks = this._callbacks || {}; 507 | (this._callbacks[event] = this._callbacks[event] || []) 508 | .push(fn); 509 | return this; 510 | }; 511 | 512 | /** 513 | * Adds an `event` listener that will be invoked a single 514 | * time then automatically removed. 515 | * 516 | * @param {String} event 517 | * @param {Function} fn 518 | * @return {Emitter} 519 | * @api public 520 | */ 521 | 522 | Emitter.prototype.once = function(event, fn){ 523 | var self = this; 524 | this._callbacks = this._callbacks || {}; 525 | 526 | function on() { 527 | self.off(event, on); 528 | fn.apply(this, arguments); 529 | } 530 | 531 | on.fn = fn; 532 | this.on(event, on); 533 | return this; 534 | }; 535 | 536 | /** 537 | * Remove the given callback for `event` or all 538 | * registered callbacks. 539 | * 540 | * @param {String} event 541 | * @param {Function} fn 542 | * @return {Emitter} 543 | * @api public 544 | */ 545 | 546 | Emitter.prototype.off = 547 | Emitter.prototype.removeListener = 548 | Emitter.prototype.removeAllListeners = 549 | Emitter.prototype.removeEventListener = function(event, fn){ 550 | this._callbacks = this._callbacks || {}; 551 | 552 | // all 553 | if (0 == arguments.length) { 554 | this._callbacks = {}; 555 | return this; 556 | } 557 | 558 | // specific event 559 | var callbacks = this._callbacks[event]; 560 | if (!callbacks) return this; 561 | 562 | // remove all handlers 563 | if (1 == arguments.length) { 564 | delete this._callbacks[event]; 565 | return this; 566 | } 567 | 568 | // remove specific handler 569 | var cb; 570 | for (var i = 0; i < callbacks.length; i++) { 571 | cb = callbacks[i]; 572 | if (cb === fn || cb.fn === fn) { 573 | callbacks.splice(i, 1); 574 | break; 575 | } 576 | } 577 | return this; 578 | }; 579 | 580 | /** 581 | * Emit `event` with the given args. 582 | * 583 | * @param {String} event 584 | * @param {Mixed} ... 585 | * @return {Emitter} 586 | */ 587 | 588 | Emitter.prototype.emit = function(event){ 589 | this._callbacks = this._callbacks || {}; 590 | var args = [].slice.call(arguments, 1) 591 | , callbacks = this._callbacks[event]; 592 | 593 | if (callbacks) { 594 | callbacks = callbacks.slice(0); 595 | for (var i = 0, len = callbacks.length; i < len; ++i) { 596 | callbacks[i].apply(this, args); 597 | } 598 | } 599 | 600 | return this; 601 | }; 602 | 603 | /** 604 | * Return array of callbacks for `event`. 605 | * 606 | * @param {String} event 607 | * @return {Array} 608 | * @api public 609 | */ 610 | 611 | Emitter.prototype.listeners = function(event){ 612 | this._callbacks = this._callbacks || {}; 613 | return this._callbacks[event] || []; 614 | }; 615 | 616 | /** 617 | * Check if this emitter has `event` handlers. 618 | * 619 | * @param {String} event 620 | * @return {Boolean} 621 | * @api public 622 | */ 623 | 624 | Emitter.prototype.hasListeners = function(event){ 625 | return !! this.listeners(event).length; 626 | }; 627 | 628 | }, {}], 629 | 3: [function(require, module, exports) { 630 | /** 631 | * Extend an object with another. 632 | * 633 | * @param {Object, ...} src, ... 634 | * @return {Object} merged 635 | * @api private 636 | */ 637 | 638 | module.exports = function(src) { 639 | var objs = [].slice.call(arguments, 1), obj; 640 | 641 | for (var i = 0, len = objs.length; i < len; i++) { 642 | obj = objs[i]; 643 | for (var prop in obj) { 644 | src[prop] = obj[prop]; 645 | } 646 | } 647 | 648 | return src; 649 | } 650 | 651 | }, {}], 652 | 4: [function(require, module, exports) { 653 | /** 654 | * Module Dependencies 655 | */ 656 | 657 | var validate = require('./validate'); 658 | var assert = require('assert'); 659 | 660 | /** 661 | * Export `form` 662 | */ 663 | 664 | module.exports = form; 665 | 666 | /** 667 | * Exclude 668 | */ 669 | 670 | var exclude = /^(button|submit|reset|hidden)$/; 671 | 672 | /** 673 | * Initialize `form` 674 | * 675 | * @param {HTMLForm} el 676 | */ 677 | 678 | function form(el, schema) { 679 | assert(el.nodeName == 'FORM'); 680 | 681 | // browser-only modules 682 | var event = require('event'); 683 | 684 | // bind onto the form 685 | var submit = el.querySelector('input[type="submit"]'); 686 | event.bind(el, 'submit', onsubmit(el, submit, schema)); 687 | 688 | var inputs = el.querySelectorAll('input, textarea'); 689 | for (var i = 0, input; input = inputs[i]; i++) { 690 | if (exclude.test(input.type)) return; 691 | event.bind(input, 'blur', onevent(input, schema.attrs, field)); 692 | } 693 | } 694 | 695 | /** 696 | * Listen to submit events 697 | */ 698 | 699 | function onsubmit(form, button, schema) { 700 | var Form = require('form'); 701 | 702 | return function submit(e) { 703 | if (form.getAttribute('submitting')) return true; 704 | 705 | e.preventDefault(); 706 | e.stopImmediatePropagation(); 707 | 708 | var json = Form(form).serialize(); 709 | schema(json, function(err, v) { 710 | if (err) { 711 | form.setAttribute('invalid', err.message); 712 | for (var name in err.fields) { 713 | field(form.querySelector('[name="' + name + '"]'))(err.fields[name]); 714 | } 715 | } else { 716 | form.setAttribute('submitting', 'submitting'); 717 | form.removeAttribute('invalid'); 718 | submitForm(form); 719 | } 720 | }); 721 | }; 722 | } 723 | 724 | /** 725 | * Listen for blur events 726 | * 727 | * @param {InputElement} inputs 728 | * @param {Object} attrs 729 | * @param {Function} fn 730 | * @return {Function} 731 | */ 732 | 733 | function onevent(input, attrs, fn) { 734 | var name = input.getAttribute('name'); 735 | return function event(e) { 736 | var value = input.value; 737 | if ('' === value) return; 738 | attrs[name](value, fn(input)); 739 | }; 740 | } 741 | 742 | /** 743 | * Check validation on a field 744 | * 745 | * @param {InputElement} input 746 | * @return {Function} 747 | */ 748 | 749 | function field(input) { 750 | return function check(err, v) { 751 | if (err) { 752 | input.setAttribute('invalid', err.message); 753 | } else { 754 | input.removeAttribute('invalid'); 755 | if (v) input.value = v; 756 | } 757 | } 758 | } 759 | 760 | /** 761 | * Submit a `form` programmatically, 762 | * triggering submit handlers. 763 | * 764 | * @param {Element} form 765 | */ 766 | 767 | function submitForm(form) { 768 | var trigger = require('trigger-event'); 769 | var button = document.createElement('button'); 770 | button.style.display = 'none'; 771 | form.appendChild(button); 772 | trigger(button, 'click', { clientX: 0, clientY: 0}); 773 | form.removeChild(button); 774 | } 775 | 776 | 777 | }, {"./validate":2,"assert":8,"event":9,"form":10,"trigger-event":11}], 778 | 8: [function(require, module, exports) { 779 | 780 | /** 781 | * Module dependencies. 782 | */ 783 | 784 | var equals = require('equals'); 785 | var fmt = require('fmt'); 786 | var stack = require('stack'); 787 | 788 | /** 789 | * Assert `expr` with optional failure `msg`. 790 | * 791 | * @param {Mixed} expr 792 | * @param {String} [msg] 793 | * @api public 794 | */ 795 | 796 | module.exports = exports = function (expr, msg) { 797 | if (expr) return; 798 | throw error(msg || message()); 799 | }; 800 | 801 | /** 802 | * Assert `actual` is weak equal to `expected`. 803 | * 804 | * @param {Mixed} actual 805 | * @param {Mixed} expected 806 | * @param {String} [msg] 807 | * @api public 808 | */ 809 | 810 | exports.equal = function (actual, expected, msg) { 811 | if (actual == expected) return; 812 | throw error(msg || fmt('Expected %o to equal %o.', actual, expected), actual, expected); 813 | }; 814 | 815 | /** 816 | * Assert `actual` is not weak equal to `expected`. 817 | * 818 | * @param {Mixed} actual 819 | * @param {Mixed} expected 820 | * @param {String} [msg] 821 | * @api public 822 | */ 823 | 824 | exports.notEqual = function (actual, expected, msg) { 825 | if (actual != expected) return; 826 | throw error(msg || fmt('Expected %o not to equal %o.', actual, expected)); 827 | }; 828 | 829 | /** 830 | * Assert `actual` is deep equal to `expected`. 831 | * 832 | * @param {Mixed} actual 833 | * @param {Mixed} expected 834 | * @param {String} [msg] 835 | * @api public 836 | */ 837 | 838 | exports.deepEqual = function (actual, expected, msg) { 839 | if (equals(actual, expected)) return; 840 | throw error(msg || fmt('Expected %o to deeply equal %o.', actual, expected), actual, expected); 841 | }; 842 | 843 | /** 844 | * Assert `actual` is not deep equal to `expected`. 845 | * 846 | * @param {Mixed} actual 847 | * @param {Mixed} expected 848 | * @param {String} [msg] 849 | * @api public 850 | */ 851 | 852 | exports.notDeepEqual = function (actual, expected, msg) { 853 | if (!equals(actual, expected)) return; 854 | throw error(msg || fmt('Expected %o not to deeply equal %o.', actual, expected)); 855 | }; 856 | 857 | /** 858 | * Assert `actual` is strict equal to `expected`. 859 | * 860 | * @param {Mixed} actual 861 | * @param {Mixed} expected 862 | * @param {String} [msg] 863 | * @api public 864 | */ 865 | 866 | exports.strictEqual = function (actual, expected, msg) { 867 | if (actual === expected) return; 868 | throw error(msg || fmt('Expected %o to strictly equal %o.', actual, expected), actual, expected); 869 | }; 870 | 871 | /** 872 | * Assert `actual` is not strict equal to `expected`. 873 | * 874 | * @param {Mixed} actual 875 | * @param {Mixed} expected 876 | * @param {String} [msg] 877 | * @api public 878 | */ 879 | 880 | exports.notStrictEqual = function (actual, expected, msg) { 881 | if (actual !== expected) return; 882 | throw error(msg || fmt('Expected %o not to strictly equal %o.', actual, expected)); 883 | }; 884 | 885 | /** 886 | * Assert `block` throws an `error`. 887 | * 888 | * @param {Function} block 889 | * @param {Function} [error] 890 | * @param {String} [msg] 891 | * @api public 892 | */ 893 | 894 | exports.throws = function (block, err, msg) { 895 | var threw; 896 | try { 897 | block(); 898 | } catch (e) { 899 | threw = e; 900 | } 901 | 902 | if (!threw) throw error(msg || fmt('Expected %s to throw an error.', block.toString())); 903 | if (err && !(threw instanceof err)) { 904 | throw error(msg || fmt('Expected %s to throw an %o.', block.toString(), err)); 905 | } 906 | }; 907 | 908 | /** 909 | * Assert `block` doesn't throw an `error`. 910 | * 911 | * @param {Function} block 912 | * @param {Function} [error] 913 | * @param {String} [msg] 914 | * @api public 915 | */ 916 | 917 | exports.doesNotThrow = function (block, err, msg) { 918 | var threw; 919 | try { 920 | block(); 921 | } catch (e) { 922 | threw = e; 923 | } 924 | 925 | if (threw) throw error(msg || fmt('Expected %s not to throw an error.', block.toString())); 926 | if (err && (threw instanceof err)) { 927 | throw error(msg || fmt('Expected %s not to throw an %o.', block.toString(), err)); 928 | } 929 | }; 930 | 931 | /** 932 | * Create a message from the call stack. 933 | * 934 | * @return {String} 935 | * @api private 936 | */ 937 | 938 | function message() { 939 | if (!Error.captureStackTrace) return 'assertion failed'; 940 | var callsite = stack()[2]; 941 | var fn = callsite.getFunctionName(); 942 | var file = callsite.getFileName(); 943 | var line = callsite.getLineNumber() - 1; 944 | var col = callsite.getColumnNumber() - 1; 945 | var src = get(file); 946 | line = src.split('\n')[line].slice(col); 947 | var m = line.match(/assert\((.*)\)/); 948 | return m && m[1].trim(); 949 | } 950 | 951 | /** 952 | * Load contents of `script`. 953 | * 954 | * @param {String} script 955 | * @return {String} 956 | * @api private 957 | */ 958 | 959 | function get(script) { 960 | var xhr = new XMLHttpRequest; 961 | xhr.open('GET', script, false); 962 | xhr.send(null); 963 | return xhr.responseText; 964 | } 965 | 966 | /** 967 | * Error with `msg`, `actual` and `expected`. 968 | * 969 | * @param {String} msg 970 | * @param {Mixed} actual 971 | * @param {Mixed} expected 972 | * @return {Error} 973 | */ 974 | 975 | function error(msg, actual, expected){ 976 | var err = new Error(msg); 977 | err.showDiff = 3 == arguments.length; 978 | err.actual = actual; 979 | err.expected = expected; 980 | return err; 981 | } 982 | 983 | }, {"equals":12,"fmt":13,"stack":14}], 984 | 12: [function(require, module, exports) { 985 | var type = require('type') 986 | 987 | // (any, any, [array]) -> boolean 988 | function equal(a, b, memos){ 989 | // All identical values are equivalent 990 | if (a === b) return true 991 | var fnA = types[type(a)] 992 | var fnB = types[type(b)] 993 | return fnA && fnA === fnB 994 | ? fnA(a, b, memos) 995 | : false 996 | } 997 | 998 | var types = {} 999 | 1000 | // (Number) -> boolean 1001 | types.number = function(a, b){ 1002 | return a !== a && b !== b/*Nan check*/ 1003 | } 1004 | 1005 | // (function, function, array) -> boolean 1006 | types['function'] = function(a, b, memos){ 1007 | return a.toString() === b.toString() 1008 | // Functions can act as objects 1009 | && types.object(a, b, memos) 1010 | && equal(a.prototype, b.prototype) 1011 | } 1012 | 1013 | // (date, date) -> boolean 1014 | types.date = function(a, b){ 1015 | return +a === +b 1016 | } 1017 | 1018 | // (regexp, regexp) -> boolean 1019 | types.regexp = function(a, b){ 1020 | return a.toString() === b.toString() 1021 | } 1022 | 1023 | // (DOMElement, DOMElement) -> boolean 1024 | types.element = function(a, b){ 1025 | return a.outerHTML === b.outerHTML 1026 | } 1027 | 1028 | // (textnode, textnode) -> boolean 1029 | types.textnode = function(a, b){ 1030 | return a.textContent === b.textContent 1031 | } 1032 | 1033 | // decorate `fn` to prevent it re-checking objects 1034 | // (function) -> function 1035 | function memoGaurd(fn){ 1036 | return function(a, b, memos){ 1037 | if (!memos) return fn(a, b, []) 1038 | var i = memos.length, memo 1039 | while (memo = memos[--i]) { 1040 | if (memo[0] === a && memo[1] === b) return true 1041 | } 1042 | return fn(a, b, memos) 1043 | } 1044 | } 1045 | 1046 | types['arguments'] = 1047 | types.array = memoGaurd(arrayEqual) 1048 | 1049 | // (array, array, array) -> boolean 1050 | function arrayEqual(a, b, memos){ 1051 | var i = a.length 1052 | if (i !== b.length) return false 1053 | memos.push([a, b]) 1054 | while (i--) { 1055 | if (!equal(a[i], b[i], memos)) return false 1056 | } 1057 | return true 1058 | } 1059 | 1060 | types.object = memoGaurd(objectEqual) 1061 | 1062 | // (object, object, array) -> boolean 1063 | function objectEqual(a, b, memos) { 1064 | if (typeof a.equal == 'function') { 1065 | memos.push([a, b]) 1066 | return a.equal(b, memos) 1067 | } 1068 | var ka = getEnumerableProperties(a) 1069 | var kb = getEnumerableProperties(b) 1070 | var i = ka.length 1071 | 1072 | // same number of properties 1073 | if (i !== kb.length) return false 1074 | 1075 | // although not necessarily the same order 1076 | ka.sort() 1077 | kb.sort() 1078 | 1079 | // cheap key test 1080 | while (i--) if (ka[i] !== kb[i]) return false 1081 | 1082 | // remember 1083 | memos.push([a, b]) 1084 | 1085 | // iterate again this time doing a thorough check 1086 | i = ka.length 1087 | while (i--) { 1088 | var key = ka[i] 1089 | if (!equal(a[key], b[key], memos)) return false 1090 | } 1091 | 1092 | return true 1093 | } 1094 | 1095 | // (object) -> array 1096 | function getEnumerableProperties (object) { 1097 | var result = [] 1098 | for (var k in object) if (k !== 'constructor') { 1099 | result.push(k) 1100 | } 1101 | return result 1102 | } 1103 | 1104 | module.exports = equal 1105 | 1106 | }, {"type":15}], 1107 | 15: [function(require, module, exports) { 1108 | 1109 | var toString = {}.toString 1110 | var DomNode = typeof window != 'undefined' 1111 | ? window.Node 1112 | : Function 1113 | 1114 | /** 1115 | * Return the type of `val`. 1116 | * 1117 | * @param {Mixed} val 1118 | * @return {String} 1119 | * @api public 1120 | */ 1121 | 1122 | module.exports = exports = function(x){ 1123 | var type = typeof x 1124 | if (type != 'object') return type 1125 | type = types[toString.call(x)] 1126 | if (type) return type 1127 | if (x instanceof DomNode) switch (x.nodeType) { 1128 | case 1: return 'element' 1129 | case 3: return 'text-node' 1130 | case 9: return 'document' 1131 | case 11: return 'document-fragment' 1132 | default: return 'dom-node' 1133 | } 1134 | } 1135 | 1136 | var types = exports.types = { 1137 | '[object Function]': 'function', 1138 | '[object Date]': 'date', 1139 | '[object RegExp]': 'regexp', 1140 | '[object Arguments]': 'arguments', 1141 | '[object Array]': 'array', 1142 | '[object String]': 'string', 1143 | '[object Null]': 'null', 1144 | '[object Undefined]': 'undefined', 1145 | '[object Number]': 'number', 1146 | '[object Boolean]': 'boolean', 1147 | '[object Object]': 'object', 1148 | '[object Text]': 'text-node', 1149 | '[object Uint8Array]': 'bit-array', 1150 | '[object Uint16Array]': 'bit-array', 1151 | '[object Uint32Array]': 'bit-array', 1152 | '[object Uint8ClampedArray]': 'bit-array', 1153 | '[object Error]': 'error', 1154 | '[object FormData]': 'form-data', 1155 | '[object File]': 'file', 1156 | '[object Blob]': 'blob' 1157 | } 1158 | 1159 | }, {}], 1160 | 13: [function(require, module, exports) { 1161 | 1162 | /** 1163 | * Export `fmt` 1164 | */ 1165 | 1166 | module.exports = fmt; 1167 | 1168 | /** 1169 | * Formatters 1170 | */ 1171 | 1172 | fmt.o = JSON.stringify; 1173 | fmt.s = String; 1174 | fmt.d = parseInt; 1175 | 1176 | /** 1177 | * Format the given `str`. 1178 | * 1179 | * @param {String} str 1180 | * @param {...} args 1181 | * @return {String} 1182 | * @api public 1183 | */ 1184 | 1185 | function fmt(str){ 1186 | var args = [].slice.call(arguments, 1); 1187 | var j = 0; 1188 | 1189 | return str.replace(/%([a-z])/gi, function(_, f){ 1190 | return fmt[f] 1191 | ? fmt[f](args[j++]) 1192 | : _ + f; 1193 | }); 1194 | } 1195 | 1196 | }, {}], 1197 | 14: [function(require, module, exports) { 1198 | 1199 | /** 1200 | * Expose `stack()`. 1201 | */ 1202 | 1203 | module.exports = stack; 1204 | 1205 | /** 1206 | * Return the stack. 1207 | * 1208 | * @return {Array} 1209 | * @api public 1210 | */ 1211 | 1212 | function stack() { 1213 | var orig = Error.prepareStackTrace; 1214 | Error.prepareStackTrace = function(_, stack){ return stack; }; 1215 | var err = new Error; 1216 | Error.captureStackTrace(err, arguments.callee); 1217 | var stack = err.stack; 1218 | Error.prepareStackTrace = orig; 1219 | return stack; 1220 | } 1221 | }, {}], 1222 | 9: [function(require, module, exports) { 1223 | var bind = window.addEventListener ? 'addEventListener' : 'attachEvent', 1224 | unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent', 1225 | prefix = bind !== 'addEventListener' ? 'on' : ''; 1226 | 1227 | /** 1228 | * Bind `el` event `type` to `fn`. 1229 | * 1230 | * @param {Element} el 1231 | * @param {String} type 1232 | * @param {Function} fn 1233 | * @param {Boolean} capture 1234 | * @return {Function} 1235 | * @api public 1236 | */ 1237 | 1238 | exports.bind = function(el, type, fn, capture){ 1239 | el[bind](prefix + type, fn, capture || false); 1240 | return fn; 1241 | }; 1242 | 1243 | /** 1244 | * Unbind `el` event `type`'s callback `fn`. 1245 | * 1246 | * @param {Element} el 1247 | * @param {String} type 1248 | * @param {Function} fn 1249 | * @param {Boolean} capture 1250 | * @return {Function} 1251 | * @api public 1252 | */ 1253 | 1254 | exports.unbind = function(el, type, fn, capture){ 1255 | el[unbind](prefix + type, fn, capture || false); 1256 | return fn; 1257 | }; 1258 | }, {}], 1259 | 10: [function(require, module, exports) { 1260 | // dependencies 1261 | var classes = require("classes"); 1262 | var formElement = require("form-element"); 1263 | var serialize = require("form-serialize"); 1264 | var value = require("value"); 1265 | 1266 | 1267 | // single export 1268 | module.exports = Form; 1269 | 1270 | 1271 | /** 1272 | * A helper for working with HTML forms 1273 | * 1274 | * @constructor 1275 | * @param {HTMLFormElement} el 1276 | */ 1277 | function Form(el) { 1278 | if (!(this instanceof Form)) { 1279 | return new Form(el); 1280 | } 1281 | 1282 | this.element = el; 1283 | this.classes = classes(this.element); 1284 | } 1285 | 1286 | 1287 | /** 1288 | * Retrieves an input from the form by name. If 2 arguments are passed, 1289 | * the first is assumed to be the name of a `