├── .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 |
26 | -
27 | Jump To …
28 | +
29 |
30 |
31 | <% for (var i=0, l=sources.length; i
32 | <% var source = sources[i]; %>
33 |
34 | <%= path.basename(source) %>
35 |
36 | <% } %>
37 |
38 |
39 |
40 | <% } %>
41 |
42 | <% if (!hasTitle) { %>
43 | -
44 |
45 |
<%= title %>
46 |
47 |
48 | <% } %>
49 | <% for (var i=0, l=sections.length; i
50 | <% var section = sections[i]; %>
51 | -
52 |
53 | <% heading = section.docsHtml.match(/^\s*<(h\d)>/) %>
54 |
57 | <%= section.docsHtml %>
58 |
59 | <% if (section.codeText.replace(/\s/gm, '') != '') { %>
60 | <%= section.codeHtml %>
61 | <% } %>
62 |
63 | <% } %>
64 |
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 |
--------------------------------------------------------------------------------