From 4268e6057e250440b64a65ee4bdb0451b7e4e718 Mon Sep 17 00:00:00 2001 From: Matt Spence Date: Tue, 22 Feb 2011 22:46:01 +0000 Subject: [PATCH] Tie to html elements, success and failure callbacks and documentation Added the ability to tie model instance attributes and non form html elements (includes test). Added ability to define success and failure callback for a tie (includes tests). Added some documentation for Tie, mostly taken from the GitHub Gist here: https://gist.github.com/733106 Signed-off-by: Matt Spence --- model/validations/qunit/validations_test.js | 43 ++++++ tie/test/qunit/qunit.js | 59 +++++++- tie/tie.js | 148 +++++++++++++++++++- 3 files changed, 240 insertions(+), 10 deletions(-) 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(){