I'm using Knockout custom handlers to update my objects like this:
ko.bindingHandlers.Tile = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var $element = $(element);
var $parent = bindingContext.$parent || viewModel.$parent; // TODO: Clean up this mess
/*
Do lot of things using bindingContext.$parent, calculate elements dimension, populate fields etc.
*/
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
$(element).show();
}
};
ko.bindingHandlers.TileColumn = {
/* another custom handler, we got a lot of those things */
}
Now I implemented a Resize function and I can reuse all that weird logic in the custom handler to redraw/resize my elements.
var baseResize = this.Resize;
this.Resize = function () {
baseResize.call(this);
var tiles = this.Element.find('.tile');
var tilesVM = this.DataSource.Items;
for (var i = 0x0; i < tiles.length; i++) {
tilesVM[i].$parent = this.ViewModel; // Clean up this mess too
ko.applyBindingsToNode(tiles[i], null, tilesVM[i]);
}
}
This works but feels like it's not the right way to do it. I'm struggling to refactor that handlers and get out of it the resize logic: not sure if this is the right way to do it.
While we wait for that refactoring I want a better way to call the applyBindings passing the correct bindingContext.
Note I did a very ugly thing here:
var $parent = bindingContext.$parent || viewModel.$parent; // TODO: Clean up this mess
And here:
tilesVM[i].$parent = this.ViewModel; // Clean up this mess too
Because the bindingContext parameter does not contain the parent view model when I explicit call the custom handler.
Of course that handler works just fine without that ugly messy line when the component loads and knockout is doing all the magic behind the curtain.
Robert, thanks for your time.
That documentation link does not helped since I got 3 levels: TileParent, Tile and TileChilds and I want to only call the Tile level and I cannot see a way to do it without applyBindingsToNode.
But that pointed me out to the binding documentation. After reading this blog and digging in the ko source code I realized one wonderfull thing: I can pass a ViewModel OR a BindingContext as parameter.
At first I was sad I cannot pass both at the same time but I see the VM is inside the BC $data.
Finally I found this usefull ko.contextFor(element)
for (var i = 0x0; i < tiles.length; i++) {
var bindingContext = ko.contextFor(tiles[i]);
ko.applyBindingsToNode(tiles[i], null, bindingContext);
}
and I also cleaned my handler callback
var $parent = bindingContext.$parent;
Everything still working, but this time a bit more cleaner!
Related
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.
I've a knockout.js view which shows 20 rows of data. each row has a select 2 control bound with knockout.js. (Below you can see my bindinghandler)
Now each select 2 points to the same array of items . This array has about 10.000 entries. This results in a slowdown of the whole page (about 2-3 seconds freezetime)
I'm thinking about to only load the options when the user clicks the row. like this:
self.setSelectedRow = function (entry) {
entry.options(allOptions);
var value = entry.intialValue;
entry.StationdId(value);
};
After this the select 2 is expandable and i can choose options, but the initialvalue is not applied.
Any hints on what i'm doing wrong?
Binding handler:
ko.bindingHandlers.select2 = {
init: function (el, valueAccessor, allBindingsAccessor, viewModel) {
ko.utils.domNodeDisposal.addDisposeCallback(el, function () {
$(el).select2('destroy');
});
var allBindings = allBindingsAccessor(),
select2 = ko.utils.unwrapObservable(allBindings.select2);
$(el).select2(select2);
}
};
Binding handlers usually have two functions:
An init function that is called when the binding is created (note that it can be called more than once as it is called each time you create/recreate the binding -- example: when node is in an if binding). This function should contain the code to setup the binding (which ou did well)
An update function which is called every time the observables inside your binding markup change. Note that this function is also called on init (right after the init function) so in certain cases you won't need an init function.
More info in the custom binding doc.
In your case, I think the init function is fine.
The problem is nothing is set up to handle the changes on your observables.
You can add an update function that would look like this (untested):
ko.bindingHandlers.select2 = {
init: function (el, valueAccessor, allBindingsAccessor, viewModel) {
/* your code is fine */
},
update: function (el, valueAccessor, allBindingsAccessor, viewModel) {
var allBindings = allBindingsAccessor(),
select2 = ko.utils.unwrapObservable(allBindings.select2);
$(el).select2(select2); //update the select2
}
};
Unless you are using a very outdated knockout version, I think your binding syntax is wrong.
This part is wrong:
var allBindings = allBindingsAccessor(),
select2 = ko.utils.unwrapObservable(allBindings.select2);
If you read http://knockoutjs.com/documentation/custom-bindings.html
the correct way to use allBindingsAccessor (it should be named allBindings anyway) is
var select2 = allBindingsAccessor.get('select2') || {};
BUT even this is unnecessary, valueAccessor gives you what is under current binding (select2).
So just try this:
ko.bindingHandlers.select2 = {
init: function (el, valueAccessor, allBindingsAccessor, viewModel) {
ko.utils.domNodeDisposal.addDisposeCallback(el, function () {
$(el).select2('destroy');
});
$(el).select2(ko.unwrap(valueAccessor()));
}
};
I'm trying to get my custom binding to work with both observables and plain objects. I followed the answer in this question:
writeValueToProperty isn't available
However, if I look at the object returned if I execute the allBindingsAccessor, the property '_ko_property_writers' is undefined.
Does anyone know if this has changed at all in version 3 of knockout?
edit
Sorry I should have stated, I am trying to 'write' the value back to the model, in an observable agnostic way
This was helpful for me:
ko.expressionRewriting.twoWayBindings.numericValue = true;
ko.bindingHandlers.numericValue = {
...
}
It is defined after specifying binding as two-way.
So I can use something like that inside my custom binding:
ko.expressionRewriting.writeValueToProperty(underlying, allBindingsAccessor, 'numericValue', parseFloat(value));
writeValueToProperty is defined internally as:
writeValueToProperty: function(property, allBindings, key, value, checkIfDifferent) {
if (!property || !ko.isObservable(property)) {
var propWriters = allBindings.get('_ko_property_writers');
if (propWriters && propWriters[key])
propWriters[key](value);
} else if (ko.isWriteableObservable(property) && (!checkIfDifferent || property.peek() !== value)) {
property(value);
}
}
The standard way to do this is with ko.unwrap as described here: http://knockoutjs.com/documentation/custom-bindings.html
For example:
ko.bindingHandlers.slideVisible = {
update: function(element, valueAccessor, allBindings) {
// First get the latest data that we're bound to
var value = valueAccessor();
// Next, whether or not the supplied model property is observable, get its current value
var valueUnwrapped = ko.unwrap(value);
// Grab some more data from another binding property
var duration = allBindings.get('slideDuration') || 400; // 400ms is default duration unless otherwise specified
// Now manipulate the DOM element
if (valueUnwrapped == true)
$(element).slideDown(duration); // Make the element visible
else
$(element).slideUp(duration); // Make the element invisible
}
};
In that example valueUnwrapped is correct whether the user bound to an observable or a normal object.
I want to create a custom binding that behaves like the if binding, but instead of removing the element entirely, it replaces it with another element of the same height whenever it should be removed.
I'm struggling to find a way of doing this that isn't hacky. I don't know enough about the internals of knockout to go about this in an educated way.
Any pointers greatly appreciated.
Thanks!
You could write your own binding:
ko.bindingHandlers.shim = {
update: function(element, valueAccessor, allBindings) {
// First get the latest data that we're bound to
var value = valueAccessor();
// Next, whether or not the supplied model property is observable, get its current value
var shim = ko.unwrap(value);
if (shim) {
var shimEl = $(element).data('shim');
// Create the shim element if not created yet
if (!shimEl) {
shimEl = $('<div />').addClass('shim').appendTo(element);
// Equal the height of the elements
shimEl.height($(element).height());
$(element).data('shim', shimEl);
}
shimEl.show();
} else {
var shimEl = $(element).data('shim');
if (shimEl) {
shimEl.hide();
}
}
// You can also trigger the if-binding at this point
// ko.bindingHandlers.if.update(element, valueAccessor, allBindings);
}
};
Then use it like this:
<div data-bind="shim: [condition]"></div>
I`m attempting to bind an observable array of people two a two column responsive layout with click events using knockoutjs.
I have created a custom binding called TwoCol that loops through the array, and appends nodes to the DOM to create my suggested layout, but the click events are giving me trouble when I try to apply them in a custom binding nested in a loop.
I have played with it quite a bit, and encountered all types of results, but where I`m at now is calling my ~click~ event during binding, rather than on click.
http://jsfiddle.net/5SPVm/6/
HTML:
<div data-bind="TwoCol: Friends" id="" style="padding: 20px">
JAVASCRIPT:
function FriendsModel() {
var self = this;
this.Friends = ko.observableArray();
this.SelectedFriend = "";
this.SetSelected = function (person) {
alert(person);
self.SelectedFriend = person;
}
}
function isOdd(num) {
return num % 2;
}
ko.bindingHandlers.TwoCol = {
update: function (elem, valueAccessor) {
var i = 0;
var rowDiv;
var vFriends = ko.utils.unwrapObservable(valueAccessor());
$(elem).html('');
while (i < vFriends.length) {
//create row container every other iteration
if (!isOdd(i)) {
rowDiv = document.createElement("div");
$(rowDiv).addClass("row-fluid");
elem.appendChild(rowDiv);
}
//add column for every iteration
var colDiv = document.createElement("div");
$(colDiv).addClass("span6");
rowDiv.appendChild(colDiv);
//actual code has fairly complex button html here
var htmlDiv = document.createElement("div");
var htmlButton = vFriends[i]
htmlDiv.innerHTML = htmlButton;
colDiv.appendChild(htmlDiv);
//i think i need to add the event to the template too?
//$(htmlDiv).attr("data-bind", "click: { alert: $data }")
//it seems that the SetSelected Method is called while looping
ko.applyBindingsToDescendants(htmlDiv, { click: friends.SetSelected(vFriends[i]) });
i++;
}
return { controlsDescendantBindings: true };
}
}
var friends = new FriendsModel();
friends.Friends.push('bob');
friends.Friends.push('rob');
friends.Friends.push('mob');
friends.Friends.push('lob');
ko.applyBindings(friends);
I don't think you're using ko.applyBindingsToDescendants correctly. I admit I'm a little confused as to the meaning of some of the values in your code, so I may have interpreted something incorrectly.
Here's a fiddle where I think it's working the way you intended:
http://jsfiddle.net/5SPVm/7/
http://jsfiddle.net/5SPVm/8/
Notice if manually control descendant bindings (return { controlsDescendantBindings: true };), you need to set that up in the init callback, instead of update. The update callback is too late for that.
Quick rundown of the changes (edited):
Moved the controlsDescendantBindings into the init binding callback
Added the necessary parameter names to the binding param list to access additional values.
I re-enabled the html.attr call. Notice that now, because the binding context is set to the actual item, the SetSelected method doesn't exist at that level anymore, so it is necessary to use $parent.SetSelected.
$(htmlDiv).attr("data-bind", "click: $parent.SetSelected")
Fixed the ko.applyBindingsToDescendants call. This method takes a binding context, which is created from the current binding context, and also takes the element to apply the binding to. You don't want to reapply the binding, which is why this whole thing needs to be in the init handler.
var childBindingContext = bindingContext.createChildContext(vFriends[i]);
ko.applyBindingsToDescendants(childBindingContext, colDiv);