Reputation: 6234
Currently, when using a Knockout foreach
binding you can access the current index with $index
. I'd like to make other, similar functionality available to my inner bindings -- for example:
array
(lets me access the array being operated on)length
(length of said array)first
(whether or not the current item is the first item)last
(whether or not the current item is the last item)only
(whether or not the current item is the only item)You get the idea. Unfortunately the code that sets $index
is buried deep within the code for the template
binding with no apparent way to augment the context.
I'm able to get access to array
and length
via custom foreach
binding that extends the bindingContext
(I know there are caveats to this re: destroy
but it works for me), but I can't figure out how to implement the other methods that require access to the "current" item without a custom inner binding that is executed for each array iteration.
I'd like to be able to do something like this:
<div data-bind="foreach: items">
<input type="text" data-bind="value: description" />
<button data-bind="visible: $last, click: $array.push({})">Add Another</button>
</div>
(As we know, neither $array
nor $last
exist). Assume that the button
element could be coming from an external template with no way to know how to path to the current array (so $parent.items.push
won't work for me).
Is there a way to do this?
Upvotes: 1
Views: 419
Reputation: 6234
The best I could come up with was to create specialized bindings that store context info about the array and current item.
Custom foreach
binding that exposes context about the array:
ko.bindingHandlers.xforeach = (function() {
var createContext = function(array) {
return {
'$array': array,
'$arrayLength': function() { return ko.unwrap(array).length; }
};
};
return {
init: ko.bindingHandlers.foreach.init,
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var extendedContext = createContext(valueAccessor());
return ko.bindingHandlers.foreach.update.call(this, element, valueAccessor, allBindings, viewModel, bindingContext.extend({
'$foreachContext' : extendedContext
}));
}
};
})();
ko.virtualElements.allowedBindings.xforeach = true;
Custom template
binding that exposes context about the item/array:
ko.bindingHandlers.xforeachItem = (function() {
var createContext = function(currentContext, forEachContext) {
return {
first: function() { return currentContext.$index() === 0; },
last: function() { return currentContext.$index() === (forEachContext.$arrayLength() - 1); }
};
};
return {
init: ko.bindingHandlers.template.init,
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var extendedContext = createContext(bindingContext, bindingContext.$parentContext.$foreachContext);
return ko.bindingHandlers.template.update.call(this, element, valueAccessor, allBindings, viewModel, bindingContext.extend({
'$foreachItemContext' : extendedContext
}));
}
};
})();
ko.virtualElements.allowedBindings.xforeachItem = true;
Example usage:
<div data-bind="xforeach: items">
<div data-bind="xforeachItem: {}">
<input type="text" data-bind="value: description" />
<span data-bind="visible: $foreachItemContext.first(), text: $foreachContext.$arrayLength()"></span>
<button data-bind="visible: $foreachItemContext.last(), click: $root.add">Add Another</button>
</div>
</div>
And finally, a fiddle showing it in action: http://jsfiddle.net/magnafides/wkCLd/2/
Upvotes: 1
Reputation: 10994
You can use $parent.data() to get the actual array.
For $length
, you can do this:
$parent.items.length
For $first
, you can do this:
$index() == 0
For $last
, you can do this:
$index() == ($parent.items.length - 1)
For $only
, you can do this:
$index() == 0 && $parent.items.length == 1
Here is the fiddle.
Upvotes: 0