diff --git a/model/validations/qunit/validations_test.js b/model/validations/qunit/validations_test.js index 0ae64aa2..ae9ecdf1 100644 --- a/model/validations/qunit/validations_test.js +++ b/model/validations/qunit/validations_test.js @@ -8,6 +8,7 @@ module("jquery/model/validations",{ }) test("models can validate, events, callbacks", 11,function(){ + Person.validate("age", {message : "it's a date type"},function(val){ return ! ( this.date instanceof Date ) }) @@ -61,6 +62,23 @@ test("validatesFormatOf", function(){ }); test("validatesInclusionOf", function(){ + + Person.validateInclusionOf("thing",["value1","value2","value3"]); + + ok(!new Person({thing: "value2"}).errors(),"no errors"); + + var errors = new Person({thing: "foobar"}).errors(); + + ok(errors, "there are errors") + equals(errors.thing.length,1,"one error on thing"); + + equals(errors.thing[0],"is not a valid option (perhaps out of range)","basic message"); + + Person.validateInclusionOf("otherThing",["value1","value2","value3"],{message: "not in array"}) + + var errors2 = new Person({thing: "1-2", otherThing: "a"}).errors(); + + equals(errors2.otherThing[0],"not in array", "can supply a custom message") }) @@ -98,9 +116,34 @@ test("validatesPresenceOf", function(){ errors = task.errors();; ok(!errors, "no errors "+typeof errors); + }) test("validatesRangeOf", function(){ + + jQuery.Model.extend("ValidatesRangeOfMock1",{},{}); + + ValidatesRangeOfMock1.validateRangeOf("thing",2,3); + + ok(!new ValidatesRangeOfMock1({thing: 2.5}).errors(),"no errors"); + + var errors = new ValidatesRangeOfMock1({thing: 4}).errors(); + + ok(errors, "there are errors") + + equals(errors.thing.length,1,"one error on thing"); + + equals(errors.thing[0],"is out of range [2,3]","basic message"); + + jQuery.Model.extend("ValidatesRangeOfMock2",{},{}); + + ValidatesRangeOfMock2.validateRangeOf("otherThing",-100,-10,{message: "not in range"}) + + ok(!new ValidatesRangeOfMock2({otherThing: -50}).errors(),"no errors, with custom message"); + + var errors2 = new ValidatesRangeOfMock2({thing: 2.5, otherThing: 3}).errors(); + + equals(errors2.otherThing[0],"not in range", "can supply a custom message") }) diff --git a/tie/test/qunit/qunit.js b/tie/test/qunit/qunit.js index 2fda153b..fc8715cf 100644 --- a/tie/test/qunit/qunit.js +++ b/tie/test/qunit/qunit.js @@ -1,6 +1,6 @@ steal .plugins("funcunit/qunit", "jquery/tie",'jquery/model') - .then("tie_test").then(function(){ + .then(function(){ module("jquery/tie",{ @@ -37,6 +37,27 @@ steal equals(inp2.val(), "6", "nothing set"); + }); + + test("sets age of standard element on tie", function(){ + + var person1 = new Person({age: 5}); + var inp = $("
").appendTo( $("#qunit-test-area") ); + + inp.tie(person1, 'age'); + + equals(inp.text(), "5", "sets age"); + + var person2 = new Person(); + var inp2 = $("").appendTo( $("#qunit-test-area") ); + inp2.tie(person2, 'age'); + equals(inp2.val(), "", "nothing set"); + + person2.attr("age",6); + + equals(inp2.html(), "6", "nothing set"); + + }); test("removing the controller, removes the tie ", 3, function(){ @@ -93,15 +114,47 @@ steal }); test("input error recovery", function(){ + var person1 = new Person({age: 5}); var inp = $("").appendTo( $("#qunit-test-area") ); + + var error_called = false; - inp.tie(person1, 'age'); + inp.tie(person1, 'age',function() {}, function() { error_called = true; }); inp.val(100).trigger('change'); equals(inp.val(), "5", "input value stays the same"); equals(person1.attr('age'), "5", "persons age stays the same"); - }) + equals(error_called,true, "error function called"); + + }); + + test("input success calls callback", function() { + var person1 = new Person({age: 5}); + var inp = $("").appendTo( $("#qunit-test-area") ); + + var success_called = false; + + inp.tie(person1, 'age',function() {success_called=true}); + + equals(success_called, true, "success called for init"); + + success_called = false; + + person1.attr('age',4); + + equals(success_called, true, "success called for model change"); + + success_called = false; + + inp.val(3).trigger('change'); + + equals(success_called, true, "success called for input change"); + + + + + }); }); \ No newline at end of file diff --git a/tie/tie.js b/tie/tie.js index afefd82b..c53bbdde 100644 --- a/tie/tie.js +++ b/tie/tie.js @@ -2,20 +2,145 @@ steal.plugins('jquery/controller').then(function($){ /** * @core + * @tag core * @class jQuery.Tie - * - * The $.fn.tie plugin binds form elements and controllers with + * @page jquery.tie Tie + * @plugin jquery/tie + * @test jquery/tie/qunit.html + + * The $.fn.tie plugin binds form elements, dom elements and controllers with * models and vice versa. The result is that a change in - * a model will automatically update the form element or controller - * AND a change event on the element will update the model. + * a model will automatically update the form element, dom elemnt, or controller + * AND a change event on the element will update the model. + * + * ## Setup + * + * @codestart + * $.Model("Person") + * + * var person = new Person({age: 5}); + * @codeend + * + * ## Form elements + * + * @codestart + * $('input:first').tie(person,'age'); + * @codeend + * + * When this code is run, it will automatically set the input's value to 5. + * If I do the following ... + * + * @codestart + * person.attr('age',7); + * @codeend + * + * ... It will update the input element. + * + * If I change the input element manually, it will effectively do a: + * + * @codestart + * person.attr('age',$('input:first').val()); + * @codeend + * + * ## Non-form elements + * + * Tie will also call html on elements allowing you to link any html elements. + * + * @codestart + * $('p.age').tie(person,'age'); + * @codeend + * + * This will link all paragraphs with a class of age. Initially it will set the + * text to 7 (as we have already changed the persons age to 7). + * + * If I do the following ... + * + * @codestart + * person.attr('age',2); + * @codeend + * + * ... It will update all the paragraph elements. + * + * ## Controllers * + * For form elements, tie uses $(el).val() to get and set values and listens + * for change events to know when the input element has changed. + * + * For controller, it's basically the same way. Your controller only has to + * do 2 things: * + * 1. implement a val function that take an optional value. + * If a value is provided, it should update the UI appropriately; + * otherwise, it should return the value: * + * @codestart + * $.Controller('Rating',{ + * val : function(value){ + * if(value !== undefined){ + * //update the UI + * }else{ + * //return the value + * } + * } + * }) + * @codeend + * + * 2. When the model should be updated, trigger a change event + * with the new value: + * + * @codestart + * this.element.trigger('change',7); + * @codeend + * + * Here's a slider widget implemented this way: + * https://github.com/jupiterjs/mxui/blob/master/slider/slider.js + * Notice in dropend, it triggers a change with the value of the slider. + * + * You could tie a slider to a person's age like: + * + * @codestart + * $('#slider').mxui_slider().tie(person,'age'); + * @codeend + * + * Reads pretty well doesn't it! + * + * ## Validation * + * Here's how we could setup our model to validate ages: + * + * @codestart + * $.Model.extend("Person",{ + * setAge : function(age, success, error){ + * age = +(age); + * if(isNaN(age) || !isFinite(age) || age < 1 || age > 10){ + * error() + * }else{ + * return age; + * } + * } + * }); + * @codeend + * + * This checks that age is a number between 1 and 10. You could also + * use the validations plugin for this. * + * If setAge made an Ajax request to the server, you would call + * success(finalAge) instead of returning the correct value. + * */ $.Controller.extend("jQuery.Tie",{ - init : function(el, inst, attr, type){ + /** + * @function jQuery.Tie + * @parent jquery.tie + * Initiate a link between an html element or controller and model. + * @param {Object} inst A model instance to bind with. + * @param {String} attr The model attribute to trigger changes on. + * @param {Function} success (optional) A callback function if the element, controller, or model is successfully updated + * @param {Function} failure (optional) A callback function if the element, controller, or model cannot be updated, for example if it fails the models validation. + * @param {Object} type (optional) TODO I'm not entirely clear on what this does, something to do with you being able to give it a different controller to call value on I think. + * + */ + init : function(el, inst, attr, success, failure, type){ // if there's a controller if(!type){ //find the first one that implements val @@ -32,6 +157,8 @@ $.Controller.extend("jQuery.Tie",{ this.type = type; this.attr = attr; this.inst = inst; + this.success = success; + this.failure = failure; this.bind(inst, attr, "attrChanged"); //destroy this controller if the model instance is destroyed @@ -47,12 +174,17 @@ $.Controller.extend("jQuery.Tie",{ this.element[type]("val",value); }else{ - this.element.val(value) + this.element.val(value); + this.element.html(value); } + if (typeof this.success == "function") + this.success(this.element); }, attrChanged : function(inst, ev, val){ if (val !== this.lastValue) { this.setVal(val); + if (typeof this.success == "function") + this.success(this.element); this.lastValue = val; } }, @@ -61,7 +193,8 @@ $.Controller.extend("jQuery.Tie",{ this.element[this.type]("val", val) } else { - this.element.val(val) + this.element.val(val); + this.element.html(val); } }, change : function(el, ev, val){ @@ -73,6 +206,7 @@ $.Controller.extend("jQuery.Tie",{ }, setBack : function(){ + this.failure(this.element); this.setVal(this.lastValue); }, destroy : function(){