KnockoutJS custom binding scoping issue - javascript

I have a custom binding that overrides knockout's click handler like so:
var originalInit = ko.bindingHandlers.click.init,
originalUpdate = ko.bindingHandlers.click.update;
ko.bindingHandlers.click = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel, context) {
var wrappedValueAccessor = function() {
return function(data, event) {
var disabled = allBindingsAccessor.get('disabled');
var clickResult = valueAccessor().call(viewModel, data, event);
if (clickResult && typeof clickResult.always === "function") {
$(element).attr('disabled','disabled');
clickResult.always(function(){
$(element).removeAttr('disabled');
});
}
};
};
originalInit(element, wrappedValueAccessor, allBindingsAccessor, viewModel, context);
},
update: originalUpdate
};
Find the fiddle here: http://jsfiddle.net/92q5vgfp/
The problem is when I try to access allBindingsAccessor inside the click from the chrome debugger, it's not available.
However, if i have a console.log(allBindingsAccessor), chrome's debugger can see it.
Update So, while I was writing this, we tried a random thing, which was to assign the function to a variable before returning it. That worked. Don't know why or how.
var wrappedValueAccessor = function() {
var test = function(data, event) {
...
};
return test;
};
So that's my question, WHY would assigning the function to a local var and returning it work but not directly returning it? Is this a bug in chrome or expected (somehow)?

In the linked snippet allBindingsAccessor is not accessed inside the inner function so v8 simply optimizes it out and don't add to the function closure. See crbug.com/172386 for more details.

Related

Custom Binding not working updating from Knockout 2.3 to 3.3

My Windows 8.1 App was working fine on Knockout 2.3 but after updating to 3.3 it seems like I get the wrong Context in my custom binding.
First here is how I apply binding for individual elements in the command bar of my app :
var cmdArray = [];
var commandIsRunning = function() {
return _.any(cmdArray, function(command) {
return command.isRunning();
});
};
_.each(_bottomCommands, function (row) {
if(row.command) {
// command wrapper
var commandWrapper = ko.command({
action: function() {
var rowCommand = row.command();
if (rowCommand) {
return rowCommand();
}
return WinJS.Promise.as();
},
canExecute: function() {
var rowCommand = row.command();
if (rowCommand) {
return rowCommand.canExecute() && !commandIsRunning();
}
return false;
}
});
cmdArray.push(commandWrapper);
//Bind the command
var element = document.querySelector('#' + row.id);
if (element) {
element.setAttribute('data-bind', 'command: $data');
ko.applyBindings(commandWrapper, element);
}
}
});
Here is my custom binding code
ko.bindingHandlers.command = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var command = ko.utils.unwrapObservable(valueAccessor());
ko.bindingHandlers.click.init.call(this, element, ko.observable(command), allBindingsAccessor, viewModel, bindingContext);
},
update: function (element, valueAccessor, allBindingsAccessor) {
var command = ko.utils.unwrapObservable(valueAccessor());
ko.bindingHandlers.enable.update.call(this, element, command.canExecute, allBindingsAccessor);
}
};
The problem is in:
ko.bindingHandlers.enable.update.call(this, element, command.canExecute, allBindingsAccessor);
canExecute is undefined which I think is because I am not getting the right context in the init and update handlers. So what am I doing wrong in my code? Again the code was working in Knockout 2.3 , so could it be a Knockout issue?
UPDATE:
I created jsFiddle to show the problem. It contains the definition for ko.command because I thought that could be the cause of problem
JSFiddle
The error is caused because Knockout 3.x binds to functions differently. In 2.x, you could bind directly to a function, but in 3.x, Knockout calls the function to get the viewmodel. You can still bind to a function in Knockout 3.x, but you'll need to wrap it in an observable or in another function.
ko.applyBindings(function() { return commandWrapper }, element);
https://jsfiddle.net/mbest/nrb97g7e/38/

Knockout.js unable to process binding: undefined is not a function?

I'm trying to implement a custom binding for an accordion-like document layout on a webpage, but I'm encountering an issue I can't easily solve.
Immediately on page load, I am presented with the error:
Uncaught TypeError: Unable to process binding "accordion: function (){return currentAccordionSection }"
Message: undefined is not a function
I have tried declaring my observable as both a function and normally in the data-bind syntax without success. I have initialized my observable with a default value (null) and it has not fixed this issue. Below is my entire ViewModel:
var libraryViewModel = function () {
var self = this;
ko.bindingHandlers.accordion = {
update: function (element, valueAccessor) {
console.log(ko.unwrap(valueAccessor()));
var value = ko.unwrap(valueAccessor());
var section = $(element.text());
//ko.bindingHandlers.css.update(element, function () {
// if (value === section) {
// return 'library-section-active';
// }
//});
//ko.bindingHandlers.css.update($(element).children('i:last-child').get(0), function () {
// if (value === section) {
// return 'fa fa-chevron-up';
// } else {
// return 'fa fa-chevron-down';
// }
//});
}
}
self.currentAccordionSection = ko.observable(null);
self.updateAccordionSection = function (section) {
self.currentAccordionSection(section);
}
}
Some of the code above is commented out as it is not relevant to the problem at hand and I have disabled it to implement a reduced test case to narrow down the problem. Here is my binding declaration:
<h2 class="library-header" data-bind="accordion: currentAccordionSection, click: updateAccordionSection.bind('Text')">
What exactly am I doing wrong?
The problem is this line:
var section = $(element.text());
as per knockout's documentation
element — The DOM element involved in this binding
text is a jQuery function not a DOM function so I think you are looking for something like:
$(element).text() or $($(element).text()) instead? I'd assume the former since it makes more sense.
As for the nested binding handler I'm not sure why that is in the viewmodel since it's exposed on the knockout global object you're not protecting yourself from anything just making your code more unreadable. They are designed to be resuable so you can use them with different viewModels

Custom JQuery Plugin Method error

I've been working on writing a custom jquery plugin for one of my web applications but I've been running into a strange error, I think it's due to my unfamiliarity with object-oriented programming.
The bug that I've been running into comes when I try to run the $(".list-group").updateList('template', 'some template') twice, the first time it works just fine, but the second time I run the same command, I get an object is not a function error. Here's the plugin code:
(function($){
defaultOptions = {
defaultId: 'selective_update_',
listSelector: 'li'
};
function UpdateList(item, options) {
this.options = $.extend(defaultOptions, options);
this.item = $(item);
this.init();
console.log(this.options);
}
UpdateList.prototype = {
init: function() {
console.log('initiation');
},
template: function(template) {
// this line is where the errors come
this.template = template;
},
update: function(newArray) {
//update code is here
// I can run this multiple times in a row without it breaking
}
}
// jQuery plugin interface
$.fn.updateList = function(opt) {
// slice arguments to leave only arguments after function name
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var item = $(this), instance = item.data('UpdateList');
if(!instance) {
// create plugin instance and save it in data
item.data('UpdateList', new UpdateList(this, opt));
} else {
// if instance already created call method
if(typeof opt === 'string') {
instance[opt](args);
}
}
});
}
}(jQuery));
One thing I did notice when I went to access this.template - It was in an array so I had to call this.template[0] to get the string...I don't know why it's doing that, but I suspect it has to do with the error I'm getting. Maybe it can assign the string the first time, but not the next? Any help would be appreciated!
Thanks :)
this.template = template
Is in fact your problem, as you are overwriting the function that is set on the instance. You end up overwriting it to your args array as you pass that as your argument to the initial template function. It basically will do this:
this.template = ["some template"];
Thus the next time instance[opt](args) runs it will try to execute that array as if it were a function and hence get the not a function error.
JSFiddle

Custom event document onContentChange

Here jsFiddle to test sample
I'm currently writing a jQuery snippet to handle any html content change in DOM 'triggered' by any jQuery domManip function (extending some functions). Not sure it's the best way to do it, so any advice will be welcome.
This snippet works as expected if bound to document. However, if I try to bind it to a specific element, I'm facing problem which some function as .remove(). Maybe it's due to custom event not using normal propagation behaviour but I'm really not sure.
This is a working sample, I bind contentChange event to document, works cross-browser as I can test it: {Firefox, IE9, Chrome and Safari under Win7}
;
(function ($) {
$.fn.contentChange = function (types, data, fn) {
return this.on('contentChange', types, null, data, fn);
};
var oDomManip = $.fn.domManip,
oHtml = $.fn.html,
oEmpty = $.fn.empty,
oRemove = $.fn.remove,
extendFct = function (oFct, sender, args) {
return oFct.apply(sender, args), $.event.trigger('contentChange');
//=>if testing specific element (#test) use instead following line
//return oFct.apply(sender, args), $(sender).trigger('contentChange');
};
$.fn.domManip = function () {
extendFct(oDomManip, this, arguments)
};
$.fn.html = function () {
extendFct(oHtml, this, arguments)
};
$.fn.empty = function () {
extendFct(oEmpty, this, arguments)
};
$.fn.remove = function () {
extendFct(oRemove, this, arguments)
};
})(jQuery);
I use: $.event.trigger('contentChange') to trigger custom event.
Called like it:
$(document).contentChange(function () {
console.log("onContentChange")
});
However, if I use:
$('#test').contentChange(function () {
console.log("onContentChange")
});
The custom event is not triggered.
So, to trigger a custom event on a specific element, I can triggered it like this:
$(sender).trigger('contentChange');
But now, call to remove() method on self or children doesn't triggered my custom event.
I can understand that event callback function won't be called if I remove the element, but why isn't it called when removing children (while it's working if bound to document!)?
I was expecting this line to make custom event bubbles to '#test':
$('#test').find('div:first').remove();
Is there any way to triggered this custom event bound to a specific element when manipulating this element and/or its children?
You need to trigger the event on the element that was modified.
http://jsfiddle.net/Gw4Lj/2/
return oFct.apply(sender, args), sender.trigger('contentChange');
however, with that change, you will no longer catch the event that was triggered on an element that isn't connected to the DOM because it isn't a descendant of that document, which is ok in my opinion because it isn't associated to that DOM, it's in a DOM Fragment.
I come with slightly modified version wich seems to work fine for the purpose i reach.
Need optimization for the .on() method extend, so please feel free to share your feedbacks.
Inspired from here: https://groups.google.com/forum/?fromgroups=#!topic/jquery-dev/ZaMw2XB6wyM
Thanks to Wil Stuckey
Here jsFiddle
;(function ($) {
var fctsToObserve = {
append: [$.fn.append, 'self'],
prepend: [$.fn.prepend, 'self'],
remove: [$.fn.remove, 'parent'],
before: [$.fn.before, 'parent'],
after: [$.fn.after, 'parent']
}, fctsObserveKeys = '';
$.each(fctsToObserve, function (key, element) {
fctsObserveKeys += "hasChanged." + key + " ";
});
var oOn = $.fn.on;
$.fn.on = function () {
if (arguments[0].indexOf('hasChanged') != -1) arguments[0] += " " + fctsObserveKeys;
return oOn.apply(this, arguments);
};
$.fn.hasChanged = function (types, data, fn) {
return this.on(fctsObserveKeys, types, null, data, fn);
};
$.extend($, {
observeMethods: function (namespace) {
var namespace = namespace ? "." + namespace : "";
var _len = $.fn.length;
delete $.fn.length;
$.each(fctsToObserve, function (key) {
var _pre = this;
$.fn[key] = function () {
var target = _pre[1] === 'self' ? this : this.parent(),
ret = _pre[0].apply(this, arguments);
target.trigger("hasChanged." + key + namespace, arguments);
return ret;
};
});
$.fn.length = _len;
}
});
$.observeMethods()
})(jQuery);

knockout js bind with datetimepicker gives an exception

I think I can easily bind a date data with jquery ui calendar and knockout.js thanks to this answer.
Now I need to bind a date data as well as its time. Of course, I can use timepicker. But I am not sure how I can bind its data with knockout.js. I expected it'd be similar to datepicker so I made following script
ko.bindingHandlers.datetimepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datetimepickerOptions || {};
$(element).datetimepicker(options);
//handle the field changing
ko.utils.registerEventHandler(element, "change", function () {
var observable = valueAccessor();
observable($(element).datetimepicker("getDate"));//****
});
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).datetimepicker("destroy");
});
},
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()),
current = $(element).datetimepicker("getDate");
if (value - current !== 0) {
$(element).datetimepicker("setDate", value);
}
}
};
But when I ran the script, I get an error in line of //**** in javascript
TypeError: observable is not a function
But I can't find what I did wrong here.
This particular error is due to the observable = valueAccessor() line. You are assigning to observable the value of valueAccessor by adding the () to the end. In order to pass a value to observable in this way, you would need to write instead:
var observable = valueAccessor;
Otherwise, observable is not an 'observable function'.
I just found following code is working. As few open source code do, this addon is not very stable and will call change event with null observable sometimes. So I made the code to catch the exception and move on.
ko.bindingHandlers.datetimepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datetimepickerOptions || {};
$(element).datetimepicker(options);
//handle the field changing
ko.utils.registerEventHandler(element, "change", function () {
var observable = valueAccessor();
try {
observable($(element).datetimepicker("getDate"));//****
}
catch(ex) {}
});
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).datetimepicker("destroy");
});
},
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()),
current = $(element).datetimepicker("getDate");
if (value - current !== 0) {
$(element).datetimepicker("setDate", value);
}
}
};
Replace this line
var observable = valueAccessor();
With
var xxxx= valueAccessor();
Because you cannot use the observable, because it is reserved keyword in knockout.
Also, you may get error somewhere in future if you use observable as variable name.

Categories

Resources