diff --git a/controller/controller.js b/controller/controller.js index 9da38d5f..6eba59c1 100644 --- a/controller/controller.js +++ b/controller/controller.js @@ -333,6 +333,7 @@ steal('jquery/class', 'jquery/lang/string', 'jquery/event/destroyed', function( * "Controllers.Task" -> $().controllers_task() * */ + setup: function() { // Allow contollers to inherit "defaults" from superclasses as it done in $.Class this._super.apply(this, arguments); diff --git a/controller/view/view.js b/controller/view/view.js index f77254a1..3347b3f1 100644 --- a/controller/view/view.js +++ b/controller/view/view.js @@ -10,7 +10,7 @@ steal('jquery/controller', 'jquery/view').then(function( $ ) { classPartsWithoutPrefix.splice(0, 2); // Remove prefix (usually 2 elements) var classPartsWithoutPrefixSlashes = classPartsWithoutPrefix.join('/'), - hasControllers = (classParts.length > 2) && classParts[1] == 'Controllers', + hasControllers = false, //(classParts.length > 2) && classParts[1] == 'Controllers', path = hasControllers? jQuery.String.underscore(classParts[0]): jQuery.String.underscore(classParts.join("/")), controller_name = classPartsWithoutPrefix.join('/').toLowerCase(), suffix = (typeof view == "string" && /\.[\w\d]+$/.test(view)) ? "" : jQuery.View.ext; @@ -104,11 +104,12 @@ steal('jquery/controller', 'jquery/view').then(function( $ ) { view = jQuery.Controller._calculatePosition(this.Class, view, this.called); //calculate data - data = data || this; + var self = this; + data = data? data : this; //calculate helpers var helpers = calculateHelpers.call(this, myhelpers); - + $.extend(helpers,{view: function(){ return self.view.apply(self, arguments)}}) return jQuery.View(view, data, helpers); //what about controllers in other folders? }; diff --git a/dom/fixture/fixture.js b/dom/fixture/fixture.js index b77ce738..903df0e1 100644 --- a/dom/fixture/fixture.js +++ b/dom/fixture/fixture.js @@ -165,11 +165,12 @@ steal('jquery/dom', } if(id === undefined){ - id = settings.url.replace(/\/(\w+)(\/|$)/g, function(all, num){ - if(num != 'update'){ - id = num; - } - }) + id = settings.url.match(/\d+(?=(\.json))/) && settings.url.match(/\d+(?=(\.json))/)[0] +// id = settings.url.replace(/\/(\w+)(\/|$)/g, function(all, num){ +// if(num != 'update'){ +// id = num; +// } +// }) } if(id === undefined){ // if still not set, guess a random number @@ -524,7 +525,8 @@ steal('jquery/dom', * } * */ - make: function( types, count, make, filter ) { + make: function( types, count, make, filter, preventAutoFiltering ) { + preventAutoFiltering = $.makeArray(preventAutoFiltering) if(typeof types === "string"){ types = [types+"s",types ] } @@ -595,7 +597,8 @@ steal('jquery/dom', for ( var param in settings.data ) { i=0; if ( settings.data[param] !== undefined && // don't do this if the value of the param is null (ignore it) - (param.indexOf("Id") != -1 || param.indexOf("_id") != -1) ) { + $.inArray(param, preventAutoFiltering) == -1 && + (param.indexOf("Id") != -1 || param.indexOf("_id") != -1) ) { while ( i < retArr.length ) { if ( settings.data[param] != retArr[i][param] ) { retArr.splice(i, 1); diff --git a/event/resize/resize.js b/event/resize/resize.js index db36d1b1..bde49d4a 100644 --- a/event/resize/resize.js +++ b/event/resize/resize.js @@ -129,8 +129,20 @@ steal('jquery/event').then(function( $ ) { // if index == -1 it's the window while (++index < length && (child = resizers[index]) && (isWindow || $.contains(where, child)) ) { - // call the event - $.event.handle.call(child, ev); + if($(child).is(':visible')){ + // call the event + $.event.handle.call(child, ev); + } else { + // move index until the item is not in the current child + while (++index < length && (sub = resizers[index]) ) { + if (!$.contains(child, sub) ) { + // set index back one + index--; + break + } + } + } + if ( ev.isPropagationStopped() ) { // move index until the item is not in the current child diff --git a/model/backup/backup.js b/model/backup/backup.js index e4c8c481..f35dcc7a 100644 --- a/model/backup/backup.js +++ b/model/backup/backup.js @@ -115,6 +115,7 @@ See this in action: return !same(this.serialize(), this._backupStore, !!checkAssociations); } }, + /** * @function jQuery.Model.prototype.restore * @parent jquery.model.backup @@ -126,7 +127,36 @@ See this in action: this.attrs(props); return this; - } + }, + + changed: function(attr) { + var self= this, + attrs = this.Class.attributes, + changes = [], + changed = function(attr){ + // consider all attr as changed when instance is new + if (self.isNew()){ + return true + } + // check assoc by calling their changed function individually + if (typeof self[attr] == 'object' && self[attr] !== null && self[attr].changed && self[attr].changed().length){ + return true + } + // check attributes and plural assoc + if ( (self._backupStore && !same(self.serialize(attr), self._backupStore[attr]))){ + return true + } + } + if (attr){ + return changed(attr) + } + for (attr in attrs) { + if ( changed(attr) ){ + changes.push(attr) + } + } + return changes + } }) }) diff --git a/model/backup/qunit/qunit.js b/model/backup/qunit/qunit.js index 4869d98d..a4954076 100644 --- a/model/backup/qunit/qunit.js +++ b/model/backup/qunit/qunit.js @@ -11,36 +11,36 @@ module("jquery/model/backup",{ test("backing up", function(){ var recipe = new Recipe({name: "cheese"}); ok(!recipe.isDirty(), "not backedup, but clean") - + recipe.backup(); ok(!recipe.isDirty(), "backedup, but clean"); - + recipe.name = 'blah' - + ok(recipe.isDirty(), "dirty"); - + recipe.restore(); - + ok(!recipe.isDirty(), "restored, clean"); - + equals(recipe.name, "cheese" ,"name back"); - + }); test("backup / restore with associations", function(){ $.Model("Instruction"); $.Model("Cookbook"); - + $.Model("Recipe",{ attributes : { instructions : "Instruction.models", cookbook: "Cookbook.model" } },{}); - - - + + + var recipe = new Recipe({ name: "cheese burger", instructions : [ @@ -55,53 +55,90 @@ test("backup / restore with associations", function(){ title : "Justin's Grillin Times" } }); - + //test basic is dirty - + ok(!recipe.isDirty(), "not backedup, but clean") - + recipe.backup(); ok(!recipe.isDirty(), "backedup, but clean"); - + recipe.name = 'blah' - + ok(recipe.isDirty(), "dirty"); - + recipe.restore(); - + ok(!recipe.isDirty(), "restored, clean"); - + equals(recipe.name, "cheese burger" ,"name back"); - + // test belongs too - + ok(!recipe.cookbook.isDirty(), "cookbook not backedup, but clean"); - + recipe.cookbook.backup(); - + recipe.cookbook.attr("title","Brian's Burgers"); - + ok(!recipe.isDirty(), "recipe itself is clean"); - + ok(recipe.isDirty(true), "recipe is dirty if checking associations"); - + recipe.cookbook.restore() - + ok(!recipe.isDirty(true), "recipe is now clean with checking associations"); - + equals(recipe.cookbook.title, "Justin's Grillin Times" ,"cookbook title back"); - + //try belongs to recursive restore - + recipe.cookbook.attr("title","Brian's Burgers"); recipe.restore(); ok(recipe.isDirty(true), "recipe is dirty if checking associations, after a restore"); - + recipe.restore(true); ok(!recipe.isDirty(true), "cleaned all of recipe and its associations"); - - + + }) +test('makeParams only save changed attributes and associations', function(){ + $.Model('User',{ + attributes: { + name: 'string', + category: 'Category.model', + loans: 'Loans.model' + } + },{}) + $.Model('Category',{},{}) + $.Model('Loan',{},{}) + + u = new User({name: 'test', category: {name: 'category 1'}, loans:[{id:2, name: 'test'}]}) + data = u.makeParams() + equals(data['name'], 'test', "attributes") + equals(data['category']['name'], 'category 1', "single association") + equals(data['loans'][0]["name"], 'test', 'plural association') + + u = new User({id: 1, name: 'test', category: {id:2, name: 'category 1'}, loans:[{id:2, name: 'test'}]}) + data = u.makeParams() + delete data["id"] + ok($.isEmptyObject(data), 'nothing should change since its not a new object') + + u.category.attr('name', 'cat name has changed') + data = u.makeParams() + equals(data['name'], undefined) + equals(data['category']['name'], 'cat name has changed') + equals(data['category']['id'], 2) + equals(data['loans'], undefined) + + u.attr('loans',[{id:2, name: 'test'},{name: 'added loan'}]) + data = u.makeParams() + equals(data['loans'].length, 2) +}) + + + + }) \ No newline at end of file diff --git a/model/list/list.js b/model/list/list.js index 8f878e8a..b24946aa 100644 --- a/model/list/list.js +++ b/model/list/list.js @@ -363,7 +363,14 @@ $.Class("jQuery.Model.List", return this.map(function(item){ return item.serialize() }); - } + }, + makeParams : function(nested){ + return this.map(function(item){ + //if (item.isDirty()){ // don t send back unchanged item + return item.makeParams(nested) + //} + }); + } }); var push = [].push, diff --git a/model/model.js b/model/model.js index 57ec4931..29bda4b8 100644 --- a/model/model.js +++ b/model/model.js @@ -91,9 +91,9 @@ steal('jquery/class', 'jquery/lang/string', function() { reject = function(data){ deferred.rejectWith(self, [data]) }, - args = [self.serialize(), resolve, reject], + args = [self.makeParams(), resolve, reject], constructor = self.constructor; - + if(type == 'destroy'){ args.shift(); } @@ -859,15 +859,21 @@ steal('jquery/class', 'jquery/lang/string', function() { if (!attributes ) { return null; } + if( attributes instanceof this){ attributes = attributes.serialize(); } - return new this( + var instance = new this( // checks for properties in an object (like rails 2.0 gives); isObject(attributes[this._shortName]) || isObject(attributes.data) || isObject(attributes.attributes) || attributes); + if (this.list && instance[this.id] != undefined){ + this.list.remove(instance) + this.list.push(instance) + } + return instance }, /** * $.Model.models is used as a [http://api.jquery.com/extending-ajax/#Converters Ajax converter] @@ -945,7 +951,8 @@ steal('jquery/class', 'jquery/lang/string', function() { var res = getList(this.List), arr = isArray(instancesRawData), ml = ($.Model.List && instancesRawData instanceof $.Model.List), - raw = arr ? instancesRawData : (ml ? instancesRawData.serialize() : instancesRawData.data), + po = $.isPlainObject(instancesRawData) && !instancesRawData.hasOwnProperty('data'), // po as plain object with index as iterator ( allow {0=>item_attrs,1=>item_attrs, etc..} ) + raw = arr ? instancesRawData : (ml ? instancesRawData.serialize() : ( po ? $.map(instancesRawData, function(v){return v}) : instancesRawData.data )), length = raw.length, i = 0; //@steal-remove-start @@ -957,7 +964,7 @@ steal('jquery/class', 'jquery/lang/string', function() { for (; i < length; i++ ) { res.push(this.model(raw[i])); } - if (!arr ) { //push other stuff onto array + if (!arr && !po ) { //push other stuff onto array for ( var prop in instancesRawData ) { if ( prop !== 'data' ) { res[prop] = instancesRawData[prop]; @@ -1056,8 +1063,11 @@ steal('jquery/class', 'jquery/lang/string', function() { return isObject(val) && val.serialize ? val.serialize() : val; } }, + params: { + }, bind: bind, - unbind: unbind + unbind: unbind, + backendSerialize: {} }, /** * @Prototype @@ -1335,6 +1345,11 @@ steal('jquery/class', 'jquery/lang/string', function() { callback = success, list = Class.list; +// // backup the initial state of the instance + if (!this._init && this.backup && !this._backupStore ){ + this.backup() + } + val = this[property] = (value === null ? //if the value is null or undefined null : // it should be null converter.call(Class, value, function(){}, type) //convert it to something useful @@ -1445,25 +1460,62 @@ steal('jquery/class', 'jquery/lang/string', function() { } return attributes; }, - serialize : function(){ - var Class = this.constructor, + serialize : function(attr){ + var self= this, + Class = this.constructor, attrs = Class.attributes, type, converter, data = {}, - attr; - attributes = {}; - - for ( attr in attrs ) { - if ( attrs.hasOwnProperty(attr) ) { - type = attrs[attr]; - converter = Class.serialize[type] || Class.serialize['default']; - data[attr] = converter( this[attr] , type ); - } - } - return data; + serialize = function(attr){ + type = attrs[attr]; + converter = Class.serialize[type] || Class.serialize['default']; + return converter( self[attr] , type ); + }; + + if (attr){ + return serialize(attr) + } + + for ( attr in attrs ) { + if ( attrs.hasOwnProperty(attr) ) { + data[attr] = serialize(attr) + } + } + return data; + }, + makeParams: function(nested){ + var Class = this.constructor, + attrs = Class.attributes, + params = Class.params, + nested = nested || false, + type, + attr, + changed, + param, + data = {}, + prefixedData = {}; + + + for ( attr in attrs ){ + param = params[attr] == undefined ? {} : params[attr]; + type = attrs[attr]; + changed = !this.changed || this.changed(attr) ; + if ( (param != false && (changed || param == true)) + && (!param['if'] || param['if'].call(this, this[attr],type) == true) + && (!param['unless'] || param['unless'].call(this, this[attr],type) == false) ) + { + data[param['as'] || attr] = isObject(this[attr]) && this[attr].makeParams ? this[attr].makeParams(true) : this.serialize(attr) ; + } + } + + if (nested){ + data[Class.id] = getId(this); //ensure id is set for nested assoc + } + return data + }, /** * Returns if the instance is a new object. This is essentially if the * id is null or undefined. diff --git a/model/test/qunit/associations_test.js b/model/test/qunit/associations_test.js index 9e402874..42e0425b 100644 --- a/model/test/qunit/associations_test.js +++ b/model/test/qunit/associations_test.js @@ -58,11 +58,11 @@ test("associations work", function(){ }) equals(c.person.name, "Justin", "association present"); equals(c.person.Class, MyTest.Person, "belongs to association typed"); - + equals(c.issues.length, 0); - + equals(c.loans.length, 2); - + equals(c.loans[0].Class, MyTest.Loan); }); @@ -77,14 +77,14 @@ test("Model association serialize on save", function(){ loans : [] }), cSave = c.save(); - + stop(); cSave.then(function(customer){ start() equals(customer.personAttr, "My name is thecountofzero", "serialization works"); - + }); - + }); test("Model.List association serialize on save", function(){ @@ -107,7 +107,7 @@ test("Model.List association serialize on save", function(){ ] }), cSave = c.save(); - + stop(); cSave.then(function(customer){ start() @@ -115,7 +115,7 @@ test("Model.List association serialize on save", function(){ ok(customer.loansAttr._data === undefined, "_data does not exist"); ok(customer.loansAttr._use_call === undefined, "_use_call does not exist"); ok(customer.loansAttr._changed === undefined, "_changed does not exist"); - + }); - + }); \ No newline at end of file diff --git a/model/test/qunit/model_test.js b/model/test/qunit/model_test.js index 5b5dbda9..b3b0cff7 100644 --- a/model/test/qunit/model_test.js +++ b/model/test/qunit/model_test.js @@ -27,7 +27,7 @@ module("jquery/model", { test("CRUD", function(){ - + Person.findAll({}, function(response){ equals("findAll", response) }) @@ -90,7 +90,7 @@ test("findOne deferred", function(){ }); test("save deferred", function(){ - + $.Model("Person",{ create : function(attrs, success, error){ return $.ajax({ @@ -105,21 +105,21 @@ test("save deferred", function(){ }) } },{}); - + var person = new Person({name: "Justin"}), personD = person.save(); - + stop(); personD.then(function(person){ start() equals(person.id, 5, "we got an id") - + }); - + }); test("update deferred", function(){ - + $.Model("Person",{ update : function(id, attrs, success, error){ return $.ajax({ @@ -134,21 +134,21 @@ test("update deferred", function(){ }) } },{}); - + var person = new Person({name: "Justin", id:5}), personD = person.save(); - + stop(); personD.then(function(person){ start() equals(person.thing, "er", "we got updated") - + }); - + }); test("destroy deferred", function(){ - + $.Model("Person",{ destroy : function(id, success, error){ return $.ajax({ @@ -162,15 +162,15 @@ test("destroy deferred", function(){ }) } },{}); - + var person = new Person({name: "Justin", id:5}), personD = person.destroy(); - + stop(); personD.then(function(person){ start() equals(person.thing, "er", "we got destroyed") - + }); }); @@ -210,28 +210,28 @@ test("models", function(){ test("async setters", function(){ - + /* $.Model("Test.AsyncModel",{ setName : function(newVal, success, error){ - - + + setTimeout(function(){ success(newVal) }, 100) } }); - + var model = new Test.AsyncModel({ name : "justin" }); equals(model.name, "justin","property set right away") - + //makes model think it is no longer new model.id = 1; - + var count = 0; - + model.bind('name', function(ev, newName){ equals(newName, "Brian",'new name'); equals(++count, 1, "called once"); @@ -245,14 +245,14 @@ test("async setters", function(){ test("binding", 2,function(){ var inst = new Person({foo: "bar"}); - + inst.bind("foo", function(ev, val){ - ok(true,"updated") + ok(true,"updated") equals(val, "baz", "values match") }); - + inst.attr("foo","baz"); - + }); test("error binding", 1, function(){ @@ -269,8 +269,8 @@ test("error binding", 1, function(){ equals(error, "no name", "error message provided") }) school.attr("name",""); - - + + }) test("auto methods",function(){ @@ -286,24 +286,24 @@ test("auto methods",function(){ School.findAll({type:"schools"}, function(schools){ ok(schools,"findAll Got some data back"); equals(schools[0].constructor.shortName,"School","there are schools") - + School.findOne({id : "4"}, function(school){ ok(school,"findOne Got some data back"); equals(school.constructor.shortName,"School","a single school"); - - + + new School({name: "Highland"}).save(function(){ equals(this.name,"Highland","create gets the right name") this.update({name: "LHS"}, function(){ start(); equals(this.name,"LHS","create gets the right name") - + $.fixture.on = true; }) }) - + }) - + }) }) @@ -355,34 +355,34 @@ test("Model events" , function(){ success() } },{}); - + stop(); $([Test.Event]).bind('created',function(ev, passedItem){ - + ok(this === Test.Event, "got model") ok(passedItem === item, "got instance") equals(++order, 1, "order"); passedItem.update({}); - + }).bind('updated', function(ev, passedItem){ equals(++order, 2, "order"); ok(this === Test.Event, "got model") ok(passedItem === item, "got instance") - + passedItem.destroy({}); - + }).bind('destroyed', function(ev, passedItem){ equals(++order, 3, "order"); ok(this === Test.Event, "got model") ok(passedItem === item, "got instance") - + start(); - + }) - + var item = new Test.Event(); item.save(); - + }); @@ -444,17 +444,8 @@ test("removeAttr test", function(){ var person = new Person({foo: "bar"}) equals(person.foo, "bar", "property set"); person.removeAttr('foo') - + equals(person.foo, undefined, "property removed"); var attrs = person.attrs() equals(attrs.foo, undefined, "attrs removed"); }); - -test("identity should replace spaces with underscores", function(){ - $.Model("Task",{},{}); - t = new Task({ - id: "id with spaces" - }); - equals(t.identity(), "task_id_with_spaces") -}); - diff --git a/model/test/qunit/qunit.js b/model/test/qunit/qunit.js index b4fca3b7..71679bec 100644 --- a/model/test/qunit/qunit.js +++ b/model/test/qunit/qunit.js @@ -1,8 +1,8 @@ //we probably have to have this only describing where the tests are steal("jquery/model","jquery/dom/fixture") //load your app .then('funcunit/qunit') //load qunit - .then("./model_test.js","./associations_test.js") - .then( +.then("./model_test.js","./associations_test.js") +.then( "jquery/model/backup/qunit", "jquery/model/list/test/qunit" ) diff --git a/view/helpers/helpers.js b/view/helpers/helpers.js index 75b3f91b..05aa09ef 100644 --- a/view/helpers/helpers.js +++ b/view/helpers/helpers.js @@ -209,22 +209,40 @@ $.extend($.EJS.Helpers.prototype, { * @param {Object} choices * @param {Object} html_options */ - select_tag: function( name, value, choices, html_options ) { + select_tag: function( name, value, choices, html_options ) { + var values = $.isArray(value) ? value : [value]; html_options = html_options || {}; html_options.id = html_options.id || name; //html_options.value = value; html_options.name = name; + if (html_options.hasOwnProperty('include_blank')){ + choices.unshift({text: ((html_options.include_blank == true ) ? '' : html_options.include_blank), value: ''}); + delete html_options['include_blank']; + } var txt = ''; txt += this.start_tag_for('select', html_options); - for(var i = 0; i < choices.length; i++) + var prevGroupOn = null; + + if (choices[0] && choices[0].hasOwnProperty('groupOn')){ + txt += this.start_tag_for('optgroup', {label: choices[0].groupLabel}); + prevGroupOn = choices[0].groupOn + } + for(var i = 0; i < choices.length; i++) { - var choice = choices[i]; + var choice = choices[i]; + + if(choice.hasOwnProperty('groupOn') && choice.groupOn != prevGroupOn){ + txt += this.tag_end('optgroup'); + txt += this.start_tag_for('optgroup', {label: choice.groupLabel}); + prevGroupOn = choice.groupOn + } + if(typeof choice == 'string') choice = {value: choice}; if(!choice.text) choice.text = choice.value; if(!choice.value) choice.text = choice.text; var optionOptions = {value: choice.value}; - if(choice.value == value) + if($.inArray(choice.value, values) != -1) optionOptions.selected ='selected'; txt += this.start_tag_for('option', optionOptions )+choice.text+this.tag_end('option'); } @@ -318,7 +336,7 @@ $.extend($.EJS.Helpers.prototype, { options.src = steal.root.join("resources/images/"+image_location); return this.single_tag_for('img', options); } - + }); $.EJS.Helpers.prototype.text_tag = $.EJS.Helpers.prototype.text_area_tag;