From 55d89d1aeba0cbed40bbb4bbc96f019d54738bb2 Mon Sep 17 00:00:00 2001 From: Ryan Niemeyer Date: Fri, 9 Mar 2012 10:27:32 -0600 Subject: [PATCH] Fix for issue #8. Move back to handling template updates in update function (to capture dependencies during parsing of binding) and initialize widget in setTimeout to allow template to render. --- build/knockout-sortable.js | 132 ++++++++++++++++----------------- build/knockout-sortable.min.js | 2 +- spec/knockout-sortable.spec.js | 109 ++++++++++++++++++++++----- src/knockout-sortable.js | 132 ++++++++++++++++----------------- 4 files changed, 225 insertions(+), 150 deletions(-) diff --git a/build/knockout-sortable.js b/build/knockout-sortable.js index 5b4626c..4712c0d 100644 --- a/build/knockout-sortable.js +++ b/build/knockout-sortable.js @@ -62,80 +62,71 @@ ko.bindingHandlers.sortable = { ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, sortable.allowDrop); } - //attach meta-data - ko.utils.domData.set(element, listKey, templateOptions.foreach); - //wrap the template binding - var templateArgs = [element, function() { return templateOptions; }, allBindingsAccessor, data, context]; - ko.bindingHandlers.template.init.apply(this, templateArgs); - ko.computed({ - read: function() { - ko.bindingHandlers.template.update.apply(this, templateArgs); - }, - disposeWhenNodeIsRemoved: element, - owner: this - }); - - //initialize sortable binding - $element.sortable(ko.utils.extend(sortable.options, { - update: function(event, ui) { - var sourceParent, targetParent, targetIndex, arg, - el = ui.item[0], - item = ko.utils.domData.get(el, itemKey); - - if (item) { - //identify parents - sourceParent = ko.utils.domData.get(el, parentKey); - targetParent = ko.utils.domData.get(el.parentNode, listKey); - targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el); - - if (sortable.beforeMove || sortable.afterMove) { - arg = { - item: item, - sourceParent: sourceParent, - sourceParentNode: el.parentNode, - sourceIndex: sourceParent.indexOf(item), - targetParent: targetParent, - targetIndex: targetIndex, - cancelDrop: false - }; - } + ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context); + + //initialize sortable binding after template binding has rendered in update function + setTimeout(function() { + $element.sortable(ko.utils.extend(sortable.options, { + update: function(event, ui) { + var sourceParent, targetParent, targetIndex, arg, + el = ui.item[0], + item = ko.utils.domData.get(el, itemKey); + + if (item) { + //identify parents + sourceParent = ko.utils.domData.get(el, parentKey); + targetParent = ko.utils.domData.get(el.parentNode, listKey); + targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el); + + if (sortable.beforeMove || sortable.afterMove) { + arg = { + item: item, + sourceParent: sourceParent, + sourceParentNode: el.parentNode, + sourceIndex: sourceParent.indexOf(item), + targetParent: targetParent, + targetIndex: targetIndex, + cancelDrop: false + }; + } - if (sortable.beforeMove) { - sortable.beforeMove.call(this, arg, event, ui); - if (arg.cancelDrop) { - $(ui.sender).sortable('cancel'); - return; + if (sortable.beforeMove) { + sortable.beforeMove.call(this, arg, event, ui); + if (arg.cancelDrop) { + $(ui.sender).sortable('cancel'); + return; + } } - } - if (targetIndex >= 0) { - sourceParent.remove(item); - targetParent.splice(targetIndex, 0, item); - } + if (targetIndex >= 0) { + sourceParent.remove(item); + targetParent.splice(targetIndex, 0, item); + } - //rendering is handled by manipulating the observableArray; ignore dropped element - ko.utils.domData.set(el, itemKey, null); - ui.item.remove(); + //rendering is handled by manipulating the observableArray; ignore dropped element + ko.utils.domData.set(el, itemKey, null); + ui.item.remove(); - //allow binding to accept a function to execute after moving the item - if (sortable.afterMove) { - sortable.afterMove.call(this, arg, event, ui); + //allow binding to accept a function to execute after moving the item + if (sortable.afterMove) { + sortable.afterMove.call(this, arg, event, ui); + } } - } - }, - connectWith: sortable.connectClass ? "." + sortable.connectClass : false - })); - - //handle enabling/disabling sorting - if (sortable.isEnabled !== undefined) { - ko.computed({ - read: function() { - $element.sortable(ko.utils.unwrapObservable(sortable.isEnabled) ? "enable" : "disable"); }, - disposeWhenNodeIsRemoved: element - }); - } + connectWith: sortable.connectClass ? "." + sortable.connectClass : false + })); + + //handle enabling/disabling sorting + if (sortable.isEnabled !== undefined) { + ko.computed({ + read: function() { + $element.sortable(ko.utils.unwrapObservable(sortable.isEnabled) ? "enable" : "disable"); + }, + disposeWhenNodeIsRemoved: element + }); + } + }, 0); //handle disposal ko.utils.domNodeDisposal.addDisposeCallback(element, function() { @@ -144,6 +135,15 @@ ko.bindingHandlers.sortable = { return { 'controlsDescendantBindings': true }; }, + update: function(element, valueAccessor, allBindingsAccessor, data, context) { + var templateOptions = prepareTemplateOptions(valueAccessor); + + //attach meta-data + ko.utils.domData.set(element, listKey, templateOptions.foreach); + + //call template binding's update with correct options + ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context); + }, afterRender: function(elements, data) { ko.utils.arrayForEach(elements, function(element) { if (element.nodeType === 1) { diff --git a/build/knockout-sortable.min.js b/build/knockout-sortable.min.js index e91b76b..6679c4b 100644 --- a/build/knockout-sortable.min.js +++ b/build/knockout-sortable.min.js @@ -1,2 +1,2 @@ //knockout-sortable | (c) 2012 Ryan Niemeyer | http://www.opensource.org/licenses/mit-license -(function(a,b,c){var d=function(b){var c={},d=a.utils.unwrapObservable(b());return d.data?(c.foreach=d.data,c.name=d.template,c.afterAdd=d.afterAdd,c.beforeRemove=d.beforeRemove,c.afterRender=d.afterRender,c.includeDestroyed=d.includeDestroyed,c.templateEngine=d.templateEngine):c.foreach=b(),d.afterRender?c.afterRender=function(b,c){a.bindingHandlers.sortable.afterRender.call(c,b,c),d.afterRender.call(c,b,c)}:c.afterRender=a.bindingHandlers.sortable.afterRender,c},e="ko_sortItem",f="ko_sortList",g="ko_parentList";a.bindingHandlers.sortable={init:function(h,i,j,k,l){var m=b(h),n=a.utils.unwrapObservable(i()),o=d(i),p={};a.utils.extend(p,a.bindingHandlers.sortable),a.utils.extend(p,n||{}),p.connectClass&&(a.isObservable(p.allowDrop)||typeof p.allowDrop=="function")?a.computed({read:function(){var b=a.utils.unwrapObservable(p.allowDrop),c=typeof b=="function"?b.call(this,o.foreach):b;a.utils.toggleDomNodeCssClass(h,p.connectClass,c)},disposeWhenNodeIsRemoved:h},this):a.utils.toggleDomNodeCssClass(h,p.connectClass,p.allowDrop),a.utils.domData.set(h,f,o.foreach);var q=[h,function(){return o},j,k,l];return a.bindingHandlers.template.init.apply(this,q),a.computed({read:function(){a.bindingHandlers.template.update.apply(this,q)},disposeWhenNodeIsRemoved:h,owner:this}),m.sortable(a.utils.extend(p.options,{update:function(c,d){var h,i,j,k,l=d.item[0],m=a.utils.domData.get(l,e);if(m){h=a.utils.domData.get(l,g),i=a.utils.domData.get(l.parentNode,f),j=a.utils.arrayIndexOf(d.item.parent().children(),l);if(p.beforeMove||p.afterMove)k={item:m,sourceParent:h,sourceParentNode:l.parentNode,sourceIndex:h.indexOf(m),targetParent:i,targetIndex:j,cancelDrop:!1};if(p.beforeMove){p.beforeMove.call(this,k,c,d);if(k.cancelDrop){b(d.sender).sortable("cancel");return}}j>=0&&(h.remove(m),i.splice(j,0,m)),a.utils.domData.set(l,e,null),d.item.remove(),p.afterMove&&p.afterMove.call(this,k,c,d)}},connectWith:p.connectClass?"."+p.connectClass:!1})),p.isEnabled!==c&&a.computed({read:function(){m.sortable(a.utils.unwrapObservable(p.isEnabled)?"enable":"disable")},disposeWhenNodeIsRemoved:h}),a.utils.domNodeDisposal.addDisposeCallback(h,function(){m.sortable("destroy")}),{controlsDescendantBindings:!0}},afterRender:function(b,c){a.utils.arrayForEach(b,function(b){b.nodeType===1&&(a.utils.domData.set(b,e,c),a.utils.domData.set(b,g,a.utils.domData.get(b.parentNode,f)))})},connectClass:"ko_container",allowDrop:!0,afterMove:null,beforeMove:null,options:{}}})(ko,jQuery) \ No newline at end of file +(function(a,b,c){var d=function(b){var c={},d=a.utils.unwrapObservable(b());return d.data?(c.foreach=d.data,c.name=d.template,c.afterAdd=d.afterAdd,c.beforeRemove=d.beforeRemove,c.afterRender=d.afterRender,c.includeDestroyed=d.includeDestroyed,c.templateEngine=d.templateEngine):c.foreach=b(),d.afterRender?c.afterRender=function(b,c){a.bindingHandlers.sortable.afterRender.call(c,b,c),d.afterRender.call(c,b,c)}:c.afterRender=a.bindingHandlers.sortable.afterRender,c},e="ko_sortItem",f="ko_sortList",g="ko_parentList";a.bindingHandlers.sortable={init:function(h,i,j,k,l){var m=b(h),n=a.utils.unwrapObservable(i()),o=d(i),p={};return a.utils.extend(p,a.bindingHandlers.sortable),a.utils.extend(p,n||{}),p.connectClass&&(a.isObservable(p.allowDrop)||typeof p.allowDrop=="function")?a.computed({read:function(){var b=a.utils.unwrapObservable(p.allowDrop),c=typeof b=="function"?b.call(this,o.foreach):b;a.utils.toggleDomNodeCssClass(h,p.connectClass,c)},disposeWhenNodeIsRemoved:h},this):a.utils.toggleDomNodeCssClass(h,p.connectClass,p.allowDrop),a.bindingHandlers.template.init(h,function(){return o},j,k,l),setTimeout(function(){m.sortable(a.utils.extend(p.options,{update:function(c,d){var h,i,j,k,l=d.item[0],m=a.utils.domData.get(l,e);if(m){h=a.utils.domData.get(l,g),i=a.utils.domData.get(l.parentNode,f),j=a.utils.arrayIndexOf(d.item.parent().children(),l);if(p.beforeMove||p.afterMove)k={item:m,sourceParent:h,sourceParentNode:l.parentNode,sourceIndex:h.indexOf(m),targetParent:i,targetIndex:j,cancelDrop:!1};if(p.beforeMove){p.beforeMove.call(this,k,c,d);if(k.cancelDrop){b(d.sender).sortable("cancel");return}}j>=0&&(h.remove(m),i.splice(j,0,m)),a.utils.domData.set(l,e,null),d.item.remove(),p.afterMove&&p.afterMove.call(this,k,c,d)}},connectWith:p.connectClass?"."+p.connectClass:!1})),p.isEnabled!==c&&a.computed({read:function(){m.sortable(a.utils.unwrapObservable(p.isEnabled)?"enable":"disable")},disposeWhenNodeIsRemoved:h})},0),a.utils.domNodeDisposal.addDisposeCallback(h,function(){m.sortable("destroy")}),{controlsDescendantBindings:!0}},update:function(b,c,e,g,h){var i=d(c);a.utils.domData.set(b,f,i.foreach),a.bindingHandlers.template.update(b,function(){return i},e,g,h)},afterRender:function(b,c){a.utils.arrayForEach(b,function(b){b.nodeType===1&&(a.utils.domData.set(b,e,c),a.utils.domData.set(b,g,a.utils.domData.get(b.parentNode,f)))})},connectClass:"ko_container",allowDrop:!0,afterMove:null,beforeMove:null,options:{}}})(ko,jQuery) \ No newline at end of file diff --git a/spec/knockout-sortable.spec.js b/spec/knockout-sortable.spec.js index c9c83ac..d1c485d 100644 --- a/spec/knockout-sortable.spec.js +++ b/spec/knockout-sortable.spec.js @@ -98,7 +98,11 @@ describe("knockout-sortable", function(){ }); it("should call .sortable on the root element", function() { - expect(options.root.data("sortable")).toBeDefined(); + waits(0); + runs(function() { + expect(options.root.data("sortable")).toBeDefined(); + }); + }); it("should attach meta-data to the root element indicating the parent observableArray", function() { @@ -275,7 +279,10 @@ describe("knockout-sortable", function(){ }); it("should set this element's sortable connectWith option to false", function() { - expect(options.root.sortable("option", "connectWith")).toEqual(false); + waits(0); + runs(function() { + expect(options.root.sortable("option", "connectWith")).toEqual(false); + }); }); }); @@ -290,10 +297,12 @@ describe("knockout-sortable", function(){ }); it("should set this element's sortable connectWith option to false", function() { - expect(options.root.sortable("option", "connectWith")).toEqual(false); + waits(0); + runs(function() { + expect(options.root.sortable("option", "connectWith")).toEqual(false); + }); }); }); - }); describe("when overriding connectClass in the binding options", function() { @@ -333,7 +342,10 @@ describe("knockout-sortable", function(){ }); it("should set this element's sortable connectWith option to false", function() { - expect(options.root.sortable("option", "connectWith")).toEqual(false); + waits(0); + runs(function() { + expect(options.root.sortable("option", "connectWith")).toEqual(false); + }); }); }); @@ -352,7 +364,10 @@ describe("knockout-sortable", function(){ }); it("should set this element's sortable connectWith option to false", function() { - expect(options.root.sortable("option", "connectWith")).toEqual(false); + waits(0); + runs(function() { + expect(options.root.sortable("option", "connectWith")).toEqual(false); + }); }); }); }); @@ -376,12 +391,18 @@ describe("knockout-sortable", function(){ }); it("should be initially disabled", function() { - expect(options.root.sortable("option", "disabled")).toBeTruthy(); + waits(0); + runs(function() { + expect(options.root.sortable("option", "disabled")).toBeTruthy(); + }); }); it("should become enabled when observable is changed to true", function() { - options.vm.isEnabled(true); - expect(options.root.sortable("option", "disabled")).toBeFalsy(); + waits(0); + runs(function() { + options.vm.isEnabled(true); + expect(options.root.sortable("option", "disabled")).toBeFalsy(); + }); }) }); @@ -392,7 +413,10 @@ describe("knockout-sortable", function(){ }); it("should be initially disabled", function() { - expect(options.root.sortable("option", "disabled")).toBeTruthy(); + waits(0); + runs(function() { + expect(options.root.sortable("option", "disabled")).toBeTruthy(); + }); }); }); }); @@ -415,12 +439,18 @@ describe("knockout-sortable", function(){ }); it("should be initially disabled", function() { - expect(options.root.sortable("option", "disabled")).toBeTruthy(); + waits(0); + runs(function() { + expect(options.root.sortable("option", "disabled")).toBeTruthy(); + }); }); it("should become enabled when observable is changed to true", function() { - options.vm.isEnabled(true); - expect(options.root.sortable("option", "disabled")).toBeFalsy(); + waits(0); + runs(function() { + options.vm.isEnabled(true); + expect(options.root.sortable("option", "disabled")).toBeFalsy(); + }); }) }); @@ -431,7 +461,10 @@ describe("knockout-sortable", function(){ }); it("should be initially disabled", function() { - expect(options.root.sortable("option", "disabled")).toBeTruthy(); + waits(0); + runs(function() { + expect(options.root.sortable("option", "disabled")).toBeTruthy(); + }); }); }); }); @@ -449,7 +482,10 @@ describe("knockout-sortable", function(){ }); it("should pass the option on to .sortable properly", function() { - expect(options.root.sortable("option", "axis")).toEqual('x'); + waits(0); + runs(function() { + expect(options.root.sortable("option", "axis")).toEqual('x'); + }); }); }); @@ -468,8 +504,47 @@ describe("knockout-sortable", function(){ }); it("should pass the option on to .sortable properly", function() { - expect(options.root.sortable("option", "axis")).toEqual('x'); + waits(0); + runs(function() { + expect(options.root.sortable("option", "axis")).toEqual('x'); + }); + }); + }); + + describe("when using a computed observable to return an observableArray", function() { + var options; + + beforeEach(function() { + options = { + elems: $(""), + vm: { + itemsOne: ko.observableArray([1, 2, 3]), + itemsTwo: ko.observableArray(["a", "b", "c"]), + useTwo: ko.observable(false) + } + }; + + options.vm.activeList = ko.computed(function() { + return this.useTwo() ? this.itemsTwo : this.itemsOne; + }, options.vm); + + setup(options); + }); + + it("should render the initial list", function() { + expect(options.root.children().first().text()).toEqual("1"); + expect(options.root.children(":nth-child(2)").text()).toEqual("2"); + expect(options.root.children(":nth-child(3)").text()).toEqual("3"); + }); + + describe("when updating the list that is returned by the computed observable", function() { + it("should render the new list", function() { + options.vm.useTwo(true); + expect(options.root.children().first().text()).toEqual("a"); + expect(options.root.children(":nth-child(2)").text()).toEqual("b"); + expect(options.root.children(":nth-child(3)").text()).toEqual("c"); + }); }); }); - }); + }); }); \ No newline at end of file diff --git a/src/knockout-sortable.js b/src/knockout-sortable.js index 5b4626c..4712c0d 100644 --- a/src/knockout-sortable.js +++ b/src/knockout-sortable.js @@ -62,80 +62,71 @@ ko.bindingHandlers.sortable = { ko.utils.toggleDomNodeCssClass(element, sortable.connectClass, sortable.allowDrop); } - //attach meta-data - ko.utils.domData.set(element, listKey, templateOptions.foreach); - //wrap the template binding - var templateArgs = [element, function() { return templateOptions; }, allBindingsAccessor, data, context]; - ko.bindingHandlers.template.init.apply(this, templateArgs); - ko.computed({ - read: function() { - ko.bindingHandlers.template.update.apply(this, templateArgs); - }, - disposeWhenNodeIsRemoved: element, - owner: this - }); - - //initialize sortable binding - $element.sortable(ko.utils.extend(sortable.options, { - update: function(event, ui) { - var sourceParent, targetParent, targetIndex, arg, - el = ui.item[0], - item = ko.utils.domData.get(el, itemKey); - - if (item) { - //identify parents - sourceParent = ko.utils.domData.get(el, parentKey); - targetParent = ko.utils.domData.get(el.parentNode, listKey); - targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el); - - if (sortable.beforeMove || sortable.afterMove) { - arg = { - item: item, - sourceParent: sourceParent, - sourceParentNode: el.parentNode, - sourceIndex: sourceParent.indexOf(item), - targetParent: targetParent, - targetIndex: targetIndex, - cancelDrop: false - }; - } + ko.bindingHandlers.template.init(element, function() { return templateOptions; }, allBindingsAccessor, data, context); + + //initialize sortable binding after template binding has rendered in update function + setTimeout(function() { + $element.sortable(ko.utils.extend(sortable.options, { + update: function(event, ui) { + var sourceParent, targetParent, targetIndex, arg, + el = ui.item[0], + item = ko.utils.domData.get(el, itemKey); + + if (item) { + //identify parents + sourceParent = ko.utils.domData.get(el, parentKey); + targetParent = ko.utils.domData.get(el.parentNode, listKey); + targetIndex = ko.utils.arrayIndexOf(ui.item.parent().children(), el); + + if (sortable.beforeMove || sortable.afterMove) { + arg = { + item: item, + sourceParent: sourceParent, + sourceParentNode: el.parentNode, + sourceIndex: sourceParent.indexOf(item), + targetParent: targetParent, + targetIndex: targetIndex, + cancelDrop: false + }; + } - if (sortable.beforeMove) { - sortable.beforeMove.call(this, arg, event, ui); - if (arg.cancelDrop) { - $(ui.sender).sortable('cancel'); - return; + if (sortable.beforeMove) { + sortable.beforeMove.call(this, arg, event, ui); + if (arg.cancelDrop) { + $(ui.sender).sortable('cancel'); + return; + } } - } - if (targetIndex >= 0) { - sourceParent.remove(item); - targetParent.splice(targetIndex, 0, item); - } + if (targetIndex >= 0) { + sourceParent.remove(item); + targetParent.splice(targetIndex, 0, item); + } - //rendering is handled by manipulating the observableArray; ignore dropped element - ko.utils.domData.set(el, itemKey, null); - ui.item.remove(); + //rendering is handled by manipulating the observableArray; ignore dropped element + ko.utils.domData.set(el, itemKey, null); + ui.item.remove(); - //allow binding to accept a function to execute after moving the item - if (sortable.afterMove) { - sortable.afterMove.call(this, arg, event, ui); + //allow binding to accept a function to execute after moving the item + if (sortable.afterMove) { + sortable.afterMove.call(this, arg, event, ui); + } } - } - }, - connectWith: sortable.connectClass ? "." + sortable.connectClass : false - })); - - //handle enabling/disabling sorting - if (sortable.isEnabled !== undefined) { - ko.computed({ - read: function() { - $element.sortable(ko.utils.unwrapObservable(sortable.isEnabled) ? "enable" : "disable"); }, - disposeWhenNodeIsRemoved: element - }); - } + connectWith: sortable.connectClass ? "." + sortable.connectClass : false + })); + + //handle enabling/disabling sorting + if (sortable.isEnabled !== undefined) { + ko.computed({ + read: function() { + $element.sortable(ko.utils.unwrapObservable(sortable.isEnabled) ? "enable" : "disable"); + }, + disposeWhenNodeIsRemoved: element + }); + } + }, 0); //handle disposal ko.utils.domNodeDisposal.addDisposeCallback(element, function() { @@ -144,6 +135,15 @@ ko.bindingHandlers.sortable = { return { 'controlsDescendantBindings': true }; }, + update: function(element, valueAccessor, allBindingsAccessor, data, context) { + var templateOptions = prepareTemplateOptions(valueAccessor); + + //attach meta-data + ko.utils.domData.set(element, listKey, templateOptions.foreach); + + //call template binding's update with correct options + ko.bindingHandlers.template.update(element, function() { return templateOptions; }, allBindingsAccessor, data, context); + }, afterRender: function(elements, data) { ko.utils.arrayForEach(elements, function(element) { if (element.nodeType === 1) {