Dynamic template handing with Knockout.js - javascript

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....

Related

Select 2 deferred Loading data with knockout

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()));
}
};

How to pass the binding context to custom handler call?

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!

KnockoutJS Validation binding value passed to custom binding?

Using KnockoutJS-Validation, I need to pass to a custom binding whether the field passed validation or not. Guessing I need to somehow hook into a KnockoutJS-Validation observable at field level using the allBindingsAccessor parameter but unsure how.
ko.bindingHandlers.mycustombinding = {
update: function (element, valueAccessor, allBindingsAccessor) {
allBindings = allBindingsAccessor(),
validationObservable = allBindings.validationObservable;
if (!validationObservable) {
//do cool jQuery stuff to the element if it doesn't validate
}
}
};
http://jsfiddle.net/hotdiggity/mtwLA/6/
The library adds a obserabler isValid to the observable thats extended
http://jsfiddle.net/MCNU8/
var observable = ko.observable("f").extend({ number: true });
console.log(observable.isValid());

Knockoutjs if/shim binding

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>

Knockout.js: array parameter in custom binding

i try to write a custom list-binding. This is what i have so far:
var myArr = ko.observableArray();
myArr.push("foo");
myArr.push("bar");
var view = {
matches: myArr
}
ko.bindingHandlers.matchList = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
// gives me 0
console.log(valueAccessor().length);
// gives me 2
console.log(valueAccessor()().length);
},
};
// Activates knockout.js
ko.applyBindings(view);
My html binding looks as follow:
<div data-bind="matchList: matches"></div>
Why do i have to use the second pair of parentheses to get into my array?
The valueAccessor is a function that returns what was passed to the binding. It is wrapped in a function, so that it is not evaluated immediately.
A typical pattern would be to do:
var value = ko.utils.unwrapObservable(valueAccessor());
ko.utils.unwrapObservable will safely handle both observables and non-observables and return the value. So, if it is an observable, then it will return yourValue(), otherwise it will just return yourValue. This allows your binding to support binding against either observables or plain properties.
In addition, some bindings need to deal with the observable itself and some bindings need to deal with the value of the observable. So, the observable is not unwrapped, by default. So, valueAccessor() returns your observable (which is a function) and then it is up to you to decide if you want to unwrap it to get the value or possibly set the value of it.
I think the confusing thing here is that the valueAccessor passed to init is different from the parameter of the same name passed to update. In init, it's a function that returns functions that in turn returns your array. Check out this sample code from their documentation. I added two console logs at the end that should show you the function that valueAccessor() returns:
var myArr = ko.observableArray();
myArr.push("foo");
myArr.push("bar");
var view = {
matches: myArr
}
ko.bindingHandlers.matchList = {
init: function(element, valueAccessor, allBindingsAccessor, viewModel) {
var value = ko.utils.unwrapObservable(valueAccessor()); // Get the current value of the current property we're bound to
$(element).toggle(value); // jQuery will hide/show the element depending on whether "value" or true or false
console.log(value);
console.log(valueAccessor().toString());
}
};
// Activates knockout.js
ko.applyBindings(view);

Categories

Resources