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()));
}
};
Related
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/
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'm trying to make a table component in Knockout that can render arbitrary columns in a way that still uses Knockout for cell contents. I'm passing an array of column definition objects into the component and an array of arbitrary row objects. Then, I have a nested foreach structure that looks a bit like this:
<tbody data-bind="foreach: {data:rows, as:'row'}">
<tr data-bind="foreach: $parent.columns">
<td data-bind="html:renderCell(row)"></td>
</tr>
</tbody>
With this, I can allow the 'renderCell' function for each column to return some html to go in the cell, given the context of the row viewModel.
However, what I really want is to be able to return a Knockout template for the cell. I don't want to use <script type="text/html" id="xyz"> style templates because it doesn't suit this particular application, but I can't figure out how to get Knockout to treat the output from renderCell() as a template string.
How can I do something like the following and make it work?
<td data-bind="template:{fn: renderCell(row)}"></td>
I'd like to use other components like and other bindings in the output of the renderCell function.
So as I understand it, you want a custom template for each cell. This template is to be based on the information coming into the binding. The closet thing I can think of to let you do this is a custom binding handler:
ko.bindingHandlers.yourBindingName = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// This will be called when the binding is first applied to an element
// Set up any initial state, event handlers, etc. here
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// This will be called once when the binding is first applied to an element,
// and again whenever any observables/computeds that are accessed change
// Update the DOM element based on the supplied values here.
}
};
So that's the basic one from the knockout documentation. I'm guessing in the init function you can then select some html you wish to display:
function customTemplateOne(dataToBind) {
var spanEl = document.createElement('span');
spanEl.innerHTML = dataToBind;
return spanEl;
}
You can have a whole bunch of different functions to define different templates.
In your init you can do this:
var template = "";
switch (valueToDetermineTemplateChange)
{
case "useThis":
template = customTemplateOne(dataToBind);
}
Or you can take advantage of JavaScripts key value stuff.
var templates = {
useThis: function () {}
}
var template = templates[valueToDetermineTemplateChange]();
In order to do custom options you can do this:
ko.bindingHandlers.yourBindingName = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// This will be called when the binding is first applied to an element
// Set up any initial state, event handlers, etc. here
var options = {};
ko.extend(options, ko.bindingHandlers.yourBindingName); // get the global options
ko.extend(options, ko.unwrap(valueAccessor())); // merge with the local options
// so if you have a data property on the options which holds the ko binding you can do this:
var data = ko.unwrap(options.data);
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// This will be called once when the binding is first applied to an element,
// and again whenever any observables/computeds that are accessed change
// Update the DOM element based on the supplied values here.
},
options: {
customOption: "Some Option"
}
};
<div data-bind="yourBindingName: { data: someValue }"></div>
Instead of the applyBindingsToDescendants function, you make your init a wrap of other bindings:
ko.applyBindingsToNode(element, { html: valueAccessor() }, context); // the html is a binding handler, you can specify with, text, foreach....
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!
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.