ach
ach

Reputation: 6234

Augment the array item context in Knockout

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:

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

Answers (2)

ach
ach

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

woz
woz

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

Related Questions