├── .gitignore ├── tests ├── helper.js ├── validationDefinition.js ├── node │ └── mixin.js ├── customSelectors.js ├── forceUpdate.js ├── validators │ ├── acceptance.js │ ├── equalTo.js │ ├── oneOf.js │ ├── max.js │ ├── min.js │ ├── range.js │ ├── minLength.js │ ├── maxLength.js │ ├── patterns.js │ ├── length.js │ ├── pattern.js │ ├── rangeLength.js │ ├── namedMethod.js │ ├── required.js │ └── method.js ├── customPatterns.js ├── errorMessages.js ├── preValidate.js ├── customCallbacks.js ├── mixin.js ├── labelFormatter.js ├── isValid.js ├── customValidators.js ├── events.js ├── attributesOption.js ├── nestedValidation.js ├── binding.js └── general.js ├── bower.json ├── src ├── backbone-validation-amd.js └── backbone-validation.js ├── buster.js ├── package.json ├── LICENSE.md ├── docco ├── docco.jst └── docco.css ├── gruntfile.js ├── dist ├── backbone-validation-min.js └── backbone-validation-amd-min.js └── docs └── docco.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | *.log 3 | node_modules -------------------------------------------------------------------------------- /tests/helper.js: -------------------------------------------------------------------------------- 1 | var assert = buster.assert; 2 | var refute = buster.refute; 3 | var expect = buster.expect; -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone.validation", 3 | "version": "0.11.3", 4 | "main": "dist/backbone-validation.js", 5 | "dependencies": { 6 | "underscore": ">=1.5.0", 7 | "backbone": ">=1.1.2" 8 | }, 9 | "ignore": ["lib", "tests", ".html"] 10 | } 11 | -------------------------------------------------------------------------------- /src/backbone-validation-amd.js: -------------------------------------------------------------------------------- 1 | (function (factory) { 2 | if (typeof exports === 'object') { 3 | module.exports = factory(require('backbone'), require('underscore')); 4 | } else if (typeof define === 'function' && define.amd) { 5 | define(['backbone', 'underscore'], factory); 6 | } 7 | }(function (Backbone, _) { 8 | //= backbone-validation.js 9 | return Backbone.Validation; 10 | })); 11 | -------------------------------------------------------------------------------- /buster.js: -------------------------------------------------------------------------------- 1 | var config = exports; 2 | 3 | config['Browser'] = { 4 | environment: 'browser', 5 | sources: [ 6 | 'lib/jquery-1.6.2.js', 7 | 'lib/underscore.js', 8 | 'lib/backbone-1.0.0.js', 9 | 'dist/backbone-validation.js' 10 | ], 11 | tests: [ 12 | 'tests/*.js', 13 | 'tests/validators/*.js' 14 | ], 15 | testHelpers: ['tests/helper.js'] 16 | }; 17 | 18 | config['Node'] = { 19 | environment: 'node', 20 | tests: [ 21 | 'tests/node/*.js' 22 | ] 23 | }; -------------------------------------------------------------------------------- /tests/validationDefinition.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Backbone.Validation validation definition", { 2 | setUp: function() { 3 | var Model = Backbone.Model.extend({ 4 | validation: function() { 5 | return { 6 | name: { 7 | required: true 8 | } 9 | }; 10 | } 11 | }); 12 | 13 | _.extend(Model.prototype, Backbone.Validation.mixin); 14 | 15 | this.model = new Model(); 16 | }, 17 | 18 | "can be a function": function () { 19 | refute(this.model.set({ 20 | name: '' 21 | }, {validate: true})); 22 | 23 | assert(this.model.set({ 24 | name: 'name' 25 | }, {validate: true})); 26 | } 27 | }); -------------------------------------------------------------------------------- /tests/node/mixin.js: -------------------------------------------------------------------------------- 1 | var buster = require('buster'), 2 | backbone = require('backbone'), 3 | _ = require('underscore'), 4 | validation = require('../../dist/backbone-validation-amd'); 5 | 6 | buster.testCase("Server validation", { 7 | 8 | setUp: function(){ 9 | _.extend(backbone.Model.prototype, validation.mixin); 10 | 11 | var Model = backbone.Model.extend({ 12 | validation: { 13 | name: { 14 | required: true 15 | } 16 | } 17 | }); 18 | 19 | this.model = new Model(); 20 | }, 21 | 22 | "Passes validation": function () { 23 | buster.assert(this.model.set({name:'Name'}, {validate: true})); 24 | }, 25 | 26 | "Fails validation": function () { 27 | buster.refute(this.model.set({name:''}, {validate: true})); 28 | } 29 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone-validation", 3 | "title": "Backbone.Validation", 4 | "version": "0.11.5", 5 | "author": { 6 | "name": "Thomas Pedersen", 7 | "url": "http://thedersen.com/" 8 | }, 9 | "homepage": "http://thedersen.com/projects/backbone-validation", 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "http://github.com/thedersen/backbone.validation/issues" 13 | }, 14 | "directories": { 15 | "lib": "./dist" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "http://github.com/thedersen/backbone.validation.git" 20 | }, 21 | "scripts": { 22 | "test": "grunt buster", 23 | "prepublish": "grunt" 24 | }, 25 | "main": "./dist/backbone-validation-amd.js", 26 | "dependencies": { 27 | "backbone": ">=1.0.0", 28 | "underscore": ">=1.4.3" 29 | }, 30 | "devDependencies": { 31 | "grunt": "~0.4.1", 32 | "buster": "~0.7.8", 33 | "grunt-docco": ">=0.3.0", 34 | "grunt-shell": ">=0.1.3", 35 | "grunt-rigger": ">=0.3.0", 36 | "grunt-buster": "~0.3.1", 37 | "grunt-contrib-jshint": "~0.6.0", 38 | "grunt-contrib-uglify": "~0.2.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thedersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/customSelectors.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Overriding default id selector with class", { 2 | setUp: function() { 3 | var View = Backbone.View.extend({ 4 | render: function() { 5 | var html = $(''); 6 | this.$el.append(html); 7 | } 8 | }); 9 | 10 | var Model = Backbone.Model.extend({ 11 | validation: { 12 | name: function(val) { 13 | if (!val) { 14 | return 'Name is invalid'; 15 | } 16 | } 17 | } 18 | }); 19 | 20 | this.model = new Model(); 21 | this.view = new View({ 22 | model: this.model 23 | }); 24 | 25 | this.view.render(); 26 | this.name = $(this.view.$(".name")); 27 | }, 28 | 29 | "globally": function() { 30 | Backbone.Validation.configure({ 31 | selector: 'class' 32 | }); 33 | Backbone.Validation.bind(this.view); 34 | 35 | this.model.set({name:''}, {validate: true}); 36 | 37 | assert(this.name.hasClass('invalid')); 38 | 39 | Backbone.Validation.configure({ 40 | selector: 'name' 41 | }); 42 | }, 43 | 44 | "per view when binding": function() { 45 | Backbone.Validation.bind(this.view, { 46 | selector: 'class' 47 | }); 48 | this.model.set({name:''}, {validate: true}); 49 | 50 | assert(this.name.hasClass('invalid')); 51 | } 52 | }); -------------------------------------------------------------------------------- /tests/forceUpdate.js: -------------------------------------------------------------------------------- 1 | buster.testCase("forceUpdate", { 2 | setUp: function() { 3 | var Model = Backbone.Model.extend({ 4 | validation: { 5 | name: { 6 | required: true 7 | } 8 | } 9 | }); 10 | this.model = new Model(); 11 | this.view = new Backbone.View({model: this.model}); 12 | }, 13 | 14 | "default behaviour": { 15 | setUp: function() { 16 | Backbone.Validation.bind(this.view); 17 | }, 18 | 19 | "invalid values are not set on model": function() { 20 | refute(this.model.set({name:''}, {validate: true})); 21 | } 22 | }, 23 | 24 | "forcing update when binding": { 25 | setUp: function() { 26 | Backbone.Validation.bind(this.view, { 27 | forceUpdate: true 28 | }); 29 | }, 30 | 31 | "invalid values are set on model": function() { 32 | assert(this.model.set({name:''}, {validate: true})); 33 | } 34 | }, 35 | 36 | "forcing update when setting attribute": { 37 | setUp: function() { 38 | Backbone.Validation.bind(this.view); 39 | }, 40 | 41 | "invalid values are set on model": function() { 42 | assert(this.model.set({name:''}, {forceUpdate: true, validate: true})); 43 | } 44 | }, 45 | 46 | "forcing update globally": { 47 | setUp: function() { 48 | Backbone.Validation.configure({ 49 | forceUpdate: true 50 | }); 51 | Backbone.Validation.bind(this.view); 52 | }, 53 | 54 | tearDown: function() { 55 | Backbone.Validation.configure({ 56 | forceUpdate: false 57 | }); 58 | }, 59 | 60 | "invalid values are set on model": function() { 61 | assert(this.model.set({name:''}, {validate: true})); 62 | } 63 | } 64 | }); -------------------------------------------------------------------------------- /tests/validators/acceptance.js: -------------------------------------------------------------------------------- 1 | buster.testCase("acceptance validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | agree: { 7 | acceptance: true 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({agree: 'Agree must be accepted'}, error); 26 | done(); 27 | }); 28 | this.model.set({agree:false}, {validate: true}); 29 | }, 30 | 31 | "non-boolean is invalid": function(){ 32 | refute(this.model.set({ 33 | agree: 'non-boolean' 34 | }, {validate: true})); 35 | }, 36 | 37 | "string with true is evaluated as valid": function() { 38 | assert(this.model.set({ 39 | agree: 'true' 40 | }, {validate: true})); 41 | }, 42 | 43 | "false boolean is invalid": function() { 44 | refute(this.model.set({ 45 | agree: false 46 | }, {validate: true})); 47 | }, 48 | 49 | "true boolean is valid": function() { 50 | assert(this.model.set({ 51 | agree: true 52 | }, {validate: true})); 53 | } 54 | }); -------------------------------------------------------------------------------- /tests/customPatterns.js: -------------------------------------------------------------------------------- 1 | buster.testCase('Extending Backbone.Validation with custom pattern', { 2 | setUp: function() { 3 | _.extend(Backbone.Validation.patterns, { 4 | custom: /^test/ 5 | }); 6 | 7 | var Model = Backbone.Model.extend({ 8 | validation: { 9 | name: { 10 | pattern: 'custom' 11 | } 12 | } 13 | }); 14 | 15 | this.model = new Model(); 16 | Backbone.Validation.bind(new Backbone.View({ 17 | model: this.model 18 | })); 19 | }, 20 | 21 | "should execute the custom pattern validator": function() { 22 | assert(this.model.set({ 23 | name: 'test' 24 | }, {validate: true})); 25 | refute(this.model.set({ 26 | name: 'aa' 27 | }, {validate: true})); 28 | } 29 | }); 30 | 31 | buster.testCase('Overriding builtin pattern in Backbone.Validation', { 32 | setUp: function() { 33 | this.builtinEmail = Backbone.Validation.patterns.email; 34 | 35 | _.extend(Backbone.Validation.patterns, { 36 | email: /^test/ 37 | }); 38 | 39 | var Model = Backbone.Model.extend({ 40 | validation: { 41 | name: { 42 | pattern: 'email' 43 | } 44 | } 45 | }); 46 | 47 | this.model = new Model(); 48 | Backbone.Validation.bind(new Backbone.View({ 49 | model: this.model 50 | })); 51 | }, 52 | 53 | tearDown: function(){ 54 | Backbone.Validation.patterns.email = this.builtinEmail; 55 | }, 56 | 57 | "should execute the custom pattern validator": function() { 58 | assert(this.model.set({ 59 | name: 'test' 60 | }, { validate: true })); 61 | refute(this.model.set({ 62 | name: 'aa' 63 | }, { validate: true })); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /tests/validators/equalTo.js: -------------------------------------------------------------------------------- 1 | buster.testCase("equalTo validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | password: { 7 | required: true 8 | }, 9 | passwordRepeat: { 10 | equalTo: 'password' 11 | } 12 | } 13 | }); 14 | 15 | this.model = new Model(); 16 | this.view = new Backbone.View({ 17 | model: this.model 18 | }); 19 | 20 | Backbone.Validation.bind(this.view, { 21 | valid: this.spy(), 22 | invalid: this.spy() 23 | }); 24 | 25 | this.model.set({password: 'password'}); 26 | }, 27 | 28 | "has default error message": function(done) { 29 | this.model.bind('validated:invalid', function(model, error){ 30 | assert.equals({passwordRepeat: 'Password repeat must be the same as Password'}, error); 31 | done(); 32 | }); 33 | this.model.set({passwordRepeat:'123'}, {validate: true}); 34 | }, 35 | 36 | "value equal to (===) the specified attribute is valid": function(){ 37 | assert(this.model.set({ 38 | passwordRepeat: 'password' 39 | }, {validate: true})); 40 | }, 41 | 42 | "value not equal to (!==) the specified attribute is invalid": function(){ 43 | refute(this.model.set({ 44 | passwordRepeat: 'error' 45 | }, {validate: true})); 46 | }, 47 | 48 | "is case sensitive": function(){ 49 | refute(this.model.set({ 50 | passwordRepeat: 'Password' 51 | }, {validate: true})); 52 | }, 53 | 54 | "setting both at the same time to the same value is valid": function() { 55 | assert(this.model.set({ 56 | password: 'a', 57 | passwordRepeat: 'a' 58 | }, {validate: true})); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /tests/errorMessages.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Specifying error messages", { 2 | setUp: function() { 3 | this.model = new Backbone.Model(); 4 | this.view = new Backbone.View({model: this.model}); 5 | 6 | this.invalid = this.spy(); 7 | Backbone.Validation.bind(this.view, { 8 | invalid: this.invalid 9 | }); 10 | }, 11 | 12 | "per validator": { 13 | setUp: function() { 14 | this.model.validation = { 15 | email: [{ 16 | required: true, 17 | msg: 'required' 18 | },{ 19 | pattern: 'email', 20 | msg: function() { 21 | return 'pattern'; 22 | } 23 | }] 24 | }; 25 | }, 26 | 27 | "and violating first validator returns msg specified for first validator": function() { 28 | this.model.set({email: ''}, {validate: true}); 29 | 30 | assert.calledWith(this.invalid, this.view, 'email', 'required'); 31 | }, 32 | 33 | "and violating second validator returns msg specified for second validator": function() { 34 | this.model.set({email: 'a'}, {validate: true}); 35 | 36 | assert.calledWith(this.invalid, this.view, 'email', 'pattern'); 37 | } 38 | }, 39 | 40 | "per attribute": { 41 | setUp: function() { 42 | this.model.validation = { 43 | email: { 44 | required: true, 45 | pattern: 'email', 46 | msg: 'error' 47 | } 48 | }; 49 | }, 50 | 51 | "and violating first validator returns msg specified for attribute": function() { 52 | this.model.set({email: ''}, {validate: true}); 53 | 54 | assert.calledWith(this.invalid, this.view, 'email', 'error'); 55 | }, 56 | 57 | "and violating second validator returns msg specified for attribute": function() { 58 | this.model.set({email: 'a'}, {validate: true}); 59 | 60 | assert.calledWith(this.invalid, this.view, 'email', 'error'); 61 | } 62 | } 63 | }); -------------------------------------------------------------------------------- /tests/preValidate.js: -------------------------------------------------------------------------------- 1 | buster.testCase("preValidate", { 2 | "when model has not defined any validation": { 3 | setUp: function() { 4 | this.model = new Backbone.Model(); 5 | 6 | Backbone.Validation.bind(new Backbone.View({model: this.model})); 7 | }, 8 | 9 | "returns nothing": function() { 10 | refute(this.model.preValidate('attr', 'value')); 11 | } 12 | }, 13 | 14 | "when model has defined validation": { 15 | setUp: function() { 16 | var Model = Backbone.Model.extend({ 17 | validation: { 18 | name: { 19 | required: true 20 | }, 21 | address: { 22 | required: true, 23 | }, 24 | authenticated: { 25 | required: false 26 | } 27 | } 28 | }); 29 | this.model = new Model(); 30 | Backbone.Validation.bind(new Backbone.View({model: this.model})); 31 | }, 32 | 33 | "and pre-validating single attribute": { 34 | "returns error message when value is not valid": function() { 35 | assert(this.model.preValidate('name', '')); 36 | }, 37 | 38 | "returns nothing when value is valid": function() { 39 | refute(this.model.preValidate('name', 'name')); 40 | }, 41 | 42 | "returns nothing when attribute pre-validated has no validation": function(){ 43 | refute(this.model.preValidate('age', 2)); 44 | }, 45 | 46 | "handles null value": function() { 47 | refute(this.model.preValidate('authenticated', null)); 48 | } 49 | }, 50 | 51 | "and pre-validating hash of attributes": { 52 | "returns error object when value is not valid": function() { 53 | var result = this.model.preValidate({name: '', address: 'address'}); 54 | assert(result.name); 55 | refute(result.address); 56 | }, 57 | 58 | "returns error object when values are not valid": function() { 59 | var result = this.model.preValidate({name: '', address: ''}); 60 | assert(result.name); 61 | assert(result.address); 62 | }, 63 | 64 | "returns nothing when value is valid": function() { 65 | refute(this.model.preValidate({name: 'name'})); 66 | } 67 | } 68 | } 69 | }); -------------------------------------------------------------------------------- /tests/customCallbacks.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Overriding default callbacks in Backbone.Validation", { 2 | setUp: function() { 3 | this.originalCallbacks = {}; 4 | _.extend(this.originalCallbacks, Backbone.Validation.callbacks); 5 | 6 | this.valid = this.spy(); 7 | this.invalid = this.spy(); 8 | 9 | _.extend(Backbone.Validation.callbacks, { 10 | valid: this.valid, 11 | invalid: this.invalid 12 | }); 13 | 14 | var Model = Backbone.Model.extend({ 15 | validation: { 16 | age: function(val) { 17 | if (val === 0) { 18 | return "Age is invalid"; 19 | } 20 | } 21 | } 22 | }); 23 | 24 | this.model = new Model(); 25 | 26 | Backbone.Validation.bind(new Backbone.View({ 27 | model: this.model 28 | })); 29 | }, 30 | 31 | tearDown: function(){ 32 | _.extend(Backbone.Validation.callbacks, this.originalCallbacks); 33 | }, 34 | 35 | "validate should call overridden valid callback": function() { 36 | this.model.set({ 37 | age: 1 38 | }, {validate: true}); 39 | 40 | assert.called(this.valid); 41 | }, 42 | 43 | "validate should call overridden invalid callback": function() { 44 | this.model.set({ 45 | age: 0 46 | }, {validate: true}); 47 | 48 | assert.called(this.invalid); 49 | }, 50 | 51 | "isValid(true) should call overridden valid callback": function() { 52 | this.model.set({ 53 | age: 1 54 | }); 55 | this.model.isValid(true); 56 | assert.called(this.valid); 57 | }, 58 | 59 | "isValid(true) should call overridden invalid callback": function() { 60 | this.model.set({ 61 | age: 0 62 | }); 63 | this.model.isValid(true); 64 | assert.called(this.invalid); 65 | }, 66 | 67 | "isValid([]) should call overridden valid callback": function() { 68 | this.model.set({ 69 | age: 1 70 | }); 71 | this.model.isValid(['age']); 72 | assert.called(this.valid); 73 | }, 74 | 75 | "isValid([]) should call overridden invalid callback": function() { 76 | this.model.set({ 77 | age: 0 78 | }); 79 | this.model.isValid(['age']); 80 | assert.called(this.invalid); 81 | } 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /tests/mixin.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Mixin validation", { 2 | setUp: function() { 3 | this.origPrototype = _.clone(Backbone.Model.prototype); 4 | 5 | _.extend(Backbone.Model.prototype, Backbone.Validation.mixin); 6 | 7 | this.Model = Backbone.Model.extend({ 8 | validation: { 9 | name: function(val) { 10 | if(!val) { 11 | return 'error'; 12 | } 13 | } 14 | } 15 | }); 16 | 17 | this.model = new this.Model(); 18 | }, 19 | 20 | tearDown: function() { 21 | Backbone.Model.prototype = this.origPrototype; 22 | }, 23 | 24 | "isValid is undefined when no validation has occurred": function() { 25 | refute.defined(new this.Model().isValid()); 26 | }, 27 | 28 | "isValid is false when model is invalid": function() { 29 | assert.equals(false, this.model.isValid(true)); 30 | }, 31 | 32 | "isValid is true when model is valid": function() { 33 | this.model.set({name: 'name'}); 34 | 35 | assert.equals(true, this.model.isValid(true)); 36 | }, 37 | 38 | "refutes setting invalid value": function() { 39 | refute(this.model.set({name: ''}, {validate: true})); 40 | }, 41 | 42 | "succeeds setting valid value": function() { 43 | assert(this.model.set({name: 'name'}, {validate: true})); 44 | }, 45 | 46 | "when forcing update succeeds setting invalid value": function() { 47 | assert(this.model.set({name:''}, {forceUpdate: true, validate: true})); 48 | }, 49 | 50 | "when forcing update globally": { 51 | setUp: function() { 52 | Backbone.Validation.configure({ 53 | forceUpdate: true 54 | }); 55 | }, 56 | 57 | tearDown: function() { 58 | Backbone.Validation.configure({ 59 | forceUpdate: false 60 | }); 61 | }, 62 | 63 | "succeeds setting invalid value when forcing update globally": function() { 64 | assert(this.model.set({name:''}, {validate: true})); 65 | } 66 | }, 67 | 68 | "when setting attribute on model without validation": { 69 | setUp: function(){ 70 | this.model = new Backbone.Model(); 71 | }, 72 | 73 | "it should not complain": function() { 74 | assert(this.model.set({someAttr: 'someValue'}, {validate: true})); 75 | } 76 | } 77 | }); -------------------------------------------------------------------------------- /docco/docco.jst: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title %> 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 |
24 | <% if (sources.length > 1) { %> 25 | 40 | <% } %> 41 | 65 |
66 | 67 | -------------------------------------------------------------------------------- /tests/validators/oneOf.js: -------------------------------------------------------------------------------- 1 | buster.testCase("oneOf validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | country: { 7 | oneOf: ['Norway', 'Sweeden'] 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({country: 'Country must be one of: Norway, Sweeden' }, error); 26 | done(); 27 | }); 28 | this.model.set({country:''}, {validate: true}); 29 | }, 30 | 31 | "value is one of the values in the array is valid": function(){ 32 | assert(this.model.set({ 33 | country: 'Norway' 34 | }, {validate: true})); 35 | }, 36 | 37 | "value is not one of the values in the arraye is invalid": function(){ 38 | refute(this.model.set({ 39 | country: 'Denmark' 40 | }, {validate: true})); 41 | }, 42 | 43 | "is case sensitive": function(){ 44 | refute(this.model.set({ 45 | country: 'sweeden' 46 | }, {validate: true})); 47 | }, 48 | 49 | "when required is not specified": { 50 | "undefined is invalid": function() { 51 | refute(this.model.set({ 52 | country: undefined 53 | }, {validate: true})); 54 | }, 55 | 56 | "null is invalid": function() { 57 | refute(this.model.set({ 58 | country: null 59 | }, {validate: true})); 60 | } 61 | }, 62 | 63 | "when required:false": { 64 | setUp: function() { 65 | this.model.validation.country.required = false; 66 | }, 67 | 68 | "null is valid": function() { 69 | assert(this.model.set({ 70 | country: null 71 | }, {validate: true})); 72 | }, 73 | 74 | "undefined is valid": function() { 75 | assert(this.model.set({ 76 | country: undefined 77 | }, {validate: true})); 78 | } 79 | }, 80 | 81 | "when required:true": { 82 | setUp: function() { 83 | this.model.validation.country.required = true; 84 | }, 85 | 86 | "undefined is invalid": function() { 87 | refute(this.model.set({ 88 | country: undefined 89 | }, {validate: true})); 90 | }, 91 | 92 | "null is invalid": function() { 93 | refute(this.model.set({ 94 | country: null 95 | }, {validate: true})); 96 | } 97 | } 98 | }); -------------------------------------------------------------------------------- /tests/labelFormatter.js: -------------------------------------------------------------------------------- 1 | buster.testCase('Label formatters', { 2 | "Attribute names on the model can be formatted in error messages using": { 3 | setUp: function() { 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | someAttribute: { 7 | required: true 8 | }, 9 | some_attribute: { 10 | required: true 11 | }, 12 | some_other_attribute: { 13 | required: true 14 | } 15 | }, 16 | 17 | labels: { 18 | someAttribute: 'Custom label' 19 | } 20 | }); 21 | 22 | this.model = new Model(); 23 | _.extend(this.model, Backbone.Validation.mixin); 24 | }, 25 | 26 | tearDown: function() { 27 | // Reset to default formatter 28 | Backbone.Validation.configure({ 29 | labelFormatter: 'sentenceCase' 30 | }); 31 | }, 32 | 33 | "no formatting": { 34 | setUp: function() { 35 | Backbone.Validation.configure({ 36 | labelFormatter: 'none' 37 | }); 38 | }, 39 | 40 | "returns the attribute name": function(){ 41 | assert.equals('someAttribute is required', this.model.preValidate('someAttribute', '')); 42 | } 43 | }, 44 | 45 | "label formatting": { 46 | setUp: function() { 47 | Backbone.Validation.configure({ 48 | labelFormatter: 'label' 49 | }); 50 | }, 51 | 52 | "looks up a label on the model": function(){ 53 | assert.equals('Custom label is required', this.model.preValidate('someAttribute', '')); 54 | }, 55 | 56 | "returns sentence cased name when label is not found": function(){ 57 | assert.equals('Some attribute is required', this.model.preValidate('some_attribute', '')); 58 | }, 59 | 60 | "returns sentence cased name when label attribute is not defined": function(){ 61 | var Model = Backbone.Model.extend({ 62 | validation: { 63 | someAttribute: { 64 | required: true 65 | } 66 | } 67 | }); 68 | 69 | var model = new Model(); 70 | _.extend(model, Backbone.Validation.mixin); 71 | 72 | assert.equals('Some attribute is required', model.preValidate('someAttribute', '')); 73 | } 74 | }, 75 | 76 | "sentence formatting": { 77 | setUp: function() { 78 | Backbone.Validation.configure({ 79 | labelFormatter: 'sentenceCase' 80 | }); 81 | }, 82 | 83 | "sentence cases camel cased attribute name": function(){ 84 | assert.equals('Some attribute is required', this.model.preValidate('someAttribute', '')); 85 | }, 86 | 87 | "sentence cases underscore named attribute name": function(){ 88 | assert.equals('Some attribute is required', this.model.preValidate('some_attribute', '')); 89 | }, 90 | 91 | "sentence cases underscore named attribute name with multiple underscores": function(){ 92 | assert.equals('Some other attribute is required', this.model.preValidate('some_other_attribute', '')); 93 | } 94 | } 95 | } 96 | }); -------------------------------------------------------------------------------- /tests/isValid.js: -------------------------------------------------------------------------------- 1 | buster.testCase("isValid", { 2 | "when model has not defined any validation": { 3 | setUp: function() { 4 | this.model = new Backbone.Model(); 5 | 6 | Backbone.Validation.bind(new Backbone.View({model: this.model})); 7 | }, 8 | 9 | "returns true": function() { 10 | assert(this.model.isValid()); 11 | } 12 | }, 13 | 14 | "when model has defined validation": { 15 | setUp: function() { 16 | var Model = Backbone.Model.extend({ 17 | validation: { 18 | name: { 19 | required: true 20 | } 21 | } 22 | }); 23 | this.model = new Model(); 24 | Backbone.Validation.bind(new Backbone.View({model: this.model})); 25 | }, 26 | 27 | "returns undefined when model is never validated": function() { 28 | refute.defined(this.model.isValid()); 29 | }, 30 | 31 | "returns true when model is valid": function() { 32 | this.model.set({name: 'name'}, {validate: true}); 33 | 34 | assert(this.model.isValid()); 35 | }, 36 | 37 | "returns false when model is invalid": function() { 38 | this.model.set({name: ''}, {validate: true}); 39 | 40 | refute(this.model.isValid()); 41 | }, 42 | 43 | "can force validation by passing true": function() { 44 | refute.defined(this.model.isValid()); 45 | assert(this.model.isValid(true) === false); 46 | }, 47 | 48 | "invalid is triggered when model is invalid": function(done) { 49 | this.model.bind('invalid', function(model, attrs) { 50 | done(); 51 | }); 52 | refute(this.model.isValid(true)); 53 | }, 54 | 55 | "and passing name of attribute": { 56 | setUp: function() { 57 | this.model.validation = { 58 | name: { 59 | required: true 60 | }, 61 | age: { 62 | required: true 63 | } 64 | }; 65 | }, 66 | 67 | "returns false when attribute is invalid": function() { 68 | refute(this.model.isValid('name')); 69 | }, 70 | 71 | "invalid is triggered when attribute is invalid": function(done) { 72 | this.model.bind('invalid', function(model, attrs) { 73 | done(); 74 | }); 75 | refute(this.model.isValid('name')); 76 | }, 77 | 78 | "returns true when attribute is valid": function() { 79 | this.model.set({name: 'name'}); 80 | 81 | assert(this.model.isValid('name')); 82 | } 83 | }, 84 | 85 | "and passing array of attributes": { 86 | setUp: function() { 87 | this.model.validation = { 88 | name: { 89 | required: true 90 | }, 91 | age: { 92 | required: true 93 | }, 94 | phone: { 95 | required: true 96 | } 97 | }; 98 | }, 99 | 100 | "returns false when all attributes are invalid": function() { 101 | refute(this.model.isValid(['name', 'age'])); 102 | }, 103 | 104 | "returns false when one attribute is invalid": function() { 105 | this.model.set({name: 'name'}); 106 | 107 | refute(this.model.isValid(['name', 'age'])); 108 | }, 109 | 110 | "returns true when all attributes are valid": function() { 111 | this.model.set({name: 'name', age: 1 }); 112 | 113 | assert(this.model.isValid(['name', 'age'])); 114 | } 115 | } 116 | } 117 | }); -------------------------------------------------------------------------------- /tests/validators/max.js: -------------------------------------------------------------------------------- 1 | buster.testCase("max validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | age: { 7 | max: 10 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({age: 'Age must be less than or equal to 10'}, error); 26 | done(); 27 | }); 28 | this.model.set({age:11}, {validate: true}); 29 | }, 30 | 31 | "number higher than max is invalid": function() { 32 | refute(this.model.set({ 33 | age: 11 34 | }, {validate: true})); 35 | }, 36 | 37 | "non numeric value is invalid": function() { 38 | refute(this.model.set({ 39 | age: '10error' 40 | }, {validate: true})); 41 | }, 42 | 43 | "number equal to max is valid": function() { 44 | assert(this.model.set({ 45 | age: 10 46 | }, {validate: true})); 47 | }, 48 | 49 | "number lower than max is valid": function() { 50 | assert(this.model.set({ 51 | age: 5 52 | }, {validate: true})); 53 | }, 54 | 55 | "numeric string values are treated as numbers": function() { 56 | assert(this.model.set({ 57 | age: '10' 58 | }, {validate: true})); 59 | }, 60 | 61 | "when required is not specified": { 62 | "undefined is invalid": function() { 63 | refute(this.model.set({ 64 | age: undefined 65 | }, {validate: true})); 66 | }, 67 | 68 | "null is invalid": function() { 69 | refute(this.model.set({ 70 | age: null 71 | }, {validate: true})); 72 | } 73 | }, 74 | 75 | "when required:false": { 76 | setUp: function() { 77 | this.model.validation.age.required = false; 78 | }, 79 | 80 | "null is valid": function() { 81 | assert(this.model.set({ 82 | age: null 83 | }, {validate: true})); 84 | }, 85 | 86 | "undefined is valid": function() { 87 | assert(this.model.set({ 88 | age: undefined 89 | }, {validate: true})); 90 | } 91 | }, 92 | 93 | "when required:true": { 94 | setUp: function() { 95 | this.model.validation.age.required = true; 96 | }, 97 | 98 | "undefined is invalid": function() { 99 | refute(this.model.set({ 100 | age: undefined 101 | }, {validate: true})); 102 | }, 103 | 104 | "null is invalid": function() { 105 | refute(this.model.set({ 106 | age: null 107 | }, {validate: true})); 108 | } 109 | } 110 | }); -------------------------------------------------------------------------------- /tests/validators/min.js: -------------------------------------------------------------------------------- 1 | buster.testCase("min validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | age: { 7 | min: 1 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({age: 'Age must be greater than or equal to 1'}, error); 26 | done(); 27 | }); 28 | this.model.set({age: 0}, {validate: true}); 29 | }, 30 | 31 | "number lower than min is invalid": function() { 32 | refute(this.model.set({ 33 | age: 0 34 | }, {validate: true})); 35 | }, 36 | 37 | "non numeric value is invalid": function() { 38 | refute(this.model.set({ 39 | age: '10error' 40 | }, {validate: true})); 41 | }, 42 | 43 | "number equal to min is valid": function() { 44 | assert(this.model.set({ 45 | age: 1 46 | }, {validate: true})); 47 | }, 48 | 49 | "number greater than min is valid": function() { 50 | assert(this.model.set({ 51 | age: 2 52 | }, {validate: true})); 53 | }, 54 | 55 | "numeric string values are treated as numbers": function() { 56 | assert(this.model.set({ 57 | age: '1' 58 | }, {validate: true})); 59 | }, 60 | 61 | "when required is not specified": { 62 | "undefined is invalid": function() { 63 | refute(this.model.set({ 64 | age: undefined 65 | }, {validate: true})); 66 | }, 67 | 68 | "null is invalid": function() { 69 | refute(this.model.set({ 70 | age: null 71 | }, {validate: true})); 72 | } 73 | }, 74 | 75 | "when required:false": { 76 | setUp: function() { 77 | this.model.validation.age.required = false; 78 | }, 79 | 80 | "null is valid": function() { 81 | assert(this.model.set({ 82 | age: null 83 | }, {validate: true})); 84 | }, 85 | 86 | "undefined is valid": function() { 87 | assert(this.model.set({ 88 | age: undefined 89 | }, {validate: true})); 90 | } 91 | }, 92 | 93 | "when required:true": { 94 | setUp: function() { 95 | this.model.validation.age.required = true; 96 | }, 97 | 98 | "undefined is invalid": function() { 99 | refute(this.model.set({ 100 | age: undefined 101 | }, {validate: true})); 102 | }, 103 | 104 | "null is invalid": function() { 105 | refute(this.model.set({ 106 | age: null 107 | }, {validate: true})); 108 | } 109 | } 110 | }); -------------------------------------------------------------------------------- /tests/validators/range.js: -------------------------------------------------------------------------------- 1 | buster.testCase("range validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | age: { 7 | range: [1, 10] 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({age: 'Age must be between 1 and 10'}, error); 26 | done(); 27 | }); 28 | this.model.set({age:0}, {validate: true}); 29 | }, 30 | 31 | "number lower than first value is invalid": function() { 32 | refute(this.model.set({ 33 | age: 0 34 | }, {validate: true})); 35 | }, 36 | 37 | "number equal to first value is valid": function() { 38 | assert(this.model.set({ 39 | age: 1 40 | }, {validate: true})); 41 | }, 42 | 43 | "number higher than last value is invalid": function() { 44 | refute(this.model.set({ 45 | age: 11 46 | }, {validate: true})); 47 | }, 48 | 49 | "number equal to last value is valid": function() { 50 | assert(this.model.set({ 51 | age: 10 52 | }, {validate: true})); 53 | }, 54 | 55 | "number in range is valid": function() { 56 | assert(this.model.set({ 57 | age: 5 58 | }, {validate: true})); 59 | }, 60 | 61 | "when required is not specified": { 62 | "undefined is invalid": function() { 63 | refute(this.model.set({ 64 | age: undefined 65 | }, {validate: true})); 66 | }, 67 | 68 | "null is invalid": function() { 69 | refute(this.model.set({ 70 | age: null 71 | }, {validate: true})); 72 | } 73 | }, 74 | 75 | "when required:false": { 76 | setUp: function() { 77 | this.model.validation.age.required = false; 78 | }, 79 | 80 | "null is valid": function() { 81 | assert(this.model.set({ 82 | age: null 83 | }, {validate: true})); 84 | }, 85 | 86 | "undefined is valid": function() { 87 | assert(this.model.set({ 88 | age: undefined 89 | }, {validate: true})); 90 | } 91 | }, 92 | 93 | "when required:true": { 94 | setUp: function() { 95 | this.model.validation.age.required = true; 96 | }, 97 | 98 | "undefined is invalid": function() { 99 | refute(this.model.set({ 100 | age: undefined 101 | }, {validate: true})); 102 | }, 103 | 104 | "null is invalid": function() { 105 | refute(this.model.set({ 106 | age: null 107 | }, {validate: true})); 108 | } 109 | } 110 | }); -------------------------------------------------------------------------------- /tests/validators/minLength.js: -------------------------------------------------------------------------------- 1 | buster.testCase("minLength validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | name: { 7 | minLength: 2 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message for string": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({name: 'Name must be at least 2 characters'}, error); 26 | done(); 27 | }); 28 | this.model.set({name:''}, {validate: true}); 29 | }, 30 | 31 | "string with length shorter than minLenght is invalid": function() { 32 | refute(this.model.set({ 33 | name: 'a' 34 | }, {validate: true})); 35 | }, 36 | 37 | "string with length equal to minLength is valid": function() { 38 | assert(this.model.set({ 39 | name: 'aa' 40 | }, {validate: true})); 41 | }, 42 | 43 | "string with length greater than minLength is valid": function() { 44 | assert(this.model.set({ 45 | name: 'aaaa' 46 | }, {validate: true})); 47 | }, 48 | 49 | "spaces are treated as part of the string (no trimming)": function() { 50 | assert(this.model.set({ 51 | name: 'a ' 52 | }, {validate: true})); 53 | }, 54 | 55 | "non strings are treated as an error": function() { 56 | refute(this.model.set({ 57 | name: 123 58 | }, {validate: true})); 59 | }, 60 | 61 | "when required is not specified": { 62 | "undefined is invalid": function() { 63 | refute(this.model.set({ 64 | name: undefined 65 | }, {validate: true})); 66 | }, 67 | 68 | "null is invalid": function() { 69 | refute(this.model.set({ 70 | name: null 71 | }, {validate: true})); 72 | } 73 | }, 74 | 75 | "when required:false": { 76 | setUp: function() { 77 | this.model.validation.name.required = false; 78 | }, 79 | 80 | "null is valid": function() { 81 | assert(this.model.set({ 82 | name: null 83 | }, {validate: true})); 84 | }, 85 | 86 | "undefined is valid": function() { 87 | assert(this.model.set({ 88 | name: undefined 89 | }, {validate: true})); 90 | } 91 | }, 92 | 93 | "when required:true": { 94 | setUp: function() { 95 | this.model.validation.name.required = true; 96 | }, 97 | 98 | "undefined is invalid": function() { 99 | refute(this.model.set({ 100 | name: undefined 101 | }, {validate: true})); 102 | }, 103 | 104 | "null is invalid": function() { 105 | refute(this.model.set({ 106 | name: null 107 | }, {validate: true})); 108 | } 109 | } 110 | }); -------------------------------------------------------------------------------- /tests/validators/maxLength.js: -------------------------------------------------------------------------------- 1 | buster.testCase("maxLength validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | name: { 7 | maxLength: 2 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message for string": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({name: 'Name must be at most 2 characters'}, error); 26 | done(); 27 | }); 28 | this.model.set({name:'aaa'}, {validate: true}); 29 | }, 30 | 31 | "string with length longer than maxLenght is invalid": function() { 32 | refute(this.model.set({ 33 | name: 'aaa' 34 | }, {validate: true})); 35 | }, 36 | 37 | "string with length equal to maxLength is valid": function() { 38 | assert(this.model.set({ 39 | name: 'aa' 40 | }, {validate: true})); 41 | }, 42 | 43 | "string with length shorter than maxLength is valid": function() { 44 | assert(this.model.set({ 45 | name: 'a' 46 | }, {validate: true})); 47 | }, 48 | 49 | "spaces are treated as part of the string (no trimming)": function() { 50 | refute(this.model.set({ 51 | name: 'a ' 52 | }, {validate: true})); 53 | }, 54 | 55 | "non strings are treated as an error": function() { 56 | refute(this.model.set({ 57 | name: 123 58 | }, {validate: true})); 59 | }, 60 | 61 | "when required is not specified": { 62 | "undefined is invalid": function() { 63 | refute(this.model.set({ 64 | name: undefined 65 | }, {validate: true})); 66 | }, 67 | 68 | "null is invalid": function() { 69 | refute(this.model.set({ 70 | name: null 71 | }, {validate: true})); 72 | } 73 | }, 74 | 75 | "when required:false": { 76 | setUp: function() { 77 | this.model.validation.name.required = false; 78 | }, 79 | 80 | "null is valid": function() { 81 | assert(this.model.set({ 82 | name: null 83 | }, {validate: true})); 84 | }, 85 | 86 | "undefined is valid": function() { 87 | assert(this.model.set({ 88 | name: undefined 89 | }, {validate: true})); 90 | } 91 | }, 92 | 93 | "when required:true": { 94 | setUp: function() { 95 | this.model.validation.name.required = true; 96 | }, 97 | 98 | "undefined is invalid": function() { 99 | refute(this.model.set({ 100 | name: undefined 101 | }, {validate: true})); 102 | }, 103 | 104 | "null is invalid": function() { 105 | refute(this.model.set({ 106 | name: null 107 | }, {validate: true})); 108 | } 109 | } 110 | }); -------------------------------------------------------------------------------- /tests/validators/patterns.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Backbone.Validation patterns", { 2 | setUp: function() { 3 | var that = this; 4 | this.valid = function(value) { 5 | assert(value.match(that.pattern), value + ' should be valid'); 6 | }; 7 | 8 | this.invalid = function(value) { 9 | refute(value.match(that.pattern), value + ' should be invalid'); 10 | }; 11 | }, 12 | 13 | "email pattern matches all valid email addresses": function() { 14 | this.pattern = Backbone.Validation.patterns.email; 15 | 16 | this.valid('name@example.com'); 17 | this.valid('name@example.com'); 18 | this.valid('name+@example.co'); 19 | this.valid('n@e.co'); 20 | this.valid('first.last@backbone.example.com'); 21 | 22 | this.invalid('name'); 23 | this.invalid('name@'); 24 | this.invalid('name@example'); 25 | this.invalid('name.@example.c'); 26 | this.invalid('name,@example.c'); 27 | this.invalid('name;@example.c'); 28 | this.invalid('name@example.com.'); 29 | }, 30 | 31 | "email pattern is case insensitive": function() { 32 | this.pattern = Backbone.Validation.patterns.email; 33 | 34 | this.valid('NaMe@example.COM'); 35 | this.valid('NAME@EXAMPLE.COM'); 36 | }, 37 | 38 | "url pattern matches all valid urls": function() { 39 | this.pattern = Backbone.Validation.patterns.url; 40 | 41 | this.valid('http://thedersen.com'); 42 | this.valid('http://www.thedersen.com/'); 43 | this.valid('http://øya.no/'); 44 | this.valid('http://öya.no/'); 45 | this.valid('https://thedersen.com/'); 46 | this.valid('http://thedersen.com/backbone.validation/?query=string'); 47 | this.valid('ftp://thedersen.com'); 48 | this.valid('http://127.0.0.1'); 49 | 50 | this.invalid('thedersen.com'); 51 | this.invalid('http://thedersen'); 52 | this.invalid('http://thedersen.'); 53 | this.invalid('http://thedersen,com'); 54 | this.invalid('http://thedersen;com'); 55 | this.invalid('http://.thedersen.com'); 56 | this.invalid('http://127.0.0.1.'); 57 | }, 58 | 59 | "url pattern is case insensitive": function() { 60 | this.pattern = Backbone.Validation.patterns.url; 61 | 62 | this.valid('http://Thedersen.com'); 63 | this.valid('HTTP://THEDERSEN.COM'); 64 | }, 65 | 66 | "number pattern matches all numbers, including decimal numbers": function() { 67 | this.pattern = Backbone.Validation.patterns.number; 68 | 69 | this.valid('123'); 70 | this.valid('-123'); 71 | this.valid('123,000'); 72 | this.valid('-123,000'); 73 | this.valid('123.45'); 74 | this.valid('-123.45'); 75 | this.valid('123,000.45'); 76 | this.valid('-123,000.45'); 77 | this.valid('123,000.00'); 78 | this.valid('-123,000.00'); 79 | 80 | this.invalid('abc'); 81 | this.invalid('abc123'); 82 | this.invalid('123abc'); 83 | this.invalid('123.000,00'); 84 | this.invalid('123.0.0,00'); 85 | }, 86 | 87 | "digits pattern matches single or multiple digits": function() { 88 | this.pattern = Backbone.Validation.patterns.digits; 89 | 90 | this.valid('1'); 91 | this.valid('123'); 92 | 93 | this.invalid('a'); 94 | this.invalid('a123'); 95 | this.invalid('123a'); 96 | } 97 | }); -------------------------------------------------------------------------------- /tests/validators/length.js: -------------------------------------------------------------------------------- 1 | buster.testCase("length validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | postalCode: { 7 | length: 2 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message for string": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({postalCode: 'Postal code must be 2 characters'}, error); 26 | done(); 27 | }); 28 | this.model.set({postalCode:''}, {validate: true}); 29 | }, 30 | 31 | "string with length shorter than length is invalid": function() { 32 | refute(this.model.set({ 33 | postalCode: 'a' 34 | }, {validate: true})); 35 | }, 36 | 37 | "string with length longer than length is invalid": function() { 38 | refute(this.model.set({ 39 | postalCode: 'aaa' 40 | }, {validate: true})); 41 | }, 42 | 43 | "string with length equal to length is valid": function() { 44 | assert(this.model.set({ 45 | postalCode: 'aa' 46 | }, {validate: true})); 47 | }, 48 | 49 | "spaces are treated as part of the string (no trimming)": function() { 50 | refute(this.model.set({ 51 | postalCode: 'aa ' 52 | }, {validate: true})); 53 | }, 54 | 55 | "non strings are treated as an error": function() { 56 | refute(this.model.set({ 57 | postalCode: 123 58 | }, {validate: true})); 59 | }, 60 | 61 | "when required is not specified": { 62 | "undefined is invalid": function() { 63 | refute(this.model.set({ 64 | postalCode: undefined 65 | }, {validate: true})); 66 | }, 67 | 68 | "null is invalid": function() { 69 | refute(this.model.set({ 70 | postalCode: null 71 | }, {validate: true})); 72 | } 73 | }, 74 | 75 | "when required:false": { 76 | setUp: function() { 77 | this.model.validation.postalCode.required = false; 78 | }, 79 | 80 | "null is valid": function() { 81 | assert(this.model.set({ 82 | postalCode: null 83 | }, {validate: true})); 84 | }, 85 | 86 | "undefined is valid": function() { 87 | assert(this.model.set({ 88 | postalCode: undefined 89 | }, {validate: true})); 90 | } 91 | }, 92 | 93 | "when required:true": { 94 | setUp: function() { 95 | this.model.validation.postalCode.required = true; 96 | }, 97 | 98 | "undefined is invalid": function() { 99 | refute(this.model.set({ 100 | postalCode: undefined 101 | }, {validate: true})); 102 | }, 103 | 104 | "null is invalid": function() { 105 | refute(this.model.set({ 106 | postalCode: null 107 | }, {validate: true})); 108 | } 109 | } 110 | }); -------------------------------------------------------------------------------- /tests/validators/pattern.js: -------------------------------------------------------------------------------- 1 | buster.testCase("pattern validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | name: { 7 | pattern: /^test/ 8 | }, 9 | email: { 10 | pattern: 'email' 11 | } 12 | } 13 | }); 14 | 15 | this.model = new Model({ 16 | name: 'test', 17 | email: 'test@example.com' 18 | }); 19 | this.view = new Backbone.View({ 20 | model: this.model 21 | }); 22 | 23 | Backbone.Validation.bind(this.view, { 24 | valid: this.spy(), 25 | invalid: this.spy() 26 | }); 27 | }, 28 | 29 | "has default error message": function(done) { 30 | this.model.bind('validated:invalid', function(model, error){ 31 | assert.equals({email: 'Email must be a valid email'}, error); 32 | done(); 33 | }); 34 | this.model.set({email:''}, {validate: true}); 35 | }, 36 | 37 | "has default error message for inline pattern": function(done) { 38 | this.model.bind('validated:invalid', function(model, error){ 39 | assert.equals({name: 'Name is invalid'}, error); 40 | done(); 41 | }); 42 | this.model.set({name:''}, {validate: true}); 43 | }, 44 | 45 | "value not matching pattern is invalid": function() { 46 | refute(this.model.set({ 47 | name: 'aaa' 48 | }, {validate: true})); 49 | }, 50 | 51 | "value matching pattern is valid": function() { 52 | assert(this.model.set({ 53 | name: 'test' 54 | }, {validate: true})); 55 | }, 56 | 57 | "when required is not specified": { 58 | "undefined is invalid": function() { 59 | refute(this.model.set({ 60 | name: undefined 61 | }, {validate: true})); 62 | }, 63 | 64 | "null is invalid": function() { 65 | refute(this.model.set({ 66 | name: null 67 | }, {validate: true})); 68 | } 69 | }, 70 | 71 | "when required:false": { 72 | setUp: function() { 73 | this.model.validation.name.required = false; 74 | }, 75 | 76 | "null is valid": function() { 77 | assert(this.model.set({ 78 | name: null 79 | }, {validate: true})); 80 | }, 81 | 82 | "undefined is valid": function() { 83 | assert(this.model.set({ 84 | name: undefined 85 | }, {validate: true})); 86 | } 87 | }, 88 | 89 | "when required:true": { 90 | setUp: function() { 91 | this.model.validation.name.required = true; 92 | }, 93 | 94 | "undefined is invalid": function() { 95 | refute(this.model.set({ 96 | name: undefined 97 | }, {validate: true})); 98 | }, 99 | 100 | "null is invalid": function() { 101 | refute(this.model.set({ 102 | name: null 103 | }, {validate: true})); 104 | } 105 | }, 106 | 107 | "can use one of the built-in patterns by specifying the name of it": function(){ 108 | refute(this.model.set({ 109 | email: 'aaa' 110 | }, {validate: true})); 111 | 112 | assert(this.model.set({ 113 | email: 'a@example.com' 114 | }, {validate: true})); 115 | } 116 | }); -------------------------------------------------------------------------------- /tests/validators/rangeLength.js: -------------------------------------------------------------------------------- 1 | buster.testCase("rangeLength validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | name: { 7 | rangeLength: [2, 4] 8 | } 9 | } 10 | }); 11 | 12 | this.model = new Model(); 13 | this.view = new Backbone.View({ 14 | model: this.model 15 | }); 16 | 17 | Backbone.Validation.bind(this.view, { 18 | valid: this.spy(), 19 | invalid: this.spy() 20 | }); 21 | }, 22 | 23 | "has default error message for strings": function(done) { 24 | this.model.bind('validated:invalid', function(model, error){ 25 | assert.equals({name: 'Name must be between 2 and 4 characters'}, error); 26 | done(); 27 | }); 28 | this.model.set({name:'a'}, {validate: true}); 29 | }, 30 | 31 | "string with length shorter than first value is invalid": function() { 32 | refute(this.model.set({ 33 | name: 'a' 34 | }, {validate: true})); 35 | }, 36 | 37 | "string with length equal to first value is valid": function() { 38 | assert(this.model.set({ 39 | name: 'aa' 40 | }, {validate: true})); 41 | }, 42 | 43 | "string with length longer than last value is invalid": function() { 44 | refute(this.model.set({ 45 | name: 'aaaaa' 46 | }, {validate: true})); 47 | }, 48 | 49 | "string with length equal to last value is valid": function() { 50 | assert(this.model.set({ 51 | name: 'aaaa' 52 | }, {validate: true})); 53 | }, 54 | 55 | "string with length within range is valid": function() { 56 | assert(this.model.set({ 57 | name: 'aaa' 58 | }, {validate: true})); 59 | }, 60 | 61 | "spaces are treated as part of the string (no trimming)": function() { 62 | refute(this.model.set({ 63 | name: 'aaaa ' 64 | }, {validate: true})); 65 | }, 66 | 67 | "non strings are treated as an error": function() { 68 | refute(this.model.set({ 69 | name: 123 70 | }, {validate: true})); 71 | }, 72 | 73 | "when required is not specified": { 74 | "undefined is invalid": function() { 75 | refute(this.model.set({ 76 | name: undefined 77 | }, {validate: true})); 78 | }, 79 | 80 | "null is invalid": function() { 81 | refute(this.model.set({ 82 | name: null 83 | }, {validate: true})); 84 | } 85 | }, 86 | 87 | "when required:false": { 88 | setUp: function() { 89 | this.model.validation.name.required = false; 90 | }, 91 | 92 | "null is valid": function() { 93 | assert(this.model.set({ 94 | name: null 95 | }, {validate: true})); 96 | }, 97 | 98 | "undefined is valid": function() { 99 | assert(this.model.set({ 100 | name: undefined 101 | }, {validate: true})); 102 | } 103 | }, 104 | 105 | "when required:true": { 106 | setUp: function() { 107 | this.model.validation.name.required = true; 108 | }, 109 | 110 | "undefined is invalid": function() { 111 | refute(this.model.set({ 112 | name: undefined 113 | }, {validate: true})); 114 | }, 115 | 116 | "null is invalid": function() { 117 | refute(this.model.set({ 118 | name: null 119 | }, {validate: true})); 120 | } 121 | } 122 | }); -------------------------------------------------------------------------------- /tests/validators/namedMethod.js: -------------------------------------------------------------------------------- 1 | buster.testCase("named method validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | name: { 7 | fn: 'validateName' 8 | } 9 | }, 10 | validateName: function(val, attr, computed){ 11 | that.ctx = this; 12 | that.attr = attr; 13 | that.computed = computed; 14 | if(val !== 'backbone') { 15 | return 'Error'; 16 | } 17 | } 18 | }); 19 | 20 | this.model = new Model(); 21 | this.view = new Backbone.View({ 22 | model: this.model 23 | }); 24 | 25 | Backbone.Validation.bind(this.view, { 26 | valid: this.spy(), 27 | invalid: this.spy() 28 | }); 29 | }, 30 | 31 | "is invalid when method returns error message": function() { 32 | refute(this.model.set({name: ''}, {validate: true})); 33 | }, 34 | 35 | "is valid when method returns undefined": function() { 36 | assert(this.model.set({name: 'backbone'}, {validate: true})); 37 | }, 38 | 39 | "context is the model": function() { 40 | this.model.set({name: ''}, {validate: true}); 41 | assert.same(this.ctx, this.model); 42 | }, 43 | 44 | "second argument is the name of the attribute being validated": function() { 45 | this.model.set({name: ''}, {validate: true}); 46 | assert.equals('name', this.attr); 47 | }, 48 | 49 | "third argument is a computed model state": function() { 50 | this.model.set({attr: 'attr'}); 51 | this.model.set({ 52 | name: 'name', 53 | age: 1 54 | }, {validate: true}); 55 | 56 | assert.equals({attr:'attr', name:'name', age:1}, this.computed); 57 | } 58 | }); 59 | 60 | buster.testCase("named method validator short hand syntax", { 61 | setUp: function() { 62 | var that = this; 63 | var Model = Backbone.Model.extend({ 64 | validation: { 65 | name: 'validateName' 66 | }, 67 | validateName: function(val, attr, computed){ 68 | that.ctx = this; 69 | that.attr = attr; 70 | that.computed = computed; 71 | if(val !== 'backbone') { 72 | return 'Error'; 73 | } 74 | } 75 | }); 76 | 77 | this.model = new Model(); 78 | this.view = new Backbone.View({ 79 | model: this.model 80 | }); 81 | 82 | Backbone.Validation.bind(this.view, { 83 | valid: this.spy(), 84 | invalid: this.spy() 85 | }); 86 | }, 87 | 88 | "is invalid when method returns error message": function() { 89 | refute(this.model.set({name: ''}, {validate: true})); 90 | }, 91 | 92 | "is valid when method returns undefined": function() { 93 | assert(this.model.set({name: 'backbone'}, {validate: true})); 94 | }, 95 | 96 | "context is the model": function() { 97 | this.model.set({name: ''}, {validate: true}); 98 | assert.same(this.ctx, this.model); 99 | }, 100 | 101 | "second argument is the name of the attribute being validated": function() { 102 | this.model.set({name: ''}, {validate: true}); 103 | assert.equals('name', this.attr); 104 | }, 105 | 106 | "third argument is a computed model state": function() { 107 | this.model.set({attr: 'attr'}); 108 | this.model.set({ 109 | name: 'name', 110 | age: 1 111 | }, {validate: true}); 112 | 113 | assert.equals({attr:'attr', name:'name', age:1}, this.computed); 114 | } 115 | }); -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.initConfig({ 3 | pkg: grunt.file.readJSON('package.json'), 4 | meta: { 5 | banner: '// <%= pkg.title %> v<%= pkg.version %>\n' + '//\n' + '// Copyright (c) 2011-<%= grunt.template.today("yyyy") %> <%= pkg.author.name %>\n' + '// Distributed under MIT License\n' + '//\n' + '// Documentation and full license available at:\n' + '// <%= pkg.homepage %>\n' 6 | }, 7 | rig: { 8 | browser: { 9 | options: { 10 | banner: '<%=grunt.config.get("meta").banner%>' 11 | }, 12 | files: { 13 | 'dist/<%= pkg.name %>.js': ['src/<%= pkg.name %>.js'] 14 | } 15 | }, 16 | amd: { 17 | options: { 18 | banner: '<%=grunt.config.get("meta").banner%>' 19 | }, 20 | files: { 21 | 'dist/<%= pkg.name %>-amd.js': ['src/<%= pkg.name %>-amd.js'] 22 | } 23 | } 24 | }, 25 | uglify: { 26 | browser: { 27 | options: { 28 | banner: '<%=grunt.config.get("meta").banner%>' 29 | }, 30 | files: { 31 | 'dist/<%= pkg.name %>-min.js': ['dist/<%= pkg.name %>.js'] 32 | } 33 | }, 34 | amd: { 35 | options: { 36 | banner: '<%=grunt.config.get("meta").banner%>' 37 | }, 38 | files: { 39 | 'dist/<%= pkg.name %>-amd-min.js': ['dist/<%= pkg.name %>-amd.js'] 40 | } 41 | } 42 | }, 43 | watch: { 44 | files: '<%=grunt.config.get("lint").files%>', 45 | tasks: 'default' 46 | }, 47 | buster: { 48 | test: { 49 | 'config': 'buster.js', 50 | 'color': 'none', 51 | 'config-group': 'Browser' 52 | }, 53 | server: { 54 | 'port': '1111' 55 | } 56 | }, 57 | jshint: { 58 | all: ['grunt.js', 'src/**/*.js', 'tests/**/*.js'] 59 | }, 60 | docco: { 61 | app: { 62 | src: ['dist/backbone-validation.js'], 63 | options: { 64 | template: 'docco/docco.jst', 65 | css: 'docco/docco.css', 66 | output: 'docs/' 67 | } 68 | } 69 | }, 70 | 71 | shell: { 72 | npm: { 73 | command: 'npm publish', 74 | options: { 75 | stdout: true, 76 | stderr: true 77 | } 78 | }, 79 | clone: { 80 | command: 'git clone git@github.com:thedersen/thedersen.github.com.git', 81 | options: { 82 | stdout: true, 83 | stderr: true 84 | } 85 | }, 86 | copyDocco: { 87 | command: 'cp docs/backbone-validation.html thedersen.github.com/projects/backbone-validation/docs/index.html', 88 | options: { 89 | stdout: true, 90 | stderr: true 91 | } 92 | }, 93 | copyCss: { 94 | command: 'cp docs/docco.css thedersen.github.com/projects/backbone-validation/docs/docco.css', 95 | options: { 96 | stdout: true, 97 | stderr: true 98 | } 99 | }, 100 | copyExamples: { 101 | command: 'cp -rf examples/ thedersen.github.com/projects/backbone-validation/examples/', 102 | options: { 103 | stdout: true, 104 | stderr: true 105 | } 106 | }, 107 | push: { 108 | command: 'git commit -am "Updated docs for Backbone.Validation" && git push origin master', 109 | options: { 110 | stdout: true, 111 | stderr: true, 112 | execOptions: { 113 | cwd: 'thedersen.github.com' 114 | } 115 | } 116 | }, 117 | cleanup: { 118 | command: 'rm -rf thedersen.github.com', 119 | options: { 120 | stdout: true, 121 | stderr: true 122 | } 123 | } 124 | } 125 | }); 126 | 127 | grunt.registerTask('default', ['rig', 'jshint', 'buster', 'uglify']); 128 | grunt.registerTask('publish', ['docco', 'shell']); 129 | 130 | grunt.loadNpmTasks('grunt-contrib-jshint'); 131 | grunt.loadNpmTasks('grunt-contrib-uglify'); 132 | grunt.loadNpmTasks('grunt-buster'); 133 | grunt.loadNpmTasks('grunt-docco'); 134 | grunt.loadNpmTasks('grunt-shell'); 135 | grunt.loadNpmTasks('grunt-rigger'); 136 | }; 137 | -------------------------------------------------------------------------------- /tests/validators/required.js: -------------------------------------------------------------------------------- 1 | buster.testCase("required validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | name: { 7 | required: true 8 | }, 9 | agree: { 10 | required: true 11 | }, 12 | posts: { 13 | required: true 14 | }, 15 | dependsOnName: { 16 | required: function(val, attr, computed) { 17 | that.ctx = this; 18 | that.attr = attr; 19 | that.computed = computed; 20 | return this.get('name') === 'name'; 21 | } 22 | } 23 | } 24 | }); 25 | 26 | this.model = new Model({ 27 | name: 'name', 28 | agree: true, 29 | posts: ['post'], 30 | dependsOnName: 'depends' 31 | }); 32 | this.view = new Backbone.View({ 33 | model: this.model 34 | }); 35 | 36 | Backbone.Validation.bind(this.view, { 37 | valid: this.spy(), 38 | invalid: this.spy() 39 | }); 40 | }, 41 | 42 | "has default error message": function(done) { 43 | this.model.bind('validated:invalid', function(model, error){ 44 | assert.equals({name: 'Name is required'}, error); 45 | done(); 46 | }); 47 | this.model.set({name:''}, {validate: true}); 48 | }, 49 | 50 | "empty string is invalid": function() { 51 | refute(this.model.set({ 52 | name: '' 53 | }, {validate: true})); 54 | }, 55 | 56 | "non-empty string is valid": function() { 57 | assert(this.model.set({ 58 | name: 'a' 59 | }, {validate: true})); 60 | }, 61 | 62 | "string with just spaces is invalid": function() { 63 | refute(this.model.set({ 64 | name: ' ' 65 | }, {validate: true})); 66 | }, 67 | 68 | "null is invalid": function() { 69 | refute(this.model.set({ 70 | name: null 71 | }, {validate: true})); 72 | }, 73 | 74 | "undefined is invalid": function() { 75 | refute(this.model.set({ 76 | name: undefined 77 | }, {validate: true})); 78 | }, 79 | 80 | "false boolean is valid": function() { 81 | assert(this.model.set({ 82 | agree: false 83 | }, {validate: true})); 84 | }, 85 | 86 | "true boolean is valid": function() { 87 | assert(this.model.set({ 88 | agree: true 89 | }, {validate: true})); 90 | }, 91 | 92 | "empty array is invalid": function() { 93 | refute(this.model.set({ 94 | posts: [] 95 | }, {validate: true})); 96 | }, 97 | 98 | "non-empty array is valid": function() { 99 | assert(this.model.set({ 100 | posts: ['post'] 101 | }, {validate: true})); 102 | }, 103 | 104 | "required can be specified as a method returning true or false": function() { 105 | this.model.set({name:'aaa'}, {validate: true}); 106 | 107 | assert(this.model.set({ 108 | dependsOnName: undefined 109 | }, {validate: true})); 110 | 111 | this.model.set({name:'name'}, {validate: true}); 112 | 113 | refute(this.model.set({ 114 | dependsOnName: undefined 115 | }, {validate: true})); 116 | }, 117 | 118 | "context is the model": function() { 119 | this.model.set({ 120 | dependsOnName: '' 121 | }, {validate: true}); 122 | assert.same(this.ctx, this.model); 123 | }, 124 | 125 | "second argument is the name of the attribute being validated": function() { 126 | this.model.set({dependsOnName: ''}, {validate: true}); 127 | assert.equals('dependsOnName', this.attr); 128 | }, 129 | 130 | "third argument is a computed model state": function() { 131 | this.model.set({attr: 'attr'}); 132 | this.model.set({ 133 | name: 'name', 134 | posts: ['post'], 135 | dependsOnName: 'value' 136 | }, {validate: true}); 137 | 138 | assert.equals({agree:true, attr:'attr', dependsOnName:'value', name:'name', posts: ['post']}, this.computed); 139 | } 140 | }); -------------------------------------------------------------------------------- /tests/validators/method.js: -------------------------------------------------------------------------------- 1 | buster.testCase("method validator", { 2 | setUp: function() { 3 | var that = this; 4 | var Model = Backbone.Model.extend({ 5 | validation: { 6 | name: { 7 | fn: function(val, attr, computed) { 8 | that.ctx = this; 9 | that.attr = attr; 10 | that.computed = computed; 11 | if (name !== 'backbone') { 12 | return 'Error'; 13 | } 14 | } 15 | } 16 | } 17 | }); 18 | this.model = new Model(); 19 | this.view = new Backbone.View({ 20 | model: this.model 21 | }); 22 | 23 | Backbone.Validation.bind(this.view, { 24 | valid: this.spy(), 25 | invalid: this.spy() 26 | }); 27 | }, 28 | 29 | "is invalid when method returns error message": function() { 30 | refute(this.model.set({ 31 | name: '' 32 | }, {validate: true})); 33 | }, 34 | 35 | "is valid when method returns undefined": function() { 36 | refute(this.model.set({ 37 | name: 'backbone' 38 | }, {validate: true})); 39 | }, 40 | 41 | "context is the model": function() { 42 | this.model.set({ 43 | name: '' 44 | }, {validate: true}); 45 | assert.same(this.ctx, this.model); 46 | }, 47 | 48 | "second argument is the name of the attribute being validated": function() { 49 | this.model.set({name: ''}, {validate: true}); 50 | assert.equals('name', this.attr); 51 | }, 52 | 53 | "third argument is a computed model state": function() { 54 | this.model.set({attr: 'attr'}); 55 | this.model.set({ 56 | name: 'name', 57 | age: 1 58 | }, {validate: true}); 59 | 60 | assert.equals({attr:'attr', name:'name', age:1}, this.computed); 61 | } 62 | }); 63 | 64 | buster.testCase("method validator short hand syntax", { 65 | setUp: function() { 66 | var that = this; 67 | var Model = Backbone.Model.extend({ 68 | validation: { 69 | name: function(val, attr, computed) { 70 | that.ctx = this; 71 | that.attr = attr; 72 | that.computed = computed; 73 | if (name !== 'backbone') { 74 | return 'Error'; 75 | } 76 | } 77 | } 78 | }); 79 | this.model = new Model(); 80 | this.view = new Backbone.View({ 81 | model: this.model 82 | }); 83 | 84 | Backbone.Validation.bind(this.view, { 85 | valid: this.spy(), 86 | invalid: this.spy() 87 | }); 88 | }, 89 | 90 | "is invalid when method returns error message": function() { 91 | refute(this.model.set({ 92 | name: '' 93 | }, {validate: true})); 94 | }, 95 | 96 | "is valid when method returns undefined": function() { 97 | refute(this.model.set({ 98 | name: 'backbone' 99 | }, {validate: true})); 100 | }, 101 | 102 | "context is the model": function() { 103 | this.model.set({ 104 | name: '' 105 | }, {validate: true}); 106 | assert.same(this.ctx, this.model); 107 | }, 108 | 109 | "second argument is the name of the attribute being validated": function() { 110 | this.model.set({name: ''}, {validate: true}); 111 | assert.equals('name', this.attr); 112 | }, 113 | 114 | "third argument is a computed model state": function() { 115 | this.model.set({attr: 'attr'}); 116 | this.model.set({ 117 | name: 'name', 118 | age: 1 119 | }, {validate: true}); 120 | 121 | assert.equals({attr:'attr', name:'name', age:1}, this.computed); 122 | } 123 | }); 124 | 125 | buster.testCase("method validator using other built in validator(s)", { 126 | setUp: function() { 127 | var Model = Backbone.Model.extend({ 128 | validation: { 129 | name: function(val, attr, computed) { 130 | return Backbone.Validation.validators.length(val, attr, 4, this); 131 | } 132 | } 133 | }); 134 | 135 | _.extend(Model.prototype, Backbone.Validation.mixin); 136 | this.model = new Model(); 137 | }, 138 | 139 | "it should format the error message returned from the built in validator": function(){ 140 | assert.equals('Name must be 4 characters', this.model.preValidate('name', '')); 141 | } 142 | }); -------------------------------------------------------------------------------- /tests/customValidators.js: -------------------------------------------------------------------------------- 1 | buster.testCase('Extending Backbone.Validation with custom validator', { 2 | setUp: function() { 3 | var that = this; 4 | _.extend(Backbone.Validation.validators, { 5 | custom: function(value, attr, customValue) { 6 | that.context = this; 7 | if (value !== customValue) { 8 | return 'error'; 9 | } 10 | } 11 | }); 12 | 13 | var Model = Backbone.Model.extend({ 14 | validation: { 15 | age: { 16 | custom: 1 17 | } 18 | } 19 | }); 20 | 21 | this.model = new Model(); 22 | Backbone.Validation.bind(new Backbone.View({ 23 | model: this.model 24 | })); 25 | }, 26 | 27 | "should execute the custom validator": function() { 28 | assert(this.model.set({ 29 | age: 1 30 | }, {validate: true})); 31 | refute(this.model.set({ 32 | age: 2 33 | }, {validate: true})); 34 | } 35 | }); 36 | 37 | buster.testCase('Overriding built-in validator in Backbone.Validation', { 38 | setUp: function() { 39 | this.builtinMin = Backbone.Validation.validators.min; 40 | 41 | _.extend(Backbone.Validation.validators, { 42 | min: function(value, attr, customValue) { 43 | if (value !== customValue) { 44 | return 'error'; 45 | } 46 | } 47 | }); 48 | 49 | var Model = Backbone.Model.extend({ 50 | validation: { 51 | age: { 52 | min: 1 53 | } 54 | } 55 | }); 56 | 57 | this.model = new Model(); 58 | Backbone.Validation.bind(new Backbone.View({ 59 | model: this.model 60 | })); 61 | }, 62 | 63 | tearDown: function(){ 64 | Backbone.Validation.validators.min = this.builtinMin; 65 | }, 66 | 67 | "should execute the overridden validator": function() { 68 | assert(this.model.set({ 69 | age: 1 70 | }, {validate: true})); 71 | refute(this.model.set({ 72 | age: 2 73 | }, {validate: true})); 74 | } 75 | }); 76 | 77 | buster.testCase("Chaining built-in validators with custom", { 78 | setUp: function() { 79 | _.extend(Backbone.Validation.validators, { 80 | custom2: function(value, attr, customValue, model) { 81 | if (value !== customValue) { 82 | return 'error'; 83 | } 84 | }, 85 | custom: function(value, attr, customValue, model) { 86 | return this.required(value, attr, true, model) || this.custom2(value, attr, customValue, model); 87 | } 88 | }); 89 | 90 | var Model = Backbone.Model.extend({ 91 | validation: { 92 | name: { 93 | custom: 'custom' 94 | } 95 | } 96 | }); 97 | 98 | this.model = new Model(); 99 | Backbone.Validation.bind(new Backbone.View({ 100 | model: this.model 101 | })); 102 | }, 103 | 104 | "violating first validator in chain return first error message": function() { 105 | assert.equals({name: 'Name is required'}, this.model.validate({name:''})); 106 | }, 107 | 108 | "violating second validator in chain return second error message": function() { 109 | assert.equals({name: 'error'}, this.model.validate({name:'a'})); 110 | }, 111 | 112 | "violating none of the validators returns undefined": function() { 113 | refute.defined(this.model.validate({name:'custom'})); 114 | } 115 | }); 116 | 117 | buster.testCase("Formatting custom validator messages", { 118 | setUp: function() { 119 | _.extend(Backbone.Validation.validators, { 120 | custom: function(value, attr, customValue, model) { 121 | if (value !== customValue) { 122 | return this.format("{0} must be equal to {1}", this.formatLabel(attr, model), customValue); 123 | } 124 | } 125 | }); 126 | 127 | var Model = Backbone.Model.extend({ 128 | validation: { 129 | name: { 130 | custom: 'custom' 131 | } 132 | } 133 | }); 134 | 135 | this.model = new Model(); 136 | Backbone.Validation.bind(new Backbone.View({ 137 | model: this.model 138 | })); 139 | }, 140 | 141 | "a custom validator can return a formatted message": function() { 142 | assert.equals({name: 'Name must be equal to custom'}, this.model.validate({name:''})); 143 | } 144 | }); 145 | -------------------------------------------------------------------------------- /tests/events.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Backbone.Validation events", { 2 | setUp: function() { 3 | var Model = Backbone.Model.extend({ 4 | validation: { 5 | age: function(val){ 6 | if(!val) { 7 | return 'age'; 8 | } 9 | }, 10 | name: function(val){ 11 | if(!val) { 12 | return 'name'; 13 | } 14 | } 15 | } 16 | }); 17 | 18 | this.model = new Model(); 19 | this.view = new Backbone.View({ 20 | model: this.model 21 | }); 22 | 23 | Backbone.Validation.bind(this.view); 24 | }, 25 | 26 | "model is updated before the events are raised": function() { 27 | this.model.bind('change', function(){ 28 | assert.equals(1, this.model.get('age')); 29 | }, this); 30 | 31 | this.model.bind('validated', function(){ 32 | assert.equals(1, this.model.get('age')); 33 | }, this); 34 | 35 | this.model.bind('validated:valid', function(){ 36 | assert.equals(1, this.model.get('age')); 37 | }, this); 38 | 39 | this.model.set({ 40 | age: 1, 41 | name: 'name' 42 | }, {validate: true}); 43 | }, 44 | 45 | "when model is valid": { 46 | "validated event is triggered with true and model": function(done) { 47 | this.model.bind('validated', function(valid, model){ 48 | assert(valid); 49 | assert.same(this.model, model); 50 | done(); 51 | }, this); 52 | 53 | this.model.set({ 54 | age: 1, 55 | name: 'name' 56 | }, {validate: true}); 57 | }, 58 | 59 | "validated:valid event is triggered with model": function(done) { 60 | this.model.bind('validated:valid', function(model){ 61 | assert.same(this.model, model); 62 | done(); 63 | }, this); 64 | 65 | this.model.set({ 66 | age: 1, 67 | name: 'name' 68 | }, {validate: true}); 69 | } 70 | }, 71 | 72 | "when one invalid value is set": { 73 | "validated event is triggered with false, model and an object with the names of the attributes with error": function(done) { 74 | this.model.bind('validated', function(valid, model, attr){ 75 | refute(valid); 76 | assert.same(this.model, model); 77 | assert.equals({age:'age', name:'name'}, attr); 78 | done(); 79 | }, this); 80 | 81 | this.model.set({age:0}, {validate: true}); 82 | }, 83 | 84 | "validated:invalid event is triggered with model and an object with the names of the attributes with error": function(done) { 85 | this.model.bind('validated:invalid', function(model, attr){ 86 | assert.same(this.model, model); 87 | assert.equals({age: 'age', name: 'name'}, attr); 88 | done(); 89 | }, this); 90 | 91 | this.model.set({age:0}, {validate: true}); 92 | }, 93 | 94 | "invalid event is triggered with model and an object with the names of the attributes with error": function(done) { 95 | this.model.bind('invalid', function(model, attr){ 96 | assert.same(this.model, model); 97 | assert.equals({age: 'age', name: 'name'}, attr); 98 | done(); 99 | }, this); 100 | 101 | this.model.set({age:0}, {validate: true}); 102 | } 103 | }, 104 | 105 | "when one valid value is set": { 106 | "validated event is triggered with false, model and an object with the names of the attributes with error": function(done) { 107 | this.model.bind('validated', function(valid, model, attrs){ 108 | refute(valid); 109 | assert.same(this.model, model); 110 | assert.equals({name: 'name'}, attrs); 111 | done(); 112 | }, this); 113 | 114 | this.model.set({ 115 | age: 1 116 | }, {validate: true}); 117 | }, 118 | 119 | "validated:invalid event is triggered with model and an object with the names of the attributes with error": function(done) { 120 | this.model.bind('validated:invalid', function(model, attrs){ 121 | assert.same(this.model, model); 122 | assert.equals({name: 'name'}, attrs); 123 | done(); 124 | }, this); 125 | 126 | this.model.set({ 127 | age: 1 128 | }, {validate: true}); 129 | } 130 | } 131 | }); -------------------------------------------------------------------------------- /tests/attributesOption.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Setting options.attributes", { 2 | setUp: function () { 3 | var View = Backbone.View.extend({ 4 | render: function () { 5 | var html = $('
'); 6 | this.$el.append(html); 7 | } 8 | }); 9 | 10 | var Model = Backbone.Model.extend({ 11 | validation: { 12 | age: { 13 | required: true 14 | }, 15 | name: { 16 | required: true 17 | }, 18 | password: { 19 | required: true 20 | }, 21 | email: { 22 | pattern: 'email' 23 | } 24 | } 25 | }); 26 | 27 | this.model = new Model(); 28 | this.view = new View({ 29 | model: this.model 30 | }); 31 | 32 | this.view.render(); 33 | this.age = $(this.view.$('[name~=age]')); 34 | this.name = $(this.view.$('[name~=name]')); 35 | }, 36 | 37 | tearDown: function () { 38 | this.view.remove(); 39 | }, 40 | 41 | "to an string array": { 42 | setUp: function () { 43 | Backbone.Validation.bind(this.view, { 44 | attributes: ['name', 'age'] 45 | }); 46 | }, 47 | tearDown: function () { 48 | Backbone.Validation.unbind(this.view); 49 | this.model.clear(); 50 | }, 51 | 52 | "only the attributes in array should be validated": function () { 53 | var errors = this.model.validate(); 54 | assert.defined(errors.name); 55 | assert.defined(errors.age); 56 | refute.defined(errors.password); 57 | refute.defined(errors.email); 58 | }, 59 | 60 | "when all the attributes in array are valid": { 61 | setUp: function () { 62 | this.model.set({ 63 | age: 1, 64 | name: 'hello', 65 | email: 'invalidemail' 66 | }); 67 | }, 68 | "validation will pass": function () { 69 | var errors = this.model.validate(); 70 | refute.defined(errors); 71 | } 72 | } 73 | }, 74 | "to an function returning an string array": { 75 | setUp: function () { 76 | Backbone.Validation.bind(this.view, { 77 | attributes: function(view) { 78 | return ['name', 'age']; 79 | } 80 | }); 81 | }, 82 | tearDown: function () { 83 | Backbone.Validation.unbind(this.view); 84 | this.model.clear(); 85 | }, 86 | 87 | "only the attributes returned by the function should be validated": function () { 88 | var errors = this.model.validate(); 89 | assert.defined(errors.name); 90 | assert.defined(errors.age); 91 | refute.defined(errors.password); 92 | refute.defined(errors.email); 93 | }, 94 | 95 | "when all the attributes returned by the function are valid": { 96 | setUp: function () { 97 | this.model.set({ 98 | age: 1, 99 | name: 'hello', 100 | email: 'invalidemail' 101 | }); 102 | }, 103 | "validation will pass": function () { 104 | var errors = this.model.validate(); 105 | refute.defined(errors); 106 | } 107 | } 108 | }, 109 | "to 'inputNames' builtin attributeLoader": { 110 | setUp: function () { 111 | Backbone.Validation.bind(this.view, { 112 | attributes: 'inputNames' 113 | }); 114 | }, 115 | tearDown: function () { 116 | Backbone.Validation.unbind(this.view); 117 | this.model.clear(); 118 | }, 119 | 120 | "only the attributes with present in form should be validated": function () { 121 | var errors = this.model.validate(); 122 | assert.defined(errors.name); 123 | refute.defined(errors.age); 124 | refute.defined(errors.password); 125 | refute.defined(errors.email); 126 | }, 127 | 128 | "submit and buttons should not be included in attribute array": function () { 129 | var attrs = Backbone.Validation.attributeLoaders.inputNames(this.view); 130 | assert.equals(attrs.length, 1, 'Length of array returned by inputNames loader'); 131 | assert.contains(attrs, 'name'); 132 | refute.contains(attrs, 'save'); 133 | refute.contains(attrs, 'cancel'); 134 | }, 135 | "when all the attributes present in form are valid": { 136 | setUp: function () { 137 | this.model.set({ 138 | name: 'hello', 139 | email: 'invalidemail' 140 | }); 141 | }, 142 | "validation will pass": function () { 143 | var errors = this.model.validate(); 144 | refute.defined(errors); 145 | } 146 | } 147 | }, 148 | "to an custom attributeLoader": { 149 | setUp: function () { 150 | _.extend(Backbone.Validation.attributeLoaders, { 151 | myAttrLoader: function() { 152 | return ['age']; 153 | } 154 | }); 155 | Backbone.Validation.bind(this.view, { 156 | attributes: 'myAttrLoader' 157 | }); 158 | }, 159 | tearDown: function () { 160 | Backbone.Validation.unbind(this.view); 161 | this.model.clear(); 162 | }, 163 | 164 | "only the attributes returned by the registered function should be validated": function () { 165 | var errors = this.model.validate(); 166 | assert.defined(errors.age); 167 | refute.defined(errors.name); 168 | refute.defined(errors.password); 169 | }, 170 | 171 | "when all the attributes returned by the registered function are valid": { 172 | setUp: function () { 173 | this.model.set({ 174 | age: 1, 175 | email: 'invalidemail' 176 | }); 177 | }, 178 | "validation will pass": function () { 179 | var errors = this.model.validate(); 180 | refute.defined(errors); 181 | } 182 | } 183 | } 184 | }); -------------------------------------------------------------------------------- /dist/backbone-validation-min.js: -------------------------------------------------------------------------------- 1 | // Backbone.Validation v0.11.5 2 | // 3 | // Copyright (c) 2011-2015 Thomas Pedersen 4 | // Distributed under MIT License 5 | // 6 | // Documentation and full license available at: 7 | // http://thedersen.com/projects/backbone-validation 8 | Backbone.Validation=function(a){"use strict";var b={forceUpdate:!1,selector:"name",labelFormatter:"sentenceCase",valid:Function.prototype,invalid:Function.prototype},c={formatLabel:function(a,c){return i[b.labelFormatter](a,c)},format:function(){var a=Array.prototype.slice.call(arguments),b=a.shift();return b.replace(/\{(\d+)\}/g,function(b,c){return"undefined"!=typeof a[c]?a[c]:b})}},d=function(b,c,e){return c=c||{},e=e||"",a.each(b,function(f,g){b.hasOwnProperty(g)&&(f&&a.isArray(f)?a.forEach(f,function(a,b){d(a,c,e+g+"."+b+"."),c[e+g+"."+b]=a}):f&&"object"==typeof f&&f.constructor===Object&&d(f,c,e+g+"."),c[e+g]=f)}),c},e=function(){var e=function(b,c){return c=c||a.keys(a.result(b,"validation")||{}),a.reduce(c,function(a,b){return a[b]=void 0,a},{})},g=function(b,c){var d=b.attributes;return a.isFunction(d)?d=d(c):a.isString(d)&&a.isFunction(j[d])&&(d=j[d](c)),a.isArray(d)?d:void 0},h=function(b,c){var d=b.validation?a.result(b,"validation")[c]||{}:{};return(a.isFunction(d)||a.isString(d))&&(d={fn:d}),a.isArray(d)||(d=[d]),a.reduce(d,function(b,c){return a.each(a.without(a.keys(c),"msg"),function(a){b.push({fn:k[a],val:c[a],msg:c.msg})}),b},[])},i=function(b,d,e,f){return a.reduce(h(b,d),function(g,h){var i=a.extend({},c,k),j=h.fn.call(i,e,d,h.val,b,f);return j===!1||g===!1?!1:j&&!g?a.result(h,"msg")||j:g},"")},l=function(b,c,d){var e,f={},g=!0,h=a.clone(c);return a.each(d,function(a,c){e=i(b,c,a,h),e&&(f[c]=e,g=!1)}),{invalidAttrs:f,isValid:g}},m=function(b,c){return{preValidate:function(b,c){var d,e=this,f={};return a.isObject(b)?(a.each(b,function(a,b){d=e.preValidate(b,a),d&&(f[b]=d)}),a.isEmpty(f)?void 0:f):i(this,b,c,a.extend({},this.attributes))},isValid:function(e){var f,h,j,k;return e=e||g(c,b),a.isString(e)?h=[e]:a.isArray(e)&&(h=e),h&&(f=d(this.attributes),a.each(this.associatedViews,function(b){a.each(h,function(d){j=i(this,d,f[d],a.extend({},this.attributes)),j?(c.invalid(b,d,j,c.selector),k=k||{},k[d]=j):c.valid(b,d,c.selector)},this)},this)),e===!0&&(k=this.validate()),k&&this.trigger("invalid",this,k,{validationError:k}),h?!k:this.validation?this._isValid:!0},validate:function(f,h){var i=this,j=!f,k=a.extend({},c,h),m=e(i,g(c,b)),n=a.extend({},m,i.attributes,f),o=d(n),p=f?d(f):o,q=l(i,n,a.pick(o,a.keys(m)));return i._isValid=q.isValid,a.each(i.associatedViews,function(b){a.each(m,function(a,c){var d=q.invalidAttrs.hasOwnProperty(c),e=p.hasOwnProperty(c);d||k.valid(b,c,k.selector),d&&(e||j)&&k.invalid(b,c,q.invalidAttrs[c],k.selector)})}),a.defer(function(){i.trigger("validated",i._isValid,i,q.invalidAttrs),i.trigger("validated:"+(i._isValid?"valid":"invalid"),i,q.invalidAttrs)}),!k.forceUpdate&&a.intersection(a.keys(q.invalidAttrs),a.keys(p)).length>0?q.invalidAttrs:void 0}}},n=function(b,c,d){c.associatedViews?c.associatedViews.push(b):c.associatedViews=[b],a.extend(c,m(b,d))},o=function(b,c){c&&b.associatedViews&&b.associatedViews.length>1?b.associatedViews=a.without(b.associatedViews,c):(delete b.validate,delete b.preValidate,delete b.isValid,delete b.associatedViews)},p=function(a){n(this.view,a,this.options)},q=function(a){o(a)};return{version:"0.11.3",configure:function(c){a.extend(b,c)},bind:function(c,d){d=a.extend({},b,f,d);var e=d.model||c.model,g=d.collection||c.collection;if("undefined"==typeof e&&"undefined"==typeof g)throw"Before you execute the binding your view must have a model or a collection.\nSee http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.";e?n(c,e,d):g&&(g.each(function(a){n(c,a,d)}),g.bind("add",p,{view:c,options:d}),g.bind("remove",q))},unbind:function(b,c){c=a.extend({},c);var d=c.model||b.model,e=c.collection||b.collection;d?o(d,b):e&&(e.each(function(a){o(a,b)}),e.unbind("add",p),e.unbind("remove",q))},mixin:m(null,b)}}(),f=e.callbacks={valid:function(a,b,c){a.$("["+c+'~="'+b+'"]').removeClass("invalid").removeAttr("data-error")},invalid:function(a,b,c,d){a.$("["+d+'~="'+b+'"]').addClass("invalid").attr("data-error",c)}},g=e.patterns={digits:/^\d+$/,number:/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,email:/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i,url:/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i},h=e.messages={required:"{0} is required",acceptance:"{0} must be accepted",min:"{0} must be greater than or equal to {1}",max:"{0} must be less than or equal to {1}",range:"{0} must be between {1} and {2}",length:"{0} must be {1} characters",minLength:"{0} must be at least {1} characters",maxLength:"{0} must be at most {1} characters",rangeLength:"{0} must be between {1} and {2} characters",oneOf:"{0} must be one of: {1}",equalTo:"{0} must be the same as {1}",digits:"{0} must only contain digits",number:"{0} must be a number",email:"{0} must be a valid email",url:"{0} must be a valid url",inlinePattern:"{0} is invalid"},i=e.labelFormatters={none:function(a){return a},sentenceCase:function(a){return a.replace(/(?:^\w|[A-Z]|\b\w)/g,function(a,b){return 0===b?a.toUpperCase():" "+a.toLowerCase()}).replace(/_/g," ")},label:function(a,b){return b.labels&&b.labels[a]||i.sentenceCase(a,b)}},j=e.attributeLoaders={inputNames:function(a){var b=[];return a&&a.$("form [name]").each(function(){/^(?:input|select|textarea)$/i.test(this.nodeName)&&this.name&&"submit"!==this.type&&-1===b.indexOf(this.name)&&b.push(this.name)}),b}},k=e.validators=function(){var b=String.prototype.trim?function(a){return null===a?"":String.prototype.trim.call(a)}:function(a){var b=/^\s+/,c=/\s+$/;return null===a?"":a.toString().replace(b,"").replace(c,"")},c=function(b){return a.isNumber(b)||a.isString(b)&&b.match(g.number)},d=function(c){return!(a.isNull(c)||a.isUndefined(c)||a.isString(c)&&""===b(c)||a.isArray(c)&&a.isEmpty(c))};return{fn:function(b,c,d,e,f){return a.isString(d)&&(d=e[d]),d.call(e,b,c,f)},required:function(b,c,e,f,g){var i=a.isFunction(e)?e.call(f,b,c,g):e;return i||d(b)?i&&!d(b)?this.format(h.required,this.formatLabel(c,f)):void 0:!1},acceptance:function(b,c,d,e){return"true"===b||a.isBoolean(b)&&b!==!1?void 0:this.format(h.acceptance,this.formatLabel(c,e))},min:function(a,b,d,e){return!c(a)||d>a?this.format(h.min,this.formatLabel(b,e),d):void 0},max:function(a,b,d,e){return!c(a)||a>d?this.format(h.max,this.formatLabel(b,e),d):void 0},range:function(a,b,d,e){return!c(a)||ad[1]?this.format(h.range,this.formatLabel(b,e),d[0],d[1]):void 0},length:function(b,c,d,e){return a.isString(b)&&b.length===d?void 0:this.format(h.length,this.formatLabel(c,e),d)},minLength:function(b,c,d,e){return!a.isString(b)||b.lengthd?this.format(h.maxLength,this.formatLabel(c,e),d):void 0},rangeLength:function(b,c,d,e){return!a.isString(b)||b.lengthd[1]?this.format(h.rangeLength,this.formatLabel(c,e),d[0],d[1]):void 0},oneOf:function(b,c,d,e){return a.include(d,b)?void 0:this.format(h.oneOf,this.formatLabel(c,e),d.join(", "))},equalTo:function(a,b,c,d,e){return a!==e[c]?this.format(h.equalTo,this.formatLabel(b,d),this.formatLabel(c,d)):void 0},pattern:function(a,b,c,e){return d(a)&&a.toString().match(g[c]||c)?void 0:this.format(h[c]||h.inlinePattern,this.formatLabel(b,e),c)}}}();return a.each(k,function(b,d){k[d]=a.bind(k[d],a.extend({},c,k))}),e}(_); -------------------------------------------------------------------------------- /dist/backbone-validation-amd-min.js: -------------------------------------------------------------------------------- 1 | // Backbone.Validation v0.11.5 2 | // 3 | // Copyright (c) 2011-2015 Thomas Pedersen 4 | // Distributed under MIT License 5 | // 6 | // Documentation and full license available at: 7 | // http://thedersen.com/projects/backbone-validation 8 | !function(a){"object"==typeof exports?module.exports=a(require("backbone"),require("underscore")):"function"==typeof define&&define.amd&&define(["backbone","underscore"],a)}(function(a,b){return a.Validation=function(a){"use strict";var b={forceUpdate:!1,selector:"name",labelFormatter:"sentenceCase",valid:Function.prototype,invalid:Function.prototype},c={formatLabel:function(a,c){return i[b.labelFormatter](a,c)},format:function(){var a=Array.prototype.slice.call(arguments),b=a.shift();return b.replace(/\{(\d+)\}/g,function(b,c){return"undefined"!=typeof a[c]?a[c]:b})}},d=function(b,c,e){return c=c||{},e=e||"",a.each(b,function(f,g){b.hasOwnProperty(g)&&(f&&a.isArray(f)?a.forEach(f,function(a,b){d(a,c,e+g+"."+b+"."),c[e+g+"."+b]=a}):f&&"object"==typeof f&&f.constructor===Object&&d(f,c,e+g+"."),c[e+g]=f)}),c},e=function(){var e=function(b,c){return c=c||a.keys(a.result(b,"validation")||{}),a.reduce(c,function(a,b){return a[b]=void 0,a},{})},g=function(b,c){var d=b.attributes;return a.isFunction(d)?d=d(c):a.isString(d)&&a.isFunction(j[d])&&(d=j[d](c)),a.isArray(d)?d:void 0},h=function(b,c){var d=b.validation?a.result(b,"validation")[c]||{}:{};return(a.isFunction(d)||a.isString(d))&&(d={fn:d}),a.isArray(d)||(d=[d]),a.reduce(d,function(b,c){return a.each(a.without(a.keys(c),"msg"),function(a){b.push({fn:k[a],val:c[a],msg:c.msg})}),b},[])},i=function(b,d,e,f){return a.reduce(h(b,d),function(g,h){var i=a.extend({},c,k),j=h.fn.call(i,e,d,h.val,b,f);return j===!1||g===!1?!1:j&&!g?a.result(h,"msg")||j:g},"")},l=function(b,c,d){var e,f={},g=!0,h=a.clone(c);return a.each(d,function(a,c){e=i(b,c,a,h),e&&(f[c]=e,g=!1)}),{invalidAttrs:f,isValid:g}},m=function(b,c){return{preValidate:function(b,c){var d,e=this,f={};return a.isObject(b)?(a.each(b,function(a,b){d=e.preValidate(b,a),d&&(f[b]=d)}),a.isEmpty(f)?void 0:f):i(this,b,c,a.extend({},this.attributes))},isValid:function(e){var f,h,j,k;return e=e||g(c,b),a.isString(e)?h=[e]:a.isArray(e)&&(h=e),h&&(f=d(this.attributes),a.each(this.associatedViews,function(b){a.each(h,function(d){j=i(this,d,f[d],a.extend({},this.attributes)),j?(c.invalid(b,d,j,c.selector),k=k||{},k[d]=j):c.valid(b,d,c.selector)},this)},this)),e===!0&&(k=this.validate()),k&&this.trigger("invalid",this,k,{validationError:k}),h?!k:this.validation?this._isValid:!0},validate:function(f,h){var i=this,j=!f,k=a.extend({},c,h),m=e(i,g(c,b)),n=a.extend({},m,i.attributes,f),o=d(n),p=f?d(f):o,q=l(i,n,a.pick(o,a.keys(m)));return i._isValid=q.isValid,a.each(i.associatedViews,function(b){a.each(m,function(a,c){var d=q.invalidAttrs.hasOwnProperty(c),e=p.hasOwnProperty(c);d||k.valid(b,c,k.selector),d&&(e||j)&&k.invalid(b,c,q.invalidAttrs[c],k.selector)})}),a.defer(function(){i.trigger("validated",i._isValid,i,q.invalidAttrs),i.trigger("validated:"+(i._isValid?"valid":"invalid"),i,q.invalidAttrs)}),!k.forceUpdate&&a.intersection(a.keys(q.invalidAttrs),a.keys(p)).length>0?q.invalidAttrs:void 0}}},n=function(b,c,d){c.associatedViews?c.associatedViews.push(b):c.associatedViews=[b],a.extend(c,m(b,d))},o=function(b,c){c&&b.associatedViews&&b.associatedViews.length>1?b.associatedViews=a.without(b.associatedViews,c):(delete b.validate,delete b.preValidate,delete b.isValid,delete b.associatedViews)},p=function(a){n(this.view,a,this.options)},q=function(a){o(a)};return{version:"0.11.3",configure:function(c){a.extend(b,c)},bind:function(c,d){d=a.extend({},b,f,d);var e=d.model||c.model,g=d.collection||c.collection;if("undefined"==typeof e&&"undefined"==typeof g)throw"Before you execute the binding your view must have a model or a collection.\nSee http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.";e?n(c,e,d):g&&(g.each(function(a){n(c,a,d)}),g.bind("add",p,{view:c,options:d}),g.bind("remove",q))},unbind:function(b,c){c=a.extend({},c);var d=c.model||b.model,e=c.collection||b.collection;d?o(d,b):e&&(e.each(function(a){o(a,b)}),e.unbind("add",p),e.unbind("remove",q))},mixin:m(null,b)}}(),f=e.callbacks={valid:function(a,b,c){a.$("["+c+'~="'+b+'"]').removeClass("invalid").removeAttr("data-error")},invalid:function(a,b,c,d){a.$("["+d+'~="'+b+'"]').addClass("invalid").attr("data-error",c)}},g=e.patterns={digits:/^\d+$/,number:/^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/,email:/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i,url:/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i},h=e.messages={required:"{0} is required",acceptance:"{0} must be accepted",min:"{0} must be greater than or equal to {1}",max:"{0} must be less than or equal to {1}",range:"{0} must be between {1} and {2}",length:"{0} must be {1} characters",minLength:"{0} must be at least {1} characters",maxLength:"{0} must be at most {1} characters",rangeLength:"{0} must be between {1} and {2} characters",oneOf:"{0} must be one of: {1}",equalTo:"{0} must be the same as {1}",digits:"{0} must only contain digits",number:"{0} must be a number",email:"{0} must be a valid email",url:"{0} must be a valid url",inlinePattern:"{0} is invalid"},i=e.labelFormatters={none:function(a){return a},sentenceCase:function(a){return a.replace(/(?:^\w|[A-Z]|\b\w)/g,function(a,b){return 0===b?a.toUpperCase():" "+a.toLowerCase()}).replace(/_/g," ")},label:function(a,b){return b.labels&&b.labels[a]||i.sentenceCase(a,b)}},j=e.attributeLoaders={inputNames:function(a){var b=[];return a&&a.$("form [name]").each(function(){/^(?:input|select|textarea)$/i.test(this.nodeName)&&this.name&&"submit"!==this.type&&-1===b.indexOf(this.name)&&b.push(this.name)}),b}},k=e.validators=function(){var b=String.prototype.trim?function(a){return null===a?"":String.prototype.trim.call(a)}:function(a){var b=/^\s+/,c=/\s+$/;return null===a?"":a.toString().replace(b,"").replace(c,"")},c=function(b){return a.isNumber(b)||a.isString(b)&&b.match(g.number)},d=function(c){return!(a.isNull(c)||a.isUndefined(c)||a.isString(c)&&""===b(c)||a.isArray(c)&&a.isEmpty(c))};return{fn:function(b,c,d,e,f){return a.isString(d)&&(d=e[d]),d.call(e,b,c,f)},required:function(b,c,e,f,g){var i=a.isFunction(e)?e.call(f,b,c,g):e;return i||d(b)?i&&!d(b)?this.format(h.required,this.formatLabel(c,f)):void 0:!1},acceptance:function(b,c,d,e){return"true"===b||a.isBoolean(b)&&b!==!1?void 0:this.format(h.acceptance,this.formatLabel(c,e))},min:function(a,b,d,e){return!c(a)||d>a?this.format(h.min,this.formatLabel(b,e),d):void 0},max:function(a,b,d,e){return!c(a)||a>d?this.format(h.max,this.formatLabel(b,e),d):void 0},range:function(a,b,d,e){return!c(a)||ad[1]?this.format(h.range,this.formatLabel(b,e),d[0],d[1]):void 0},length:function(b,c,d,e){return a.isString(b)&&b.length===d?void 0:this.format(h.length,this.formatLabel(c,e),d)},minLength:function(b,c,d,e){return!a.isString(b)||b.lengthd?this.format(h.maxLength,this.formatLabel(c,e),d):void 0},rangeLength:function(b,c,d,e){return!a.isString(b)||b.lengthd[1]?this.format(h.rangeLength,this.formatLabel(c,e),d[0],d[1]):void 0},oneOf:function(b,c,d,e){return a.include(d,b)?void 0:this.format(h.oneOf,this.formatLabel(c,e),d.join(", "))},equalTo:function(a,b,c,d,e){return a!==e[c]?this.format(h.equalTo,this.formatLabel(b,d),this.formatLabel(c,d)):void 0},pattern:function(a,b,c,e){return d(a)&&a.toString().match(g[c]||c)?void 0:this.format(h[c]||h.inlinePattern,this.formatLabel(b,e),c)}}}();return a.each(k,function(b,d){k[d]=a.bind(k[d],a.extend({},c,k))}),e}(b),a.Validation}); -------------------------------------------------------------------------------- /docco/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | html { height: 100%; } 3 | body { 4 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 5 | font-size: 14px; 6 | line-height: 16px; 7 | color: #252519; 8 | margin: 0; padding: 0; 9 | height:100%; 10 | } 11 | #container { min-height: 100%; } 12 | 13 | a { 14 | color: #261a3b; 15 | } 16 | 17 | a:visited { 18 | color: #261a3b; 19 | } 20 | 21 | p, ul, ol { 22 | margin: 0 0 15px; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | margin: 30px 0 15px 0; 27 | } 28 | 29 | h1 { 30 | margin-top: 40px; 31 | } 32 | 33 | hr { 34 | border: 0 none; 35 | border-top: 1px solid #e5e5ee; 36 | height: 1px; 37 | margin: 20px 0; 38 | } 39 | 40 | pre, tt, code { 41 | font-size: 12px; line-height: 16px; 42 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 43 | margin: 0; padding: 0; 44 | } 45 | 46 | ul.sections { 47 | list-style: none; 48 | padding:0 0 5px 0;; 49 | margin:0; 50 | } 51 | 52 | /* 53 | Force border-box so that % widths fit the parent 54 | container without overlap because of margin/padding. 55 | 56 | More Info : http://www.quirksmode.org/css/box.html 57 | */ 58 | ul.sections > li > div { 59 | -moz-box-sizing: border-box; /* firefox */ 60 | -ms-box-sizing: border-box; /* ie */ 61 | -webkit-box-sizing: border-box; /* webkit */ 62 | -khtml-box-sizing: border-box; /* konqueror */ 63 | box-sizing: border-box; /* css3 */ 64 | } 65 | 66 | 67 | /*---------------------- Jump Page -----------------------------*/ 68 | #jump_to, #jump_page { 69 | margin: 0; 70 | background: white; 71 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 72 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 73 | font: 16px Arial; 74 | cursor: pointer; 75 | text-align: right; 76 | list-style: none; 77 | } 78 | 79 | #jump_to a { 80 | text-decoration: none; 81 | } 82 | 83 | #jump_to a.large { 84 | display: none; 85 | } 86 | #jump_to a.small { 87 | font-size: 22px; 88 | font-weight: bold; 89 | color: #676767; 90 | } 91 | 92 | #jump_to, #jump_wrapper { 93 | position: fixed; 94 | right: 0; top: 0; 95 | padding: 10px 15px; 96 | margin:0; 97 | } 98 | 99 | #jump_wrapper { 100 | display: none; 101 | padding:0; 102 | } 103 | 104 | #jump_to:hover #jump_wrapper { 105 | display: block; 106 | } 107 | 108 | #jump_page { 109 | padding: 5px 0 3px; 110 | margin: 0 0 25px 25px; 111 | } 112 | 113 | #jump_page .source { 114 | display: block; 115 | padding: 15px; 116 | text-decoration: none; 117 | border-top: 1px solid #eee; 118 | } 119 | 120 | #jump_page .source:hover { 121 | background: #f5f5ff; 122 | } 123 | 124 | #jump_page .source:first-child { 125 | } 126 | 127 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 128 | @media only screen and (min-width: 320px) { 129 | .pilwrap { display: none; } 130 | 131 | ul.sections > li > div { 132 | display: block; 133 | padding:5px 10px 0 10px; 134 | } 135 | 136 | ul.sections > li > div.annotation { 137 | background: #fff; 138 | } 139 | 140 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 141 | padding-left: 30px; 142 | } 143 | 144 | ul.sections > li > div.content { 145 | background: #f5f5ff; 146 | overflow-x:auto; 147 | -webkit-box-shadow: inset 0 0 5px #e5e5ee; 148 | box-shadow: inset 0 0 5px #e5e5ee; 149 | border: 1px solid #dedede; 150 | margin:5px 10px 5px 10px; 151 | padding-bottom: 5px; 152 | } 153 | 154 | ul.sections > li > div.annotation pre { 155 | margin: 7px 0 7px; 156 | padding-left: 15px; 157 | } 158 | 159 | ul.sections > li > div.annotation p tt, .annotation code { 160 | background: #f8f8ff; 161 | border: 1px solid #dedede; 162 | font-size: 12px; 163 | padding: 0 0.2em; 164 | } 165 | } 166 | 167 | /*---------------------- (> 481px) ---------------------*/ 168 | @media only screen and (min-width: 481px) { 169 | #container { 170 | position: relative; 171 | } 172 | body { 173 | background-color: #F5F5FF; 174 | font-size: 15px; 175 | line-height: 22px; 176 | } 177 | pre, tt, code { 178 | line-height: 18px; 179 | } 180 | 181 | #jump_to { 182 | padding: 5px 10px; 183 | } 184 | #jump_wrapper { 185 | padding: 0; 186 | } 187 | #jump_to, #jump_page { 188 | font: 10px Arial; 189 | text-transform: uppercase; 190 | } 191 | #jump_page .source { 192 | padding: 5px 10px; 193 | } 194 | #jump_to a.large { 195 | display: inline-block; 196 | } 197 | #jump_to a.small { 198 | display: none; 199 | } 200 | 201 | 202 | 203 | #background { 204 | position: absolute; 205 | top: 0; bottom: 0; 206 | width: 350px; 207 | background: #ffffff; 208 | border-right: 1px solid #e5e5ee; 209 | z-index: -1; 210 | } 211 | 212 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 213 | padding-left: 40px; 214 | } 215 | 216 | ul.sections > li { 217 | white-space: nowrap; 218 | } 219 | 220 | ul.sections > li > div { 221 | display: inline-block; 222 | } 223 | 224 | ul.sections > li > div.annotation { 225 | max-width: 350px; 226 | min-width: 350px; 227 | min-height: 5px; 228 | padding: 13px; 229 | overflow-x: hidden; 230 | white-space: normal; 231 | vertical-align: top; 232 | text-align: left; 233 | } 234 | ul.sections > li > div.annotation pre { 235 | margin: 15px 0 15px; 236 | padding-left: 15px; 237 | } 238 | 239 | ul.sections > li > div.content { 240 | padding: 13px; 241 | vertical-align: top; 242 | background: #f5f5ff; 243 | border: none; 244 | -webkit-box-shadow: none; 245 | box-shadow: none; 246 | } 247 | 248 | .pilwrap { 249 | position: relative; 250 | display: inline; 251 | } 252 | 253 | .pilcrow { 254 | font: 12px Arial; 255 | text-decoration: none; 256 | color: #454545; 257 | position: absolute; 258 | top: 3px; left: -20px; 259 | padding: 1px 2px; 260 | opacity: 0; 261 | -webkit-transition: opacity 0.2s linear; 262 | } 263 | .for-h1 .pilcrow { 264 | top: 47px; 265 | } 266 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 267 | top: 35px; 268 | } 269 | 270 | ul.sections > li > div.annotation:hover .pilcrow { 271 | opacity: 1; 272 | } 273 | } 274 | 275 | /*---------------------- (> 1025px) ---------------------*/ 276 | @media only screen and (min-width: 1025px) { 277 | 278 | #background { 279 | width: 525px; 280 | } 281 | ul.sections > li > div.annotation { 282 | max-width: 525px; 283 | min-width: 525px; 284 | padding: 10px 25px 1px 50px; 285 | } 286 | ul.sections > li > div.content { 287 | padding: 9px 15px 16px 25px; 288 | } 289 | } 290 | 291 | /*---------------------- Syntax Highlighting -----------------------------*/ 292 | 293 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 294 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 295 | /* 296 | 297 | github.com style (c) Vasily Polovnyov 298 | 299 | */ 300 | 301 | pre code { 302 | display: block; padding: 0.5em; 303 | color: #000; 304 | background: #f8f8ff 305 | } 306 | 307 | pre .comment, 308 | pre .template_comment, 309 | pre .diff .header, 310 | pre .javadoc { 311 | color: #408080; 312 | font-style: italic 313 | } 314 | 315 | pre .keyword, 316 | pre .assignment, 317 | pre .literal, 318 | pre .css .rule .keyword, 319 | pre .winutils, 320 | pre .javascript .title, 321 | pre .lisp .title, 322 | pre .subst { 323 | color: #954121; 324 | /*font-weight: bold*/ 325 | } 326 | 327 | pre .number, 328 | pre .hexcolor { 329 | color: #40a070 330 | } 331 | 332 | pre .string, 333 | pre .tag .value, 334 | pre .phpdoc, 335 | pre .tex .formula { 336 | color: #219161; 337 | } 338 | 339 | pre .title, 340 | pre .id { 341 | color: #19469D; 342 | } 343 | pre .params { 344 | color: #00F; 345 | } 346 | 347 | pre .javascript .title, 348 | pre .lisp .title, 349 | pre .subst { 350 | font-weight: normal 351 | } 352 | 353 | pre .class .title, 354 | pre .haskell .label, 355 | pre .tex .command { 356 | color: #458; 357 | font-weight: bold 358 | } 359 | 360 | pre .tag, 361 | pre .tag .title, 362 | pre .rules .property, 363 | pre .django .tag .keyword { 364 | color: #000080; 365 | font-weight: normal 366 | } 367 | 368 | pre .attribute, 369 | pre .variable, 370 | pre .instancevar, 371 | pre .lisp .body { 372 | color: #008080 373 | } 374 | 375 | pre .regexp { 376 | color: #B68 377 | } 378 | 379 | pre .class { 380 | color: #458; 381 | font-weight: bold 382 | } 383 | 384 | pre .symbol, 385 | pre .ruby .symbol .string, 386 | pre .ruby .symbol .keyword, 387 | pre .ruby .symbol .keymethods, 388 | pre .lisp .keyword, 389 | pre .tex .special, 390 | pre .input_number { 391 | color: #990073 392 | } 393 | 394 | pre .builtin, 395 | pre .constructor, 396 | pre .built_in, 397 | pre .lisp .title { 398 | color: #0086b3 399 | } 400 | 401 | pre .preprocessor, 402 | pre .pi, 403 | pre .doctype, 404 | pre .shebang, 405 | pre .cdata { 406 | color: #999; 407 | font-weight: bold 408 | } 409 | 410 | pre .deletion { 411 | background: #fdd 412 | } 413 | 414 | pre .addition { 415 | background: #dfd 416 | } 417 | 418 | pre .diff .change { 419 | background: #0086b3 420 | } 421 | 422 | pre .chunk { 423 | color: #aaa 424 | } 425 | 426 | pre .tex .formula { 427 | opacity: 0.5; 428 | } -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | html { height: 100%; } 3 | body { 4 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 5 | font-size: 14px; 6 | line-height: 16px; 7 | color: #252519; 8 | margin: 0; padding: 0; 9 | height:100%; 10 | } 11 | #container { min-height: 100%; } 12 | 13 | a { 14 | color: #261a3b; 15 | } 16 | 17 | a:visited { 18 | color: #261a3b; 19 | } 20 | 21 | p, ul, ol { 22 | margin: 0 0 15px; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | margin: 30px 0 15px 0; 27 | } 28 | 29 | h1 { 30 | margin-top: 40px; 31 | } 32 | 33 | hr { 34 | border: 0 none; 35 | border-top: 1px solid #e5e5ee; 36 | height: 1px; 37 | margin: 20px 0; 38 | } 39 | 40 | pre, tt, code { 41 | font-size: 12px; line-height: 16px; 42 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 43 | margin: 0; padding: 0; 44 | } 45 | 46 | ul.sections { 47 | list-style: none; 48 | padding:0 0 5px 0;; 49 | margin:0; 50 | } 51 | 52 | /* 53 | Force border-box so that % widths fit the parent 54 | container without overlap because of margin/padding. 55 | 56 | More Info : http://www.quirksmode.org/css/box.html 57 | */ 58 | ul.sections > li > div { 59 | -moz-box-sizing: border-box; /* firefox */ 60 | -ms-box-sizing: border-box; /* ie */ 61 | -webkit-box-sizing: border-box; /* webkit */ 62 | -khtml-box-sizing: border-box; /* konqueror */ 63 | box-sizing: border-box; /* css3 */ 64 | } 65 | 66 | 67 | /*---------------------- Jump Page -----------------------------*/ 68 | #jump_to, #jump_page { 69 | margin: 0; 70 | background: white; 71 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 72 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 73 | font: 16px Arial; 74 | cursor: pointer; 75 | text-align: right; 76 | list-style: none; 77 | } 78 | 79 | #jump_to a { 80 | text-decoration: none; 81 | } 82 | 83 | #jump_to a.large { 84 | display: none; 85 | } 86 | #jump_to a.small { 87 | font-size: 22px; 88 | font-weight: bold; 89 | color: #676767; 90 | } 91 | 92 | #jump_to, #jump_wrapper { 93 | position: fixed; 94 | right: 0; top: 0; 95 | padding: 10px 15px; 96 | margin:0; 97 | } 98 | 99 | #jump_wrapper { 100 | display: none; 101 | padding:0; 102 | } 103 | 104 | #jump_to:hover #jump_wrapper { 105 | display: block; 106 | } 107 | 108 | #jump_page { 109 | padding: 5px 0 3px; 110 | margin: 0 0 25px 25px; 111 | } 112 | 113 | #jump_page .source { 114 | display: block; 115 | padding: 15px; 116 | text-decoration: none; 117 | border-top: 1px solid #eee; 118 | } 119 | 120 | #jump_page .source:hover { 121 | background: #f5f5ff; 122 | } 123 | 124 | #jump_page .source:first-child { 125 | } 126 | 127 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 128 | @media only screen and (min-width: 320px) { 129 | .pilwrap { display: none; } 130 | 131 | ul.sections > li > div { 132 | display: block; 133 | padding:5px 10px 0 10px; 134 | } 135 | 136 | ul.sections > li > div.annotation { 137 | background: #fff; 138 | } 139 | 140 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 141 | padding-left: 30px; 142 | } 143 | 144 | ul.sections > li > div.content { 145 | background: #f5f5ff; 146 | overflow-x:auto; 147 | -webkit-box-shadow: inset 0 0 5px #e5e5ee; 148 | box-shadow: inset 0 0 5px #e5e5ee; 149 | border: 1px solid #dedede; 150 | margin:5px 10px 5px 10px; 151 | padding-bottom: 5px; 152 | } 153 | 154 | ul.sections > li > div.annotation pre { 155 | margin: 7px 0 7px; 156 | padding-left: 15px; 157 | } 158 | 159 | ul.sections > li > div.annotation p tt, .annotation code { 160 | background: #f8f8ff; 161 | border: 1px solid #dedede; 162 | font-size: 12px; 163 | padding: 0 0.2em; 164 | } 165 | } 166 | 167 | /*---------------------- (> 481px) ---------------------*/ 168 | @media only screen and (min-width: 481px) { 169 | #container { 170 | position: relative; 171 | } 172 | body { 173 | background-color: #F5F5FF; 174 | font-size: 15px; 175 | line-height: 22px; 176 | } 177 | pre, tt, code { 178 | line-height: 18px; 179 | } 180 | 181 | #jump_to { 182 | padding: 5px 10px; 183 | } 184 | #jump_wrapper { 185 | padding: 0; 186 | } 187 | #jump_to, #jump_page { 188 | font: 10px Arial; 189 | text-transform: uppercase; 190 | } 191 | #jump_page .source { 192 | padding: 5px 10px; 193 | } 194 | #jump_to a.large { 195 | display: inline-block; 196 | } 197 | #jump_to a.small { 198 | display: none; 199 | } 200 | 201 | 202 | 203 | #background { 204 | position: absolute; 205 | top: 0; bottom: 0; 206 | width: 350px; 207 | background: #ffffff; 208 | border-right: 1px solid #e5e5ee; 209 | z-index: -1; 210 | } 211 | 212 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 213 | padding-left: 40px; 214 | } 215 | 216 | ul.sections > li { 217 | white-space: nowrap; 218 | } 219 | 220 | ul.sections > li > div { 221 | display: inline-block; 222 | } 223 | 224 | ul.sections > li > div.annotation { 225 | max-width: 350px; 226 | min-width: 350px; 227 | min-height: 5px; 228 | padding: 13px; 229 | overflow-x: hidden; 230 | white-space: normal; 231 | vertical-align: top; 232 | text-align: left; 233 | } 234 | ul.sections > li > div.annotation pre { 235 | margin: 15px 0 15px; 236 | padding-left: 15px; 237 | } 238 | 239 | ul.sections > li > div.content { 240 | padding: 13px; 241 | vertical-align: top; 242 | background: #f5f5ff; 243 | border: none; 244 | -webkit-box-shadow: none; 245 | box-shadow: none; 246 | } 247 | 248 | .pilwrap { 249 | position: relative; 250 | display: inline; 251 | } 252 | 253 | .pilcrow { 254 | font: 12px Arial; 255 | text-decoration: none; 256 | color: #454545; 257 | position: absolute; 258 | top: 3px; left: -20px; 259 | padding: 1px 2px; 260 | opacity: 0; 261 | -webkit-transition: opacity 0.2s linear; 262 | } 263 | .for-h1 .pilcrow { 264 | top: 47px; 265 | } 266 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 267 | top: 35px; 268 | } 269 | 270 | ul.sections > li > div.annotation:hover .pilcrow { 271 | opacity: 1; 272 | } 273 | } 274 | 275 | /*---------------------- (> 1025px) ---------------------*/ 276 | @media only screen and (min-width: 1025px) { 277 | 278 | #background { 279 | width: 525px; 280 | } 281 | ul.sections > li > div.annotation { 282 | max-width: 525px; 283 | min-width: 525px; 284 | padding: 10px 25px 1px 50px; 285 | } 286 | ul.sections > li > div.content { 287 | padding: 9px 15px 16px 25px; 288 | } 289 | } 290 | 291 | /*---------------------- Syntax Highlighting -----------------------------*/ 292 | 293 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 294 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 295 | /* 296 | 297 | github.com style (c) Vasily Polovnyov 298 | 299 | */ 300 | 301 | pre code { 302 | display: block; padding: 0.5em; 303 | color: #000; 304 | background: #f8f8ff 305 | } 306 | 307 | pre .comment, 308 | pre .template_comment, 309 | pre .diff .header, 310 | pre .javadoc { 311 | color: #408080; 312 | font-style: italic 313 | } 314 | 315 | pre .keyword, 316 | pre .assignment, 317 | pre .literal, 318 | pre .css .rule .keyword, 319 | pre .winutils, 320 | pre .javascript .title, 321 | pre .lisp .title, 322 | pre .subst { 323 | color: #954121; 324 | /*font-weight: bold*/ 325 | } 326 | 327 | pre .number, 328 | pre .hexcolor { 329 | color: #40a070 330 | } 331 | 332 | pre .string, 333 | pre .tag .value, 334 | pre .phpdoc, 335 | pre .tex .formula { 336 | color: #219161; 337 | } 338 | 339 | pre .title, 340 | pre .id { 341 | color: #19469D; 342 | } 343 | pre .params { 344 | color: #00F; 345 | } 346 | 347 | pre .javascript .title, 348 | pre .lisp .title, 349 | pre .subst { 350 | font-weight: normal 351 | } 352 | 353 | pre .class .title, 354 | pre .haskell .label, 355 | pre .tex .command { 356 | color: #458; 357 | font-weight: bold 358 | } 359 | 360 | pre .tag, 361 | pre .tag .title, 362 | pre .rules .property, 363 | pre .django .tag .keyword { 364 | color: #000080; 365 | font-weight: normal 366 | } 367 | 368 | pre .attribute, 369 | pre .variable, 370 | pre .instancevar, 371 | pre .lisp .body { 372 | color: #008080 373 | } 374 | 375 | pre .regexp { 376 | color: #B68 377 | } 378 | 379 | pre .class { 380 | color: #458; 381 | font-weight: bold 382 | } 383 | 384 | pre .symbol, 385 | pre .ruby .symbol .string, 386 | pre .ruby .symbol .keyword, 387 | pre .ruby .symbol .keymethods, 388 | pre .lisp .keyword, 389 | pre .tex .special, 390 | pre .input_number { 391 | color: #990073 392 | } 393 | 394 | pre .builtin, 395 | pre .constructor, 396 | pre .built_in, 397 | pre .lisp .title { 398 | color: #0086b3 399 | } 400 | 401 | pre .preprocessor, 402 | pre .pi, 403 | pre .doctype, 404 | pre .shebang, 405 | pre .cdata { 406 | color: #999; 407 | font-weight: bold 408 | } 409 | 410 | pre .deletion { 411 | background: #fdd 412 | } 413 | 414 | pre .addition { 415 | background: #dfd 416 | } 417 | 418 | pre .diff .change { 419 | background: #0086b3 420 | } 421 | 422 | pre .chunk { 423 | color: #aaa 424 | } 425 | 426 | pre .tex .formula { 427 | opacity: 0.5; 428 | } -------------------------------------------------------------------------------- /tests/nestedValidation.js: -------------------------------------------------------------------------------- 1 | buster.testCase('Nested validation', { 2 | "one level": { 3 | setUp: function() { 4 | this.valid = this.spy(); 5 | this.invalid = this.spy(); 6 | 7 | var Model = Backbone.Model.extend({ 8 | validation: { 9 | 'address.street': { 10 | required: true, 11 | msg: 'error' 12 | } 13 | } 14 | }); 15 | 16 | this.model = new Model(); 17 | this.view = new Backbone.View({model: this.model}); 18 | 19 | Backbone.Validation.bind(this.view, { 20 | invalid: this.invalid, 21 | valid: this.valid 22 | }); 23 | }, 24 | 25 | "invalid": { 26 | setUp: function () { 27 | this.result = this.model.set({ 28 | address: { 29 | street:'' 30 | } 31 | }, {validate: true}); 32 | }, 33 | 34 | "refutes setting invalid values": function() { 35 | refute(this.result); 36 | }, 37 | 38 | "calls the invalid callback": function() { 39 | assert.calledWith(this.invalid, this.view, 'address.street', 'error'); 40 | }, 41 | 42 | "is valid returns false for the specified attribute name": function () { 43 | refute(this.model.isValid('address.street')); 44 | }, 45 | 46 | "is valid returns false for the specified attribute names": function () { 47 | refute(this.model.isValid(['address.street', 'address.street'])); 48 | }, 49 | 50 | "pre validate returns error message for the specified attribute name": function () { 51 | assert(this.model.preValidate('address.street', '')); 52 | } 53 | }, 54 | 55 | "valid": { 56 | setUp: function () { 57 | this.result = this.model.set({ 58 | address: { 59 | street: 'name' 60 | } 61 | }, {validate: true}); 62 | }, 63 | 64 | "sets the value": function() { 65 | assert(this.result); 66 | }, 67 | 68 | "calls the valid callback": function() { 69 | assert.calledWith(this.valid, this.view, 'address.street'); 70 | }, 71 | 72 | "is valid returns true for the specified attribute name": function () { 73 | assert(this.model.isValid('address.street')); 74 | }, 75 | 76 | "is valid returns true for the specified attribute names": function () { 77 | assert(this.model.isValid(['address.street', 'address.street'])); 78 | }, 79 | 80 | "pre validate returns no error message for the specified attribute name": function () { 81 | refute(this.model.preValidate('address.street', 'street')); 82 | } 83 | } 84 | }, 85 | 86 | "two levels": { 87 | setUp: function() { 88 | this.valid = this.spy(); 89 | this.invalid = this.spy(); 90 | 91 | var Model = Backbone.Model.extend({ 92 | validation: { 93 | 'foo.bar.baz': { 94 | required: true, 95 | msg: 'error' 96 | } 97 | } 98 | }); 99 | 100 | this.model = new Model(); 101 | this.view = new Backbone.View({model: this.model}); 102 | 103 | Backbone.Validation.bind(this.view, { 104 | invalid: this.invalid, 105 | valid: this.valid 106 | }); 107 | }, 108 | 109 | "invalid": { 110 | setUp: function () { 111 | this.result = this.model.set({ 112 | foo: { 113 | bar: { 114 | baz: '' 115 | } 116 | } 117 | }, {validate: true}); 118 | }, 119 | 120 | "refutes setting invalid values": function() { 121 | refute(this.result); 122 | }, 123 | 124 | "calls the invalid callback": function() { 125 | assert.calledWith(this.invalid, this.view, 'foo.bar.baz', 'error'); 126 | }, 127 | 128 | "is valid returns false for the specified attribute name": function () { 129 | refute(this.model.isValid('foo.bar.baz')); 130 | }, 131 | 132 | "is valid returns false for the specified attribute names": function () { 133 | refute(this.model.isValid(['foo.bar.baz', 'foo.bar.baz'])); 134 | }, 135 | 136 | "pre validate returns error message for the specified attribute name": function () { 137 | assert(this.model.preValidate('foo.bar.baz', '')); 138 | } 139 | }, 140 | 141 | "valid": { 142 | setUp: function () { 143 | this.result = this.model.set({ 144 | foo: { 145 | bar: { 146 | baz: 'val' 147 | } 148 | } 149 | }, {validate: true}); 150 | }, 151 | 152 | "sets the value": function() { 153 | assert(this.result); 154 | }, 155 | 156 | "calls the valid callback": function() { 157 | assert.calledWith(this.valid, this.view, 'foo.bar.baz'); 158 | }, 159 | 160 | "is valid returns true for the specified attribute name": function () { 161 | assert(this.model.isValid('foo.bar.baz')); 162 | }, 163 | 164 | "is valid returns true for the specified attribute names": function () { 165 | assert(this.model.isValid(['foo.bar.baz', 'foo.bar.baz'])); 166 | }, 167 | 168 | "pre validate returns no error message for the specified attribute name": function () { 169 | refute(this.model.preValidate('foo.bar.baz', 'val')); 170 | } 171 | } 172 | }, 173 | 174 | "arrays": { 175 | setUp: function() { 176 | var Model = Backbone.Model.extend({ 177 | validation: { 178 | 'dogs.0.name': { 179 | required: true, 180 | msg: 'error' 181 | } 182 | } 183 | }); 184 | 185 | this.model = new Model(); 186 | this.view = new Backbone.View({ model: this.model }); 187 | Backbone.Validation.bind(this.view, {}); 188 | }, 189 | 190 | "invalid": { 191 | setUp: function () { 192 | this.result = this.model.set({ 193 | dogs: [{ name: '' }] 194 | }, {validate: true}); 195 | }, 196 | 197 | "refutes setting invalid values": function() { 198 | refute(this.result); 199 | }, 200 | 201 | "is valid returns false for the specified attribute name": function () { 202 | refute(this.model.isValid('dogs.0.name')); 203 | }, 204 | }, 205 | 206 | "valid": { 207 | setUp: function () { 208 | this.result = this.model.set({ 209 | dogs: [{ name: 'good boy' }] 210 | }, {validate: true}); 211 | }, 212 | 213 | "sets the value": function() { 214 | assert(this.result); 215 | }, 216 | 217 | "is valid returns true for the specified attribute name": function () { 218 | assert(this.model.isValid('dogs.0.name')); 219 | }, 220 | } 221 | }, 222 | 223 | "complex nesting": { 224 | setUp: function() { 225 | this.valid = this.spy(); 226 | this.invalid = this.spy(); 227 | 228 | var Model = Backbone.Model.extend({ 229 | validation: { 230 | 'foo.bar.baz': { 231 | required: true, 232 | msg: 'error' 233 | }, 234 | 'foo.foo': { 235 | required: true, 236 | msg: 'error' 237 | } 238 | } 239 | }); 240 | 241 | this.model = new Model(); 242 | this.view = new Backbone.View({model: this.model}); 243 | 244 | Backbone.Validation.bind(this.view, { 245 | invalid: this.invalid, 246 | valid: this.valid 247 | }); 248 | }, 249 | 250 | "invalid": { 251 | setUp: function () { 252 | this.result = this.model.set({ 253 | foo: { 254 | foo: '', 255 | bar: { 256 | baz: '' 257 | } 258 | } 259 | }, {validate: true}); 260 | }, 261 | 262 | "refutes setting invalid values": function() { 263 | refute(this.result); 264 | }, 265 | 266 | "calls the invalid callback": function() { 267 | assert.calledWith(this.invalid, this.view, 'foo.bar.baz', 'error'); 268 | assert.calledWith(this.invalid, this.view, 'foo.foo', 'error'); 269 | }, 270 | 271 | "is valid returns false for the specified attribute name": function () { 272 | refute(this.model.isValid('foo.bar.baz')); 273 | refute(this.model.isValid('foo.foo')); 274 | }, 275 | 276 | "is valid returns false for the specified attribute names": function () { 277 | refute(this.model.isValid(['foo.foo', 'foo.bar.baz'])); 278 | }, 279 | 280 | "pre validate returns error message for the specified attribute name": function () { 281 | assert(this.model.preValidate('foo.bar.baz', '')); 282 | assert(this.model.preValidate('foo.foo', '')); 283 | } 284 | }, 285 | 286 | "valid": { 287 | setUp: function () { 288 | this.result = this.model.set({ 289 | foo: { 290 | foo: 'val', 291 | bar: { 292 | baz: 'val' 293 | } 294 | } 295 | }, {validate: true}); 296 | }, 297 | 298 | "sets the value": function() { 299 | assert(this.result); 300 | }, 301 | 302 | "calls the valid callback": function() { 303 | assert.calledWith(this.valid, this.view, 'foo.bar.baz'); 304 | assert.calledWith(this.valid, this.view, 'foo.foo'); 305 | }, 306 | 307 | "is valid returns true for the specified attribute name": function () { 308 | assert(this.model.isValid('foo.bar.baz')); 309 | assert(this.model.isValid('foo.foo')); 310 | }, 311 | 312 | "is valid returns true for the specified attribute names": function () { 313 | assert(this.model.isValid(['foo.bar.baz', 'foo.foo'])); 314 | }, 315 | 316 | "pre validate returns no error message for the specified attribute name": function () { 317 | refute(this.model.preValidate('foo.bar.baz', 'val')); 318 | refute(this.model.preValidate('foo.foo', 'val')); 319 | } 320 | } 321 | }, 322 | 323 | "complex nesting with intermediate-level validators": { 324 | setUp: function() { 325 | this.valid = this.spy(); 326 | this.invalid = this.spy(); 327 | 328 | var Model = Backbone.Model.extend({ 329 | validation: { 330 | 'foo.bar': { 331 | fn: 'validateBazAndQux', 332 | msg: 'bazQuxError1' 333 | }, 334 | 'foo.foo': { 335 | fn: 'validateBazAndQux', 336 | msg: 'bazQuxError2' 337 | } 338 | }, 339 | validateBazAndQux: function (value, attr, computedState) { 340 | if (!value || !value.baz || !value.qux) { 341 | return "error"; 342 | } 343 | } 344 | }); 345 | 346 | this.model = new Model(); 347 | this.view = new Backbone.View({model: this.model}); 348 | 349 | Backbone.Validation.bind(this.view, { 350 | invalid: this.invalid, 351 | valid: this.valid 352 | }); 353 | }, 354 | 355 | "invalid": { 356 | setUp: function () { 357 | this.result = this.model.set({ 358 | foo: { 359 | bar: { 360 | baz: '', 361 | qux: 'qux' 362 | }, 363 | foo: { 364 | baz: 'baz', 365 | qux: '' 366 | } 367 | } 368 | }, {validate: true}); 369 | }, 370 | 371 | "refutes setting invalid values": function() { 372 | refute(this.result); 373 | }, 374 | 375 | "calls the invalid callback": function() { 376 | assert.calledWith(this.invalid, this.view, 'foo.bar', 'bazQuxError1'); 377 | assert.calledWith(this.invalid, this.view, 'foo.foo', 'bazQuxError2'); 378 | }, 379 | 380 | "isValid returns false for the specified attribute name": function () { 381 | refute(this.model.isValid('foo.bar')); 382 | refute(this.model.isValid('foo.foo')); 383 | }, 384 | 385 | "isValid returns false for the specified attribute names": function () { 386 | refute(this.model.isValid(['foo.foo', 'foo.bar'])); 387 | }, 388 | 389 | "preValidate returns error message for the specified attribute name": function () { 390 | assert(this.model.preValidate('foo.bar', '')); 391 | assert(this.model.preValidate('foo.foo', '')); 392 | }, 393 | 394 | "preValidate does not return error message if new nested values validate": function () { 395 | refute(this.model.preValidate('foo.bar', { baz: 1, qux: 1 })); 396 | refute(this.model.preValidate('foo.foo', { baz: 1, qux: 1 })); 397 | } 398 | }, 399 | 400 | "valid": { 401 | setUp: function () { 402 | this.result = this.model.set({ 403 | foo: { 404 | bar: { 405 | baz: 'val', 406 | qux: 'val' 407 | }, 408 | foo: { 409 | baz: 'val', 410 | qux: 'val' 411 | } 412 | } 413 | }, {validate: true}); 414 | }, 415 | 416 | "sets the value": function() { 417 | assert(this.result); 418 | }, 419 | 420 | "calls the valid callback": function() { 421 | assert.calledWith(this.valid, this.view, 'foo.bar'); 422 | assert.calledWith(this.valid, this.view, 'foo.foo'); 423 | }, 424 | 425 | "isValid returns true for the specified attribute name": function () { 426 | assert(this.model.isValid('foo.bar')); 427 | assert(this.model.isValid('foo.foo')); 428 | }, 429 | 430 | "isValid returns true for the specified attribute names": function () { 431 | assert(this.model.isValid(['foo.bar', 'foo.foo'])); 432 | }, 433 | 434 | "preValidate returns no error message for the specified attribute name": function () { 435 | refute(this.model.preValidate('foo.bar', { baz: 1, qux: 1 })); 436 | refute(this.model.preValidate('foo.foo', { baz: 1, qux: 1 })); 437 | }, 438 | 439 | "preValidate returns error message if new nested values do not validate": function () { 440 | assert.equals(this.model.preValidate('foo.bar', { baz: '', qux: '' }), 'bazQuxError1'); 441 | assert.equals(this.model.preValidate('foo.foo', { baz: '', qux: '' }), 'bazQuxError2'); 442 | } 443 | } 444 | }, 445 | 446 | "nested models and collections": { 447 | setUp: function () { 448 | this.valid = this.spy(); 449 | this.invalid = this.spy(); 450 | 451 | var Model = Backbone.Model.extend({ 452 | }); 453 | 454 | var Collection = Backbone.Collection.extend({ 455 | model: Model 456 | }); 457 | 458 | this.model = new Model(); 459 | this.model.set({ 460 | model: this.model, 461 | collection: new Collection([this.model]) 462 | }); 463 | this.view = new Backbone.View({model: this.model}); 464 | 465 | Backbone.Validation.bind(this.view, { 466 | invalid: this.invalid, 467 | valid: this.valid 468 | }); 469 | 470 | this.result = this.model.set({ 471 | foo: 'bar' 472 | }, {validate: true}); 473 | }, 474 | 475 | "are ignored": function() { 476 | assert(this.result); 477 | } 478 | } 479 | }); 480 | -------------------------------------------------------------------------------- /tests/binding.js: -------------------------------------------------------------------------------- 1 | buster.testCase('Binding to view with model', { 2 | setUp: function() { 3 | var View = Backbone.View.extend({ 4 | render: function() { 5 | Backbone.Validation.bind(this); 6 | } 7 | }); 8 | var Model = Backbone.Model.extend({ 9 | validation: { 10 | name: function(val) { 11 | if (!val) { 12 | return 'Name is invalid'; 13 | } 14 | } 15 | } 16 | }); 17 | this.model = new Model(); 18 | this.view = new View({ 19 | model: this.model 20 | }); 21 | 22 | this.view.render(); 23 | }, 24 | 25 | tearDown: function() { 26 | this.view.remove(); 27 | }, 28 | 29 | "the model's validate function is defined": function() { 30 | assert.defined(this.model.validate); 31 | }, 32 | 33 | "the model's isValid function is overridden": function() { 34 | refute.same(this.model.isValid, Backbone.Model.prototype.isValid); 35 | }, 36 | 37 | "and passing custom callbacks with the options": { 38 | setUp: function(){ 39 | this.valid = this.spy(); 40 | this.invalid = this.spy(); 41 | 42 | Backbone.Validation.bind(this.view, { 43 | valid: this.valid, 44 | invalid: this.invalid 45 | }); 46 | }, 47 | 48 | "should call valid callback passed with options": function() { 49 | this.model.set({ 50 | name: 'Ben' 51 | }, {validate: true}); 52 | 53 | assert.called(this.valid); 54 | }, 55 | 56 | "should call invalid callback passed with options": function() { 57 | this.model.set({ 58 | name: '' 59 | }, {validate: true}); 60 | 61 | assert.called(this.invalid); 62 | } 63 | }, 64 | 65 | "and passing custom callbacks and selector with the options": { 66 | setUp: function(){ 67 | this.valid = this.spy(); 68 | this.invalid = this.spy(); 69 | 70 | Backbone.Validation.bind(this.view, { 71 | selector: 'some-selector', 72 | valid: this.valid, 73 | invalid: this.invalid 74 | }); 75 | }, 76 | 77 | "should call valid callback with correct selector": function() { 78 | this.model.set({ 79 | name: 'Ben' 80 | }, {validate: true}); 81 | 82 | assert.calledWith(this.valid, this.view, 'name', 'some-selector'); 83 | }, 84 | 85 | "should call invalid callback with correct selector": function() { 86 | this.model.set({ 87 | name: '' 88 | }, {validate: true}); 89 | 90 | assert.calledWith(this.invalid, this.view, 'name', 'Name is invalid', 'some-selector'); 91 | } 92 | }, 93 | 94 | "and unbinding":{ 95 | setUp: function(){ 96 | Backbone.Validation.unbind(this.view); 97 | }, 98 | 99 | "the model's validate function is undefined": function() { 100 | refute.defined(this.model.validate); 101 | }, 102 | 103 | "the model's preValidate function is undefined": function() { 104 | refute.defined(this.model.preValidate); 105 | }, 106 | 107 | "the model's isValid function is restored": function() { 108 | assert.same(this.model.isValid, Backbone.Model.prototype.isValid); 109 | } 110 | } 111 | }); 112 | 113 | buster.testCase('Binding to view with optional model', { 114 | setUp: function() { 115 | var self = this; 116 | 117 | this.valid = this.spy(); 118 | this.invalid = this.spy(); 119 | 120 | var Model = Backbone.Model.extend({ 121 | validation: { 122 | name: function(val) { 123 | if (!val) { 124 | return 'Name is invalid'; 125 | } 126 | } 127 | } 128 | }); 129 | this.model = new Model(); 130 | 131 | var View = Backbone.View.extend({ 132 | render: function() { 133 | Backbone.Validation.bind(this, { 134 | model: self.model, 135 | valid: self.valid, 136 | invalid: self.invalid 137 | }); 138 | } 139 | }); 140 | this.view = new View(); 141 | 142 | this.view.render(); 143 | }, 144 | 145 | tearDown: function() { 146 | this.view.remove(); 147 | }, 148 | 149 | "the model's validate function is defined": function() { 150 | assert.defined(this.model.validate); 151 | }, 152 | 153 | "the model's isValid function is overridden": function() { 154 | refute.same(this.model.isValid, Backbone.Model.prototype.isValid); 155 | }, 156 | 157 | "should call valid callback passed with options": function() { 158 | this.model.set({ 159 | name: 'Ben' 160 | }, {validate: true}); 161 | 162 | assert.called(this.valid); 163 | }, 164 | 165 | "should call invalid callback passed with options": function() { 166 | this.model.set({ 167 | name: '' 168 | }, {validate: true}); 169 | 170 | assert.called(this.invalid); 171 | }, 172 | 173 | "and unbinding":{ 174 | setUp: function(){ 175 | Backbone.Validation.unbind(this.view, {model: this.model}); 176 | }, 177 | 178 | "the model's validate function is undefined": function() { 179 | refute.defined(this.model.validate); 180 | }, 181 | 182 | "the model's preValidate function is undefined": function() { 183 | refute.defined(this.model.preValidate); 184 | }, 185 | 186 | "the model's isValid function is restored": function() { 187 | assert.same(this.model.isValid, Backbone.Model.prototype.isValid); 188 | } 189 | } 190 | }); 191 | 192 | buster.testCase('Binding to view with collection', { 193 | setUp: function() { 194 | var View = Backbone.View.extend({ 195 | render: function() { 196 | Backbone.Validation.bind(this); 197 | } 198 | }); 199 | this.Model = Backbone.Model.extend({ 200 | validation: { 201 | name: function(val) { 202 | if (!val) { 203 | return 'Name is invalid'; 204 | } 205 | } 206 | } 207 | }); 208 | var Collection = Backbone.Collection.extend({ 209 | model: this.Model 210 | }); 211 | this.collection = new Collection([{name: 'Tom'}, {name: 'Thea'}]); 212 | this.view = new View({ 213 | collection: this.collection 214 | }); 215 | 216 | this.view.render(); 217 | }, 218 | 219 | tearDown: function() { 220 | this.view.remove(); 221 | }, 222 | 223 | "binds existing models in collection when binding": function() { 224 | assert.defined(this.collection.at(0).validate); 225 | assert.defined(this.collection.at(1).validate); 226 | }, 227 | 228 | "binds model that is added to the collection": function() { 229 | var model = new this.Model({name: 'Thomas'}); 230 | this.collection.add(model); 231 | 232 | assert.defined(model.validate); 233 | }, 234 | 235 | "binds models that are batch added to the collection": function() { 236 | var model1 = new this.Model({name: 'Thomas'}); 237 | var model2 = new this.Model({name: 'Hans'}); 238 | this.collection.add([model1, model2]); 239 | 240 | assert.defined(model1.validate); 241 | assert.defined(model2.validate); 242 | }, 243 | 244 | "unbinds model that is removed from collection": function() { 245 | var model = this.collection.at(0); 246 | this.collection.remove(model); 247 | 248 | refute.defined(model.validate); 249 | }, 250 | 251 | "unbinds models that are batch removed from collection": function() { 252 | var model1 = this.collection.at(0); 253 | var model2 = this.collection.at(1); 254 | this.collection.remove([model1, model2]); 255 | 256 | refute.defined(model1.validate); 257 | refute.defined(model2.validate); 258 | }, 259 | 260 | "unbinds all models in collection when unbinding view": function() { 261 | Backbone.Validation.unbind(this.view); 262 | 263 | refute.defined(this.collection.at(0).validate); 264 | refute.defined(this.collection.at(1).validate); 265 | }, 266 | 267 | "unbinds all collection events when unbinding view": function() { 268 | var that = this; 269 | Backbone.Validation.unbind(this.view); 270 | 271 | refute.exception(function() { that.collection.trigger('add'); }); 272 | refute.exception(function() { that.collection.trigger('remove'); }); 273 | } 274 | }); 275 | 276 | buster.testCase('Binding to view with optional collection', { 277 | setUp: function() { 278 | var self = this; 279 | this.Model = Backbone.Model.extend({ 280 | validation: { 281 | name: function(val) { 282 | if (!val) { 283 | return 'Name is invalid'; 284 | } 285 | } 286 | } 287 | }); 288 | var Collection = Backbone.Collection.extend({ 289 | model: this.Model 290 | }); 291 | this.collection = new Collection([{name: 'Tom'}, {name: 'Thea'}]); 292 | var View = Backbone.View.extend({ 293 | render: function() { 294 | Backbone.Validation.bind(this, {collection: self.collection}); 295 | } 296 | }); 297 | this.view = new View(); 298 | 299 | this.view.render(); 300 | }, 301 | 302 | tearDown: function() { 303 | this.view.remove(); 304 | }, 305 | 306 | "binds existing models in collection when binding": function() { 307 | assert.defined(this.collection.at(0).validate); 308 | assert.defined(this.collection.at(1).validate); 309 | }, 310 | 311 | "binds model that is added to the collection": function() { 312 | var model = new this.Model({name: 'Thomas'}); 313 | this.collection.add(model); 314 | 315 | assert.defined(model.validate); 316 | }, 317 | 318 | "binds models that are batch added to the collection": function() { 319 | var model1 = new this.Model({name: 'Thomas'}); 320 | var model2 = new this.Model({name: 'Hans'}); 321 | this.collection.add([model1, model2]); 322 | 323 | assert.defined(model1.validate); 324 | assert.defined(model2.validate); 325 | }, 326 | 327 | "unbinds model that is removed from collection": function() { 328 | var model = this.collection.at(0); 329 | this.collection.remove(model); 330 | 331 | refute.defined(model.validate); 332 | }, 333 | 334 | "unbinds models that are batch removed from collection": function() { 335 | var model1 = this.collection.at(0); 336 | var model2 = this.collection.at(1); 337 | this.collection.remove([model1, model2]); 338 | 339 | refute.defined(model1.validate); 340 | refute.defined(model2.validate); 341 | }, 342 | 343 | "unbinds all models in collection when unbinding view": function() { 344 | Backbone.Validation.unbind(this.view, {collection: this.collection}); 345 | 346 | refute.defined(this.collection.at(0).validate); 347 | refute.defined(this.collection.at(1).validate); 348 | }, 349 | 350 | "unbinds all collection events when unbinding view": function() { 351 | var that = this; 352 | Backbone.Validation.unbind(this.view, {collection: this.collection}); 353 | 354 | refute.exception(function() { that.collection.trigger('add'); }); 355 | refute.exception(function() { that.collection.trigger('remove'); }); 356 | } 357 | }); 358 | 359 | buster.testCase('Binding to view with no model or collection', { 360 | "throws exception": function(){ 361 | assert.exception(function(){ 362 | Backbone.Validation.bind(new Backbone.View()); 363 | }); 364 | } 365 | }); 366 | 367 | buster.testCase('Binding multiple views to same model', { 368 | setUp: function() { 369 | var Model = Backbone.Model.extend({ 370 | validation: { 371 | name: function(val) { 372 | if (!val) { 373 | return 'Name is invalid'; 374 | } 375 | }, 376 | surname: function(val) { 377 | if (!val) { 378 | return 'Surname is invalid'; 379 | } 380 | } 381 | } 382 | }); 383 | var View = Backbone.View.extend({ 384 | initialize: function(data){ 385 | this.attributeName = data.attributeName; 386 | }, 387 | render: function() { 388 | var html = $(''); 389 | this.$el.append(html); 390 | Backbone.Validation.bind(this); 391 | } 392 | }); 393 | this.model = new Model(); 394 | this.view1 = new View({ 395 | attributeName: 'name', 396 | model: this.model 397 | }); 398 | this.view2 = new View({ 399 | attributeName: 'surname', 400 | model: this.model 401 | }); 402 | this.view1.render(); 403 | this.view2.render(); 404 | this.name = $(this.view1.$('[name~=name]')); 405 | this.surname = $(this.view2.$('[name~=surname]')); 406 | }, 407 | 408 | tearDown: function() { 409 | this.view1.remove(); 410 | }, 411 | 412 | "both elements receive invalid class and data-error message when validating the model": function() { 413 | this.model.validate(); 414 | 415 | assert(this.name.hasClass('invalid')); 416 | assert(this.surname.hasClass('invalid')); 417 | assert.equals(this.name.data('error'), 'Name is invalid'); 418 | assert.equals(this.surname.data('error'), 'Surname is invalid'); 419 | }, 420 | 421 | "each element validates separately": { 422 | setUp: function() { 423 | this.model.set({ 424 | name: 'Rafael' 425 | }); 426 | this.model.validate(); 427 | }, 428 | 429 | "first element should not have invalid class": function() { 430 | refute(this.name.hasClass('invalid')); 431 | }, 432 | 433 | "second element should have invalid class": function() { 434 | assert(this.surname.hasClass('invalid')); 435 | } 436 | }, 437 | 438 | "each view can be unbind separately from the same model": { 439 | setUp: function() { 440 | this.model.set('name', ''); 441 | this.view2.render(); 442 | Backbone.Validation.unbind(this.view2); 443 | this.model.validate(); 444 | }, 445 | 446 | "first element is invalid and has class invalid": function() { 447 | refute(this.model.isValid('name')); 448 | assert(this.name.hasClass('invalid')); 449 | }, 450 | 451 | "second element is invalid and has not class invalid": function() { 452 | refute(this.model.isValid('surname')); 453 | refute(this.surname.hasClass('invalid')); 454 | } 455 | } 456 | }); -------------------------------------------------------------------------------- /tests/general.js: -------------------------------------------------------------------------------- 1 | buster.testCase("Backbone.Validation", { 2 | setUp: function() { 3 | var View = Backbone.View.extend({ 4 | render: function() { 5 | var html = $(''); 6 | this.$el.append(html); 7 | } 8 | }); 9 | 10 | var Model = Backbone.Model.extend({ 11 | validation: { 12 | age: function(val) { 13 | if (!val) { 14 | return 'Age is invalid'; 15 | } 16 | }, 17 | name: function(val) { 18 | if (!val) { 19 | return 'Name is invalid'; 20 | } 21 | } 22 | } 23 | }); 24 | 25 | this.model = new Model(); 26 | this.view = new View({ 27 | model: this.model 28 | }); 29 | 30 | this.view.render(); 31 | this.age = $(this.view.$('[name~=age]')); 32 | this.name = $(this.view.$('[name~=name]')); 33 | }, 34 | 35 | tearDown: function() { 36 | this.view.remove(); 37 | }, 38 | 39 | "when unbinding view without model": { 40 | setUp: function(){ 41 | Backbone.Validation.bind(this.view); 42 | }, 43 | 44 | tearDown: function() { 45 | this.view.model = this.model; 46 | }, 47 | 48 | "nothing happens": function() { 49 | delete this.view.model; 50 | Backbone.Validation.unbind(this.view); 51 | assert(true); 52 | } 53 | }, 54 | 55 | "when unbinding view which was not bound": { 56 | "nothing happens": function(){ 57 | Backbone.Validation.unbind(new Backbone.View({model:new Backbone.Model()})); 58 | assert(true); 59 | } 60 | }, 61 | 62 | "when bound to model with two validated attributes": { 63 | setUp: function() { 64 | Backbone.Validation.bind(this.view); 65 | }, 66 | 67 | "attribute without validator should be set sucessfully": function() { 68 | assert(this.model.set({ 69 | someProperty: true 70 | }, {validate: true})); 71 | }, 72 | 73 | "and setting": { 74 | 75 | "one valid value": { 76 | setUp: function() { 77 | this.model.set({ 78 | age: 1 79 | }, {validate: true}); 80 | }, 81 | 82 | "element should not have invalid class": function() { 83 | refute(this.age.hasClass('invalid')); 84 | }, 85 | 86 | "element should not have data property with error message": function() { 87 | refute.defined(this.age.data('error')); 88 | }, 89 | 90 | "should return the model": function() { 91 | assert.same(this.model.set({ 92 | age: 1 93 | }, {validate: true}), this.model); 94 | }, 95 | 96 | "should update the model": function() { 97 | assert.equals(this.model.get('age'), 1); 98 | }, 99 | 100 | "model should be invalid": function() { 101 | refute(this.model.isValid()); 102 | } 103 | }, 104 | 105 | "one invalid value": { 106 | setUp: function() { 107 | this.model.set({ 108 | age: 0 109 | }, {validate: true}); 110 | }, 111 | 112 | "element should have invalid class": function() { 113 | assert(this.age.hasClass('invalid')); 114 | }, 115 | 116 | "element should have data attribute with error message": function() { 117 | assert.equals(this.age.data('error'), 'Age is invalid'); 118 | }, 119 | 120 | "should return false": function() { 121 | refute(this.model.set({ 122 | age: 0 123 | }, {validate: true})); 124 | }, 125 | 126 | "should not update the model": function() { 127 | refute.defined(this.model.get('age')); 128 | }, 129 | 130 | "model should be invalid": function() { 131 | refute(this.model.isValid()); 132 | } 133 | }, 134 | 135 | "two valid values": { 136 | setUp: function() { 137 | this.model.set({ 138 | age: 1, 139 | name: 'hello' 140 | }, {validate: true}); 141 | }, 142 | 143 | "elements should not have invalid class": function() { 144 | refute(this.age.hasClass('invalid')); 145 | refute(this.name.hasClass('invalid')); 146 | }, 147 | 148 | "model should be valid": function() { 149 | assert(this.model.isValid()); 150 | } 151 | }, 152 | 153 | "two invalid values": { 154 | setUp: function() { 155 | this.model.set({ 156 | age: 0, 157 | name: '' 158 | }, {validate: true}); 159 | }, 160 | 161 | 162 | "elements should have invalid class": function() { 163 | assert(this.age.hasClass('invalid')); 164 | assert(this.name.hasClass('invalid')); 165 | }, 166 | 167 | "model should be invalid": function() { 168 | refute(this.model.isValid()); 169 | } 170 | }, 171 | 172 | "first value invalid and second value valid": { 173 | setUp: function() { 174 | this.result = this.model.set({ 175 | age: 1, 176 | name: '' 177 | }, {validate: true}); 178 | }, 179 | 180 | "model is not updated": function() { 181 | refute(this.result); 182 | }, 183 | 184 | "element should not have invalid class": function() { 185 | refute(this.age.hasClass('invalid')); 186 | }, 187 | 188 | "element should have invalid class": function() { 189 | assert(this.name.hasClass('invalid')); 190 | }, 191 | 192 | "model should be invalid": function() { 193 | refute(this.model.isValid()); 194 | } 195 | }, 196 | 197 | "first value valid and second value invalid": { 198 | setUp: function() { 199 | this.result = this.model.set({ 200 | age: 0, 201 | name: 'name' 202 | }, {validate: true}); 203 | }, 204 | 205 | "model is not updated": function() { 206 | refute(this.result); 207 | }, 208 | 209 | "element should not have invalid class": function() { 210 | refute(this.name.hasClass('invalid')); 211 | }, 212 | 213 | "element should have invalid class": function() { 214 | assert(this.age.hasClass('invalid')); 215 | }, 216 | 217 | "model should be invalid": function() { 218 | refute(this.model.isValid()); 219 | } 220 | }, 221 | 222 | "one value at a time correctly marks the model as either valid or invalid": function() { 223 | refute(this.model.isValid()); 224 | 225 | this.model.set({ 226 | age: 0 227 | }, {validate: true}); 228 | refute(this.model.isValid()); 229 | 230 | this.model.set({ 231 | age: 1 232 | }, {validate: true}); 233 | refute(this.model.isValid()); 234 | 235 | this.model.set({ 236 | name: 'hello' 237 | }, {validate: true}); 238 | assert(this.model.isValid()); 239 | 240 | this.model.set({ 241 | age: 0 242 | }, {validate: true}); 243 | refute(this.model.isValid()); 244 | } 245 | }, 246 | 247 | "and validate is explicitly called with no parameters": { 248 | setUp: function() { 249 | this.invalid = this.spy(); 250 | this.valid = this.spy(); 251 | this.model.validation = { 252 | age: { 253 | min: 1, 254 | msg: 'error' 255 | }, 256 | name: { 257 | required: true, 258 | msg: 'error' 259 | } 260 | }; 261 | Backbone.Validation.bind(this.view, { 262 | valid: this.valid, 263 | invalid: this.invalid 264 | }); 265 | }, 266 | 267 | "all attributes on the model is validated when nothing has been set": function(){ 268 | this.model.validate(); 269 | 270 | assert.calledWith(this.invalid, this.view, 'age', 'error'); 271 | assert.calledWith(this.invalid, this.view, 'name', 'error'); 272 | }, 273 | 274 | "all attributes on the model is validated when one property has been set without validating": function(){ 275 | this.model.set({age: 1}); 276 | 277 | this.model.validate(); 278 | 279 | assert.calledWith(this.valid, this.view, 'age'); 280 | assert.calledWith(this.invalid, this.view, 'name', 'error'); 281 | }, 282 | 283 | "all attributes on the model is validated when two properties has been set without validating": function(){ 284 | this.model.set({age: 1, name: 'name'}); 285 | 286 | this.model.validate(); 287 | 288 | assert.calledWith(this.valid, this.view, 'age'); 289 | assert.calledWith(this.valid, this.view, 'name'); 290 | }, 291 | 292 | "callbacks are not called for unvalidated attributes": function(){ 293 | 294 | this.model.set({age: 1, name: 'name', someProp: 'some value'}); 295 | 296 | this.model.validate(); 297 | 298 | assert.calledWith(this.valid, this.view, 'age'); 299 | assert.calledWith(this.valid, this.view, 'name'); 300 | refute.calledWith(this.valid, this.view, 'someProp'); 301 | } 302 | } 303 | }, 304 | 305 | "when bound to model with three validators on one attribute": { 306 | setUp: function() { 307 | this.Model = Backbone.Model.extend({ 308 | validation: { 309 | postalCode: { 310 | minLength: 2, 311 | pattern: 'digits', 312 | maxLength: 4 313 | } 314 | } 315 | }); 316 | 317 | this.model = new this.Model(); 318 | this.view.model = this.model; 319 | 320 | Backbone.Validation.bind(this.view); 321 | }, 322 | 323 | "and violating the first validator the model is invalid": function (){ 324 | this.model.set({postalCode: '1'}, {validate: true}); 325 | 326 | refute(this.model.isValid()); 327 | }, 328 | 329 | "and violating the second validator the model is invalid": function (){ 330 | this.model.set({postalCode: 'ab'}, {validate: true}); 331 | 332 | refute(this.model.isValid()); 333 | }, 334 | 335 | "and violating the last validator the model is invalid": function (){ 336 | this.model.set({postalCode: '12345'}, {validate: true}); 337 | 338 | refute(this.model.isValid()); 339 | }, 340 | 341 | "and conforming to all validators the model is valid": function (){ 342 | this.model.set({postalCode: '123'}, {validate: true}); 343 | 344 | assert(this.model.isValid()); 345 | } 346 | }, 347 | 348 | "when bound to model with to dependent attribute validations": { 349 | setUp: function() { 350 | var View = Backbone.View.extend({ 351 | render: function() { 352 | var html = $(''); 353 | this.$el.append(html); 354 | 355 | Backbone.Validation.bind(this); 356 | } 357 | }); 358 | var Model = Backbone.Model.extend({ 359 | validation: { 360 | one: function(val, attr, computed) { 361 | if(val < computed.two) { 362 | return 'error'; 363 | } 364 | }, 365 | two: function(val, attr, computed) { 366 | if(val > computed.one) { 367 | return 'return'; 368 | } 369 | } 370 | } 371 | }); 372 | 373 | 374 | this.model = new Model(); 375 | this.view = new View({ 376 | model: this.model 377 | }); 378 | 379 | this.view.render(); 380 | this.one = $(this.view.$('[name~=one]')); 381 | this.two = $(this.view.$('[name~=two]')); 382 | }, 383 | 384 | tearDown: function() { 385 | this.view.remove(); 386 | }, 387 | 388 | "when setting invalid value on second input": { 389 | setUp: function() { 390 | this.model.set({one:1}, {validate: true}); 391 | this.model.set({two:2}, {validate: true}); 392 | }, 393 | 394 | "first input is valid": function() { 395 | assert(this.one.hasClass('invalid')); 396 | }, 397 | 398 | "second input is invalid": function() { 399 | assert(this.two.hasClass('invalid')); 400 | } 401 | }, 402 | 403 | "when setting invalid value on second input and changing first": { 404 | setUp: function() { 405 | this.model.set({one:1}, {validate: true}); 406 | this.model.set({two:2}, {validate: true}); 407 | this.model.set({one:2}, {validate: true}); 408 | }, 409 | 410 | "first input is valid": function() { 411 | refute(this.one.hasClass('invalid')); 412 | }, 413 | 414 | "second input is valid": function() { 415 | refute(this.two.hasClass('invalid')); 416 | } 417 | } 418 | }, 419 | 420 | "when bound to model with custom toJSON": { 421 | setUp: function() { 422 | this.model.toJSON = function() { 423 | return { 424 | 'person': { 425 | 'age': this.attributes.age, 426 | 'name': this.attributes.name 427 | } 428 | }; 429 | }; 430 | 431 | Backbone.Validation.bind(this.view); 432 | }, 433 | 434 | "and conforming to all validators the model is valid": function (){ 435 | this.model.set({age: 12}, {validate: true}); 436 | this.model.set({name: 'Jack'}, {validate: true}); 437 | 438 | this.model.validate(); 439 | assert(this.model.isValid()); 440 | } 441 | } 442 | }); 443 | -------------------------------------------------------------------------------- /src/backbone-validation.js: -------------------------------------------------------------------------------- 1 | Backbone.Validation = (function(_){ 2 | 'use strict'; 3 | 4 | // Default options 5 | // --------------- 6 | 7 | var defaultOptions = { 8 | forceUpdate: false, 9 | selector: 'name', 10 | labelFormatter: 'sentenceCase', 11 | valid: Function.prototype, 12 | invalid: Function.prototype 13 | }; 14 | 15 | 16 | // Helper functions 17 | // ---------------- 18 | 19 | // Formatting functions used for formatting error messages 20 | var formatFunctions = { 21 | // Uses the configured label formatter to format the attribute name 22 | // to make it more readable for the user 23 | formatLabel: function(attrName, model) { 24 | return defaultLabelFormatters[defaultOptions.labelFormatter](attrName, model); 25 | }, 26 | 27 | // Replaces nummeric placeholders like {0} in a string with arguments 28 | // passed to the function 29 | format: function() { 30 | var args = Array.prototype.slice.call(arguments), 31 | text = args.shift(); 32 | return text.replace(/\{(\d+)\}/g, function(match, number) { 33 | return typeof args[number] !== 'undefined' ? args[number] : match; 34 | }); 35 | } 36 | }; 37 | 38 | // Flattens an object 39 | // eg: 40 | // 41 | // var o = { 42 | // owner: { 43 | // name: 'Backbone', 44 | // address: { 45 | // street: 'Street', 46 | // zip: 1234 47 | // } 48 | // } 49 | // }; 50 | // 51 | // becomes: 52 | // 53 | // var o = { 54 | // 'owner': { 55 | // name: 'Backbone', 56 | // address: { 57 | // street: 'Street', 58 | // zip: 1234 59 | // } 60 | // }, 61 | // 'owner.name': 'Backbone', 62 | // 'owner.address': { 63 | // street: 'Street', 64 | // zip: 1234 65 | // }, 66 | // 'owner.address.street': 'Street', 67 | // 'owner.address.zip': 1234 68 | // }; 69 | // This may seem redundant, but it allows for maximum flexibility 70 | // in validation rules. 71 | var flatten = function (obj, into, prefix) { 72 | into = into || {}; 73 | prefix = prefix || ''; 74 | 75 | _.each(obj, function(val, key) { 76 | if(obj.hasOwnProperty(key)) { 77 | if (!!val && _.isArray(val)) { 78 | _.forEach(val, function(v, k) { 79 | flatten(v, into, prefix + key + '.' + k + '.'); 80 | into[prefix + key + '.' + k] = v; 81 | }); 82 | } else if (!!val && typeof val === 'object' && val.constructor === Object) { 83 | flatten(val, into, prefix + key + '.'); 84 | } 85 | 86 | // Register the current level object as well 87 | into[prefix + key] = val; 88 | } 89 | }); 90 | 91 | return into; 92 | }; 93 | 94 | // Validation 95 | // ---------- 96 | 97 | var Validation = (function(){ 98 | 99 | // Returns an object with undefined properties for all 100 | // attributes on the model that has defined one or more 101 | // validation rules. 102 | var getValidatedAttrs = function(model, attrs) { 103 | attrs = attrs || _.keys(_.result(model, 'validation') || {}); 104 | return _.reduce(attrs, function(memo, key) { 105 | memo[key] = void 0; 106 | return memo; 107 | }, {}); 108 | }; 109 | 110 | // Returns an array with attributes passed through options 111 | var getOptionsAttrs = function(options, view) { 112 | var attrs = options.attributes; 113 | if (_.isFunction(attrs)) { 114 | attrs = attrs(view); 115 | } else if (_.isString(attrs) && (_.isFunction(defaultAttributeLoaders[attrs]))) { 116 | attrs = defaultAttributeLoaders[attrs](view); 117 | } 118 | if (_.isArray(attrs)) { 119 | return attrs; 120 | } 121 | }; 122 | 123 | 124 | // Looks on the model for validations for a specified 125 | // attribute. Returns an array of any validators defined, 126 | // or an empty array if none is defined. 127 | var getValidators = function(model, attr) { 128 | var attrValidationSet = model.validation ? _.result(model, 'validation')[attr] || {} : {}; 129 | 130 | // If the validator is a function or a string, wrap it in a function validator 131 | if (_.isFunction(attrValidationSet) || _.isString(attrValidationSet)) { 132 | attrValidationSet = { 133 | fn: attrValidationSet 134 | }; 135 | } 136 | 137 | // Stick the validator object into an array 138 | if(!_.isArray(attrValidationSet)) { 139 | attrValidationSet = [attrValidationSet]; 140 | } 141 | 142 | // Reduces the array of validators into a new array with objects 143 | // with a validation method to call, the value to validate against 144 | // and the specified error message, if any 145 | return _.reduce(attrValidationSet, function(memo, attrValidation) { 146 | _.each(_.without(_.keys(attrValidation), 'msg'), function(validator) { 147 | memo.push({ 148 | fn: defaultValidators[validator], 149 | val: attrValidation[validator], 150 | msg: attrValidation.msg 151 | }); 152 | }); 153 | return memo; 154 | }, []); 155 | }; 156 | 157 | // Validates an attribute against all validators defined 158 | // for that attribute. If one or more errors are found, 159 | // the first error message is returned. 160 | // If the attribute is valid, an empty string is returned. 161 | var validateAttr = function(model, attr, value, computed) { 162 | // Reduces the array of validators to an error message by 163 | // applying all the validators and returning the first error 164 | // message, if any. 165 | return _.reduce(getValidators(model, attr), function(memo, validator){ 166 | // Pass the format functions plus the default 167 | // validators as the context to the validator 168 | var ctx = _.extend({}, formatFunctions, defaultValidators), 169 | result = validator.fn.call(ctx, value, attr, validator.val, model, computed); 170 | 171 | if(result === false || memo === false) { 172 | return false; 173 | } 174 | if (result && !memo) { 175 | return _.result(validator, 'msg') || result; 176 | } 177 | return memo; 178 | }, ''); 179 | }; 180 | 181 | // Loops through the model's attributes and validates the specified attrs. 182 | // Returns and object containing names of invalid attributes 183 | // as well as error messages. 184 | var validateModel = function(model, attrs, validatedAttrs) { 185 | var error, 186 | invalidAttrs = {}, 187 | isValid = true, 188 | computed = _.clone(attrs); 189 | 190 | _.each(validatedAttrs, function(val, attr) { 191 | error = validateAttr(model, attr, val, computed); 192 | if (error) { 193 | invalidAttrs[attr] = error; 194 | isValid = false; 195 | } 196 | }); 197 | 198 | return { 199 | invalidAttrs: invalidAttrs, 200 | isValid: isValid 201 | }; 202 | }; 203 | 204 | // Contains the methods that are mixed in on the model when binding 205 | var mixin = function(view, options) { 206 | return { 207 | 208 | // Check whether or not a value, or a hash of values 209 | // passes validation without updating the model 210 | preValidate: function(attr, value) { 211 | var self = this, 212 | result = {}, 213 | error; 214 | 215 | if(_.isObject(attr)){ 216 | _.each(attr, function(value, key) { 217 | error = self.preValidate(key, value); 218 | if(error){ 219 | result[key] = error; 220 | } 221 | }); 222 | 223 | return _.isEmpty(result) ? undefined : result; 224 | } 225 | else { 226 | return validateAttr(this, attr, value, _.extend({}, this.attributes)); 227 | } 228 | }, 229 | 230 | // Check to see if an attribute, an array of attributes or the 231 | // entire model is valid. Passing true will force a validation 232 | // of the model. 233 | isValid: function(option) { 234 | var flattened, attrs, error, invalidAttrs; 235 | 236 | option = option || getOptionsAttrs(options, view); 237 | 238 | if(_.isString(option)){ 239 | attrs = [option]; 240 | } else if(_.isArray(option)) { 241 | attrs = option; 242 | } 243 | if (attrs) { 244 | flattened = flatten(this.attributes); 245 | //Loop through all associated views 246 | _.each(this.associatedViews, function(view) { 247 | _.each(attrs, function (attr) { 248 | error = validateAttr(this, attr, flattened[attr], _.extend({}, this.attributes)); 249 | if (error) { 250 | options.invalid(view, attr, error, options.selector); 251 | invalidAttrs = invalidAttrs || {}; 252 | invalidAttrs[attr] = error; 253 | } else { 254 | options.valid(view, attr, options.selector); 255 | } 256 | }, this); 257 | }, this); 258 | } 259 | 260 | if(option === true) { 261 | invalidAttrs = this.validate(); 262 | } 263 | if (invalidAttrs) { 264 | this.trigger('invalid', this, invalidAttrs, {validationError: invalidAttrs}); 265 | } 266 | return attrs ? !invalidAttrs : this.validation ? this._isValid : true; 267 | }, 268 | 269 | // This is called by Backbone when it needs to perform validation. 270 | // You can call it manually without any parameters to validate the 271 | // entire model. 272 | validate: function(attrs, setOptions){ 273 | var model = this, 274 | validateAll = !attrs, 275 | opt = _.extend({}, options, setOptions), 276 | validatedAttrs = getValidatedAttrs(model, getOptionsAttrs(options, view)), 277 | allAttrs = _.extend({}, validatedAttrs, model.attributes, attrs), 278 | flattened = flatten(allAttrs), 279 | changedAttrs = attrs ? flatten(attrs) : flattened, 280 | result = validateModel(model, allAttrs, _.pick(flattened, _.keys(validatedAttrs))); 281 | 282 | model._isValid = result.isValid; 283 | 284 | //After validation is performed, loop through all associated views 285 | _.each(model.associatedViews, function(view){ 286 | 287 | // After validation is performed, loop through all validated and changed attributes 288 | // and call the valid and invalid callbacks so the view is updated. 289 | _.each(validatedAttrs, function(val, attr){ 290 | var invalid = result.invalidAttrs.hasOwnProperty(attr), 291 | changed = changedAttrs.hasOwnProperty(attr); 292 | 293 | if(!invalid){ 294 | opt.valid(view, attr, opt.selector); 295 | } 296 | if(invalid && (changed || validateAll)){ 297 | opt.invalid(view, attr, result.invalidAttrs[attr], opt.selector); 298 | } 299 | }); 300 | }); 301 | 302 | // Trigger validated events. 303 | // Need to defer this so the model is actually updated before 304 | // the event is triggered. 305 | _.defer(function() { 306 | model.trigger('validated', model._isValid, model, result.invalidAttrs); 307 | model.trigger('validated:' + (model._isValid ? 'valid' : 'invalid'), model, result.invalidAttrs); 308 | }); 309 | 310 | // Return any error messages to Backbone, unless the forceUpdate flag is set. 311 | // Then we do not return anything and fools Backbone to believe the validation was 312 | // a success. That way Backbone will update the model regardless. 313 | if (!opt.forceUpdate && _.intersection(_.keys(result.invalidAttrs), _.keys(changedAttrs)).length > 0) { 314 | return result.invalidAttrs; 315 | } 316 | } 317 | }; 318 | }; 319 | 320 | // Helper to mix in validation on a model. Stores the view in the associated views array. 321 | var bindModel = function(view, model, options) { 322 | if (model.associatedViews) { 323 | model.associatedViews.push(view); 324 | } else { 325 | model.associatedViews = [view]; 326 | } 327 | _.extend(model, mixin(view, options)); 328 | }; 329 | 330 | // Removes view from associated views of the model or the methods 331 | // added to a model if no view or single view provided 332 | var unbindModel = function(model, view) { 333 | if (view && model.associatedViews && model.associatedViews.length > 1){ 334 | model.associatedViews = _.without(model.associatedViews, view); 335 | } else { 336 | delete model.validate; 337 | delete model.preValidate; 338 | delete model.isValid; 339 | delete model.associatedViews; 340 | } 341 | }; 342 | 343 | // Mix in validation on a model whenever a model is 344 | // added to a collection 345 | var collectionAdd = function(model) { 346 | bindModel(this.view, model, this.options); 347 | }; 348 | 349 | // Remove validation from a model whenever a model is 350 | // removed from a collection 351 | var collectionRemove = function(model) { 352 | unbindModel(model); 353 | }; 354 | 355 | // Returns the public methods on Backbone.Validation 356 | return { 357 | 358 | // Current version of the library 359 | version: '0.11.3', 360 | 361 | // Called to configure the default options 362 | configure: function(options) { 363 | _.extend(defaultOptions, options); 364 | }, 365 | 366 | // Hooks up validation on a view with a model 367 | // or collection 368 | bind: function(view, options) { 369 | options = _.extend({}, defaultOptions, defaultCallbacks, options); 370 | 371 | var model = options.model || view.model, 372 | collection = options.collection || view.collection; 373 | 374 | if(typeof model === 'undefined' && typeof collection === 'undefined'){ 375 | throw 'Before you execute the binding your view must have a model or a collection.\n' + 376 | 'See http://thedersen.com/projects/backbone-validation/#using-form-model-validation for more information.'; 377 | } 378 | 379 | if(model) { 380 | bindModel(view, model, options); 381 | } 382 | else if(collection) { 383 | collection.each(function(model){ 384 | bindModel(view, model, options); 385 | }); 386 | collection.bind('add', collectionAdd, {view: view, options: options}); 387 | collection.bind('remove', collectionRemove); 388 | } 389 | }, 390 | 391 | // Removes validation from a view with a model 392 | // or collection 393 | unbind: function(view, options) { 394 | options = _.extend({}, options); 395 | var model = options.model || view.model, 396 | collection = options.collection || view.collection; 397 | 398 | if(model) { 399 | unbindModel(model, view); 400 | } 401 | else if(collection) { 402 | collection.each(function(model){ 403 | unbindModel(model, view); 404 | }); 405 | collection.unbind('add', collectionAdd); 406 | collection.unbind('remove', collectionRemove); 407 | } 408 | }, 409 | 410 | // Used to extend the Backbone.Model.prototype 411 | // with validation 412 | mixin: mixin(null, defaultOptions) 413 | }; 414 | }()); 415 | 416 | 417 | // Callbacks 418 | // --------- 419 | 420 | var defaultCallbacks = Validation.callbacks = { 421 | 422 | // Gets called when a previously invalid field in the 423 | // view becomes valid. Removes any error message. 424 | // Should be overridden with custom functionality. 425 | valid: function(view, attr, selector) { 426 | view.$('[' + selector + '~="' + attr + '"]') 427 | .removeClass('invalid') 428 | .removeAttr('data-error'); 429 | }, 430 | 431 | // Gets called when a field in the view becomes invalid. 432 | // Adds a error message. 433 | // Should be overridden with custom functionality. 434 | invalid: function(view, attr, error, selector) { 435 | view.$('[' + selector + '~="' + attr + '"]') 436 | .addClass('invalid') 437 | .attr('data-error', error); 438 | } 439 | }; 440 | 441 | 442 | // Patterns 443 | // -------- 444 | 445 | var defaultPatterns = Validation.patterns = { 446 | // Matches any digit(s) (i.e. 0-9) 447 | digits: /^\d+$/, 448 | 449 | // Matches any number (e.g. 100.000) 450 | number: /^-?(?:\d+|\d{1,3}(?:,\d{3})+)(?:\.\d+)?$/, 451 | 452 | // Matches a valid email address (e.g. mail@example.com) 453 | email: /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i, 454 | 455 | // Mathes any valid url (e.g. http://www.xample.com) 456 | url: /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i 457 | }; 458 | 459 | 460 | // Error messages 461 | // -------------- 462 | 463 | // Error message for the build in validators. 464 | // {x} gets swapped out with arguments form the validator. 465 | var defaultMessages = Validation.messages = { 466 | required: '{0} is required', 467 | acceptance: '{0} must be accepted', 468 | min: '{0} must be greater than or equal to {1}', 469 | max: '{0} must be less than or equal to {1}', 470 | range: '{0} must be between {1} and {2}', 471 | length: '{0} must be {1} characters', 472 | minLength: '{0} must be at least {1} characters', 473 | maxLength: '{0} must be at most {1} characters', 474 | rangeLength: '{0} must be between {1} and {2} characters', 475 | oneOf: '{0} must be one of: {1}', 476 | equalTo: '{0} must be the same as {1}', 477 | digits: '{0} must only contain digits', 478 | number: '{0} must be a number', 479 | email: '{0} must be a valid email', 480 | url: '{0} must be a valid url', 481 | inlinePattern: '{0} is invalid' 482 | }; 483 | 484 | // Label formatters 485 | // ---------------- 486 | 487 | // Label formatters are used to convert the attribute name 488 | // to a more human friendly label when using the built in 489 | // error messages. 490 | // Configure which one to use with a call to 491 | // 492 | // Backbone.Validation.configure({ 493 | // labelFormatter: 'label' 494 | // }); 495 | var defaultLabelFormatters = Validation.labelFormatters = { 496 | 497 | // Returns the attribute name with applying any formatting 498 | none: function(attrName) { 499 | return attrName; 500 | }, 501 | 502 | // Converts attributeName or attribute_name to Attribute name 503 | sentenceCase: function(attrName) { 504 | return attrName.replace(/(?:^\w|[A-Z]|\b\w)/g, function(match, index) { 505 | return index === 0 ? match.toUpperCase() : ' ' + match.toLowerCase(); 506 | }).replace(/_/g, ' '); 507 | }, 508 | 509 | // Looks for a label configured on the model and returns it 510 | // 511 | // var Model = Backbone.Model.extend({ 512 | // validation: { 513 | // someAttribute: { 514 | // required: true 515 | // } 516 | // }, 517 | // 518 | // labels: { 519 | // someAttribute: 'Custom label' 520 | // } 521 | // }); 522 | label: function(attrName, model) { 523 | return (model.labels && model.labels[attrName]) || defaultLabelFormatters.sentenceCase(attrName, model); 524 | } 525 | }; 526 | 527 | // AttributeLoaders 528 | 529 | var defaultAttributeLoaders = Validation.attributeLoaders = { 530 | inputNames: function (view) { 531 | var attrs = []; 532 | if (view) { 533 | view.$('form [name]').each(function () { 534 | if (/^(?:input|select|textarea)$/i.test(this.nodeName) && this.name && 535 | this.type !== 'submit' && attrs.indexOf(this.name) === -1) { 536 | attrs.push(this.name); 537 | } 538 | }); 539 | } 540 | return attrs; 541 | } 542 | }; 543 | 544 | 545 | // Built in validators 546 | // ------------------- 547 | 548 | var defaultValidators = Validation.validators = (function(){ 549 | // Use native trim when defined 550 | var trim = String.prototype.trim ? 551 | function(text) { 552 | return text === null ? '' : String.prototype.trim.call(text); 553 | } : 554 | function(text) { 555 | var trimLeft = /^\s+/, 556 | trimRight = /\s+$/; 557 | 558 | return text === null ? '' : text.toString().replace(trimLeft, '').replace(trimRight, ''); 559 | }; 560 | 561 | // Determines whether or not a value is a number 562 | var isNumber = function(value){ 563 | return _.isNumber(value) || (_.isString(value) && value.match(defaultPatterns.number)); 564 | }; 565 | 566 | // Determines whether or not a value is empty 567 | var hasValue = function(value) { 568 | return !(_.isNull(value) || _.isUndefined(value) || (_.isString(value) && trim(value) === '') || (_.isArray(value) && _.isEmpty(value))); 569 | }; 570 | 571 | return { 572 | // Function validator 573 | // Lets you implement a custom function used for validation 574 | fn: function(value, attr, fn, model, computed) { 575 | if(_.isString(fn)){ 576 | fn = model[fn]; 577 | } 578 | return fn.call(model, value, attr, computed); 579 | }, 580 | 581 | // Required validator 582 | // Validates if the attribute is required or not 583 | // This can be specified as either a boolean value or a function that returns a boolean value 584 | required: function(value, attr, required, model, computed) { 585 | var isRequired = _.isFunction(required) ? required.call(model, value, attr, computed) : required; 586 | if(!isRequired && !hasValue(value)) { 587 | return false; // overrides all other validators 588 | } 589 | if (isRequired && !hasValue(value)) { 590 | return this.format(defaultMessages.required, this.formatLabel(attr, model)); 591 | } 592 | }, 593 | 594 | // Acceptance validator 595 | // Validates that something has to be accepted, e.g. terms of use 596 | // `true` or 'true' are valid 597 | acceptance: function(value, attr, accept, model) { 598 | if(value !== 'true' && (!_.isBoolean(value) || value === false)) { 599 | return this.format(defaultMessages.acceptance, this.formatLabel(attr, model)); 600 | } 601 | }, 602 | 603 | // Min validator 604 | // Validates that the value has to be a number and equal to or greater than 605 | // the min value specified 606 | min: function(value, attr, minValue, model) { 607 | if (!isNumber(value) || value < minValue) { 608 | return this.format(defaultMessages.min, this.formatLabel(attr, model), minValue); 609 | } 610 | }, 611 | 612 | // Max validator 613 | // Validates that the value has to be a number and equal to or less than 614 | // the max value specified 615 | max: function(value, attr, maxValue, model) { 616 | if (!isNumber(value) || value > maxValue) { 617 | return this.format(defaultMessages.max, this.formatLabel(attr, model), maxValue); 618 | } 619 | }, 620 | 621 | // Range validator 622 | // Validates that the value has to be a number and equal to or between 623 | // the two numbers specified 624 | range: function(value, attr, range, model) { 625 | if(!isNumber(value) || value < range[0] || value > range[1]) { 626 | return this.format(defaultMessages.range, this.formatLabel(attr, model), range[0], range[1]); 627 | } 628 | }, 629 | 630 | // Length validator 631 | // Validates that the value has to be a string with length equal to 632 | // the length value specified 633 | length: function(value, attr, length, model) { 634 | if (!_.isString(value) || value.length !== length) { 635 | return this.format(defaultMessages.length, this.formatLabel(attr, model), length); 636 | } 637 | }, 638 | 639 | // Min length validator 640 | // Validates that the value has to be a string with length equal to or greater than 641 | // the min length value specified 642 | minLength: function(value, attr, minLength, model) { 643 | if (!_.isString(value) || value.length < minLength) { 644 | return this.format(defaultMessages.minLength, this.formatLabel(attr, model), minLength); 645 | } 646 | }, 647 | 648 | // Max length validator 649 | // Validates that the value has to be a string with length equal to or less than 650 | // the max length value specified 651 | maxLength: function(value, attr, maxLength, model) { 652 | if (!_.isString(value) || value.length > maxLength) { 653 | return this.format(defaultMessages.maxLength, this.formatLabel(attr, model), maxLength); 654 | } 655 | }, 656 | 657 | // Range length validator 658 | // Validates that the value has to be a string and equal to or between 659 | // the two numbers specified 660 | rangeLength: function(value, attr, range, model) { 661 | if (!_.isString(value) || value.length < range[0] || value.length > range[1]) { 662 | return this.format(defaultMessages.rangeLength, this.formatLabel(attr, model), range[0], range[1]); 663 | } 664 | }, 665 | 666 | // One of validator 667 | // Validates that the value has to be equal to one of the elements in 668 | // the specified array. Case sensitive matching 669 | oneOf: function(value, attr, values, model) { 670 | if(!_.include(values, value)){ 671 | return this.format(defaultMessages.oneOf, this.formatLabel(attr, model), values.join(', ')); 672 | } 673 | }, 674 | 675 | // Equal to validator 676 | // Validates that the value has to be equal to the value of the attribute 677 | // with the name specified 678 | equalTo: function(value, attr, equalTo, model, computed) { 679 | if(value !== computed[equalTo]) { 680 | return this.format(defaultMessages.equalTo, this.formatLabel(attr, model), this.formatLabel(equalTo, model)); 681 | } 682 | }, 683 | 684 | // Pattern validator 685 | // Validates that the value has to match the pattern specified. 686 | // Can be a regular expression or the name of one of the built in patterns 687 | pattern: function(value, attr, pattern, model) { 688 | if (!hasValue(value) || !value.toString().match(defaultPatterns[pattern] || pattern)) { 689 | return this.format(defaultMessages[pattern] || defaultMessages.inlinePattern, this.formatLabel(attr, model), pattern); 690 | } 691 | } 692 | }; 693 | }()); 694 | 695 | // Set the correct context for all validators 696 | // when used from within a method validator 697 | _.each(defaultValidators, function(validator, key){ 698 | defaultValidators[key] = _.bind(defaultValidators[key], _.extend({}, formatFunctions, defaultValidators)); 699 | }); 700 | 701 | return Validation; 702 | }(_)); 703 | --------------------------------------------------------------------------------