Skip to content

Commit

Permalink
Merge commit 'f25c7869c8beedd512610852bd70d1fd616256e3'
Browse files Browse the repository at this point in the history
  • Loading branch information
powmedia committed Oct 31, 2011
2 parents a03c577 + f25c786 commit 0818833
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 23 deletions.
21 changes: 16 additions & 5 deletions README.md
Expand Up @@ -49,6 +49,7 @@ Define a 'schema' attribute on your Backbone models. The schema keys should matc

var User = Backbone.Model.extend({
schema: {
email: { dataType: 'email', validators: ['required', validateEmail] },
start: { type: 'DateTime' },
contact: { type: 'Object', subSchema: {
name: {},
Expand Down Expand Up @@ -124,6 +125,13 @@ For each field definition in the schema you can use the following optional attri
- Defines the text that appears in a form field's <label>
- If not defined, defaults to a formatted version of the camelCased field key. E.g. `firstName` becomes `First Name`. This behaviour can be changed by assigning your own function to Backbone.Form.helpers.keyToTitle.

**`validators`**

- A list of validators, where each validator is one of the following:
- **A string.** This should be the name of a function in `Backbone.forms.validators`. The only provided validator is `"required"`, but you can add any other validators you use regularly here.
- **A regular expression.** This should either be a compiled regular expression, or an object of the form `{'RegExp': 'string-to-make-into-regex'}`. The validator function generated will use `RegExp.test` on the value and return an error message if there is a problem.
- **A function.** This function will be passed the value of the form, and should return a truth-y value if there is an error. This would normally be a string describing the error.


Editor-specific attributes
--------------------------
Expand Down Expand Up @@ -389,14 +397,17 @@ Custom editors can be written. They must extend from Backbone.Form.editors.Base.
- The field schema can be accessed via this.schema. This allows you to pass in custom parameters.


Defaults & Validation
=====================
Initial Data
============

If a form has a model attached to it, the initial values are taken from the model's defaults. Otherwise, you may pass default values using the `schema.data`.

Formal uses the built in Backbone validation and defaults as defined on the model.
Validation
==========

For validation, it will attempt to update the model and if there are validation failures, it will report them back.
Forms provide a `validate` method, which returns a dictionary of errors, or `null`. Validation is determined using the `validators` attribute on the schema (see above).

See the Backbone documentation for more details.
If you model provides a `validate` method, then this will be called when you call `Form.validate`. Forms are also validated when you call `commit`. See the Backbone documentation for more details on model validation.


Known issues
Expand Down
121 changes: 106 additions & 15 deletions src/backbone-forms.js
@@ -1,6 +1,7 @@
;(function() {

var helpers = {};
var validators = {};

/**
* This function is used to transform the key from a schema into the title used in a label.
Expand Down Expand Up @@ -86,9 +87,38 @@
args.push(callback);

fn.apply(context, args);
}

helpers.getValidator = function(validator) {
var isRegExp = _(validator).isRegExp();
if (isRegExp || validator['RegExp']) {
if (!isRegExp) {
validator = new RegExp(validator['RegExp']);
}
return function (value) {
if (!validator.test(value)) {
return 'Value '+value+' does not pass validation against regular expression '+validator;
}
};
} else if (_(validator).isString()) {
if (validators[validator]) {
return validators[validator];
} else {
throw 'Validator "'+validator+'" not found';
}
} else if (_(validator).isFunction()) {
return validator;
} else {
throw 'Could not process validator' + validator;
}
};

validators.required = function (value) {
var exists = (value === 0 || !!value);
if (!exists) {
return 'This field is required';
}
};




var Form = Backbone.View.extend({
Expand Down Expand Up @@ -197,14 +227,43 @@
});
},

/**
* Validate the data
*
* @return {Object} Validation errors
*/
validate: function() {
var fields = this.fields,
model = this.model,
errors = {};

_.each(fields, function(field) {
var error = field.validate();
if (error) {
errors[field.key] = error;
}
});

if (model && model.validate) {
errors._nonFieldErrors = model.validate(form.getValue());
}

return _.isEmpty(errors) ? null : errors;
},

/**
* Update the model with all latest values.
*
* @return {Object} Validation errors
*/
commit: function() {
var fields = this.fields,
errors = {};
var fields = this.fields;

var errors = this.validate();

if (errors) {
return errors;
}

_.each(fields, function(field) {
var error = field.commit();
Expand Down Expand Up @@ -340,6 +399,13 @@
return this;
},

/**
* Validate the value from the editor
*/
validate: function() {
return this.editor.validate();
},

/**
* Update the model with the new value from the editor
*/
Expand Down Expand Up @@ -413,7 +479,8 @@

if (this.value === undefined) this.value = this.defaultValue;

this.schema = options.schema;
this.schema = options.schema || {};
this.validators = options.validators || this.schema.validators;
},

getValue: function() {
Expand All @@ -424,29 +491,52 @@
throw 'Not implemented. Extend and override this method.';
},

/**
* Check the validity of a particular field
*/
validate: function () {
var el = $(this.el),
error = null,
value = this.getValue();

if (this.validators) {
_(this.validators).each(function(validator) {
if (!error) {
error = helpers.getValidator(validator)(value);
}
});
}

if (!error && this.model && this.model.validate) {
var change = {};
change[this.key] = value;
error = this.model.validate(change);
}

if (error) {
el.addClass('bbf-error');
} else {
el.removeClass('bbf-error');
}

return error;
},

/**
* Update the model with the new value from the editor
*
* @return {Error|null} Validation error or null
*/
commit: function() {
var el = $(this.el),
change = {};

var error = null;
var change = {};
change[this.key] = this.getValue();

var error = null
this.model.set(change, {
error: function(model, e) {
error = e;
}
});

if (error)
el.addClass('bbf-error');
else
el.removeClass('bbf-error');

return error;
}

Expand Down Expand Up @@ -916,6 +1006,7 @@
Form.helpers = helpers;
Form.Field = Field;
Form.editors = editors;
Form.validators = validators;
Backbone.Form = Form;

})();
14 changes: 14 additions & 0 deletions test/editors.js
Expand Up @@ -15,6 +15,19 @@ module('Base');

equal(post.get('title'), 'New Title');
});

test('validate()', function() {
var editor = new editors.Text({
key: 'title',
validators: ['required']
});

ok(editor.validate());

editor.setValue('a value');

ok(_(editor.validate()).isUndefined());
});

test('TODO: Test commit() validation failure', function() {

Expand All @@ -23,6 +36,7 @@ module('Base');




module('Text');

(function() {
Expand Down
13 changes: 13 additions & 0 deletions test/form.js
Expand Up @@ -96,6 +96,19 @@ test("'idPrefix' option - Adds prefix to all DOM element IDs", function() {
equal($('#form_title', form.el).length, 1);
});

test("validate() - validates the form and returns an errors object", function () {
var form = new Form({
schema: {
title: {validators: ['required']}
}
}).render();

ok(form.validate() && form.validate().title);

form.setValue({title: 'A valid title'});
equal(form.validate(), null);
});

test("commit() - updates the model with form values", function() {
var post = new Post();

Expand Down
41 changes: 41 additions & 0 deletions test/helpers.js
Expand Up @@ -100,3 +100,44 @@ module('triggerCancellableEvent');
});

})();

module('getValidator');

(function() {

test('Bundled validator', function() {
var validator = Form.helpers.getValidator('required');

equal(validator, Form.validators.required);
});

test('Regular Expressions', function() {
var validator = Form.helpers.getValidator(/hello/);
var validator2 = Form.helpers.getValidator({'RegExp': 'another'});

ok(_(validator('hellooooo')).isUndefined());
ok(validator('bye!'));

ok(validator2('this is a test'));
ok(_(validator2('this is another test')).isUndefined());
});

test('Function', function () {
var myValidator = function () { return; };

var validator = Form.helpers.getValidator(myValidator);

equal(validator, myValidator);
});

test('Unknown', function () {
raises(function() {
Form.helpers.getValidator('unknown validator');
}, /not found/i);
raises(function() {
Form.helpers.getValidator(['this is a list', 'not a validator']);
}, /could not process/i);
});

})();

13 changes: 10 additions & 3 deletions test/index.html
Expand Up @@ -35,10 +35,17 @@ <h2 id="qunit-userAgent"></h2>
<div id="uiTest"></div>
<script>
$(function() {
function validateEmail(str) {
var regex = new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?");

return regex.test(str) ? null : 'Invalid email';
}

var Model = Backbone.Model.extend({
schema: {
tel: { type: 'Text', dataType: 'tel' },
number: { type: 'Number' },
email: { dataType: 'email', validators: ['required', validateEmail] },
tel: { type: 'Text', dataType: 'tel', validators: ['required'] },
number: { type: 'Number', validators: [/[0-9]+(?:\.[0-9]*)?/] },
checkbox: { type: 'Checkbox' },
date: { type: 'Date' },
datetime: { type: 'DateTime' },
Expand All @@ -56,7 +63,7 @@ <h2 id="qunit-userAgent"></h2>
var form = new Backbone.Form({
model: model,
fieldsets: [
['tel', 'number', 'checkbox', 'radio'],
['email', 'tel', 'number', 'checkbox', 'radio'],
{ legend: 'jQuery UI editors', fields: ['date', 'datetime', 'list'] }
]
}).render();
Expand Down

0 comments on commit 0818833

Please sign in to comment.