Skip to content
This repository

added $index value in the binding context of foreach templates. #182

Merged
merged 14 commits into from about 2 years ago

9 participants

Mark Bradley Ryan Niemeyer Steven Sanderson Simon Bartlett pilavdzic while0pass James Foster denis Michael Best
Mark Bradley

There is probably a much better way to accomplish this. Code review required

Ryan Niemeyer
Collaborator

I had been thinking a little about $index and I believe that it would be useful to make $index an observable.

Currently, your method adds $index, but does not adjust it when items are shifted. So, if it was adjusted and if $index was an observable, then if a binding does use it, it would get re-evaluated on a change.

Additionally, when I tried your changes, I had to add an index parameter to activateBindingsCallback and pass it to the call to createInnerBindingContext in that function.

Mark Bradley

You are probably right. I will need to think a bit harder about how to make that work (and some more tests).

The test I added works without your change, but that could be because the dummyTemplateEngine is implemented slightly differently to a normal template engine (I found some funny behaviour in the memoization functionality, took me a while of stepping through the code to see what was going on). I updated the branch to reflect the added parameter.

Mark Bradley

I tried to get observable indices working, however I am still getting a bug (the new test case fails on the 3rd assertion) that I can't seem to figure out. Can you take a look at it ryan?

Ryan Niemeyer
Collaborator

Will try to help with this one, as soon as I get a chance.

I kind of have mixed feelings on this functionality though. In most cases, it is not needed. I wonder if this is too much work/complexity/overhead, when people can easily attach an index to their observableArray using the subscribe technique that I usually recommend and in a dependentObservable (filtered array) they can attach an index themselves. There are downsides to that way though (adds properties to your items, doesn't work on arrays of primitives).

I suppose there could be an option to turn index tracking on/off in the template binding.

not sure if Steve can think of an easier way to provide this functionality

Mark Bradley

I think the main problem is that native templates are perceived as a replacement for jQuery templates. Since jQuery templates support an index parameter in their looping construct, not having one makes it appear as if native templates are incomplete in comparison. Undoubtedly this was probably a feature that some users used so they will likely complain. We are already starting to see this coming up on the mailing list more often.

Mark Bradley

The most recent commit fixes the logic error that was in the patch. However the testsuite still does not work.

After a lot more debugging I got fed up with the testsuite and tried to use the code I had already written in a simple test case. Funnily enough it does work with the native template engine. The problem appears to be a disparity between the native template engine and the dummyTemplateEngine used in the testsuite.

Mark Bradley

most notably using strings internally causes the "nodes" of the dummyTemplateEngine to be immutable, whereas with real template engines that work with nodes, the children are mutable. We can either fix the dummyTemplateEngine to use something other than strings internally or we can change the $index tactic such that when "retaining" nodes, it will perform a callback that can update sub-nodes.

Ryan Niemeyer
Collaborator

Glad that you got it working. It does seem like a feature that many people want. Will be interested to see what Steve thinks about this approach.

Mark Bradley finally cracked it. the problem was duplicate context creation.
it is also necessary to perform double buffering of the stored contexts such
that we do not overwrite new nodes or move new nodes by mistake.
c8a3839
Mark Bradley

So I found the problem. There are two calls to createInnerBindingContext in ko.renderTemplateForEach which is where I was creating the $index observable.

Deduplicating this with a contexts buffer that can then be updated with new contexts and moved contexts via double buffering solves all the problems with the test suite. Give it a go.

Steven Sanderson

Wow, excellent. Nice work with this.

I haven't yet looked through the implementation in detail, but in principle I agree it would be a useful feature to add.

Is it OK if I schedule this for inclusion in 1.3.1? I'm trying to avoid all new features for 1.3.0 now so that the release can be completed in a finite amount of time :)

Mark Bradley

1.3.1 is fine. I am surprised this update isn't being called 2.0 considering the amount of work going into it.

Simon Bartlett

If semantic versioning were used it would be a 2.0 release, as this version of Knockout has breaking changes (particularly in regards to template engines, and the requirement on latest version of jquery templates).

Mark Bradley Merge branch 'master' into foreach-context-index
* master:
  More indentation tweaks for tidiness
  Update the build
  Minor indentation tweaks
  Stylistic tweaks to previous commit
  Eliminate redundant IE6/7 workarounds for radio/checkbox issues. These problems no longer apply now that bindings are always applied to elements after they've been put into the DOM. Fixes issue #169
  Refactor bad code
  minor fix
  Fix render array with \"undefined\" and \"null\" items in \"foreach\"  template
  Updated remove and removeAll to modify their underlying arrays rather than creating new arrays. This makes them consistent with the rest of the array write functions. Added tests to verify original array is modified. Also added test to verify there is no notification when nothing is removed.
  changing IE detection to not rely on user agents
  Fixed a variable which should not have been global.
d190e51
pilavdzic

I am looking forward to 1.3.1 where this feature will be added.

It is very useful in places where the order of your list matters, for example.

Also, if you are working with legacy jquery templates that called custom functions that used index and you are now upgrading to ko built in templates but don't want to re-write a lot of code so you don't introduce bugs ;)

Finally, when there are nested arrays, it is lots of extra work to attach dependantobservables to each child array every time a new one is created to just implement this yourself.

I think it's GREAT idea to include in knockout.. it has been requested by many people. Sure there are other ways of doing it, but they are all much more complex and this makes things much easier for newer folks.

Thanks to everyone for this patch. I am glad it's coming and can't wait... Until then I have to write all this extra code now to workaround the issues I have currently.

Mark Bradley

one of the limitations of the current patch is if you have nested foreach templates then it will be difficult to create a reference to the outer $index from the inner foreach. (jQuery templates didn't have this problem because you bound a custom name to the index in the loop). I might put some effort into adding a parameter to the template options that allow you to specify a variable name for this use case.

while0pass

Besides $index variable in templates it would be also great to have variables like $isLast and $isFirst. It is also usefull to have a reference to the parent loop. In django templates all those variables are composed within a namespace "forloop" that is accessible within every for loop as a variable:

https://docs.djangoproject.com/en/dev/ref/templates/builtins/#for

I suppose all those variables are rather handy to have: 4 versions of index counter (0-based, 1-based, and two reversed ones), first and last items booleans and parent loop reference. for instance:

$forloop.index0
$forloop.index1
$forloop.revIndex0
$forloop.revIndex1
$forloop.isLast
$forloop.isFirst
$forloop.parentLoop

or smth like this.

Mark Bradley

I did wonder why the binding context has the parent data item as the $parent context variable, but the parent binding context isn't available (this would essentially give you the parent loop or with or other context creating template binding).

Every variable listed there (except for the 0-index and the parent context) is computable from the given list and the current index so I don't see a reason to add them to the framework merely for convenience. Perhaps as some utility functions to make it less verbose. Really I just feel that pre-computing these values isn't worth it if 99% of the time they aren't going to be used.

while0pass

Every variable listed there (except for the 0-index and the parent context)
is computable from the given list and the current index so I don't see a reason
to add them to the framework merely for convenience.

I suppose a reason is that this set of variables covers 99% of programmers' use cases with loops. So it is more than convenience, it is pragmatics. Otherwise, $index can be regarded just as a convenient feature as well. Of course, it is not, it is not only that kind of feature. But I agree that most of them are implemented much more easy if we have $index implemented than if we have not.

Mark Bradley

Actually I withdraw my assertion that we would have to pre-compute the other variables. Since they can be calculated from the one or two essential variables then we can provide them as dependent observables that defer evaluation.

added some commits December 22, 2011
Merge branch 'master' into foreach-context-index
Conflicts:
	spec/templatingBehaviors.js
	src/binding/editDetection/arrayToDomNodeChildren.js
	src/templating/templating.js
0535189
Merge branch 'master' into foreach-context-index 9e91edb
James Foster

What is the status of this feature?

denis

errors of the current solution:
1. $index not applied to child contexts
2. $index not recalculated when one of child element was deleted

Steven Sanderson

What is the status of this feature?

Still keen to have this in the next point release (note that v1.3.1, which this was originally estimated for, is the same as v2.1.0 in the new numbering scheme).

errors of the current solution

Thanks for pointing that out - we will need to resolve that before this can become part of the master branch. I need to check the current suggested implementation carefully, as I think there will be some refactoring needed.

Michael Best
Collaborator

$index not applied to child contexts

One of the changes I made in #290 is to copy custom properties to the child context, which would fix this problem.

added some commits February 22, 2012
Michael Best Merge barkmadley:foreach-context-index into 182-foreach-index
Conflicts:
	spec/templatingBehaviors.js
	src/templating/templating.js
37ffbac
Michael Best Continuation of #182 - Simplify by having setDomNodeChildrenFromArray…
…Mapping manage the observable index.
2c3b918
Michael Best
Collaborator

Currently one of the new specs fails in IE because IE strips out some spaces. It's "Data binding 'foreach' option should update bindings that reference an $index if the list changes".

added some commits February 25, 2012
Michael Best foreach $index: fix spec that failed in IE; help performance a bit
renderTemplateForEach now assumes that setDomNodeChildrenFromArrayMapping will call the mapping callback and then the afterAdding callback once for each new or changed item. Modify specs to verify this behavior.
3146a3a
Michael Best Extend the specs for setDomNodeChildrenFromArrayMapping to include re…
…placing a value through an observable item.
35704d9
denis
vamp commented March 04, 2012

what about availability $array and $length variables context?

Steven Sanderson
Owner

what about availability $array and $length variables context?

You can get this information already using $parent.someArray and $parent.someArray.length

denis
vamp commented March 05, 2012

Steve, in this case I need to know variable name of parent context (it breaks creating of reusable bindings)...

denis

what about:

        arrayItemContext['$array'] = arrayOrObservableArray;
        arrayItemContext['$length'] = length;

where (shared for all child contexts, outside executeTemplateForArrayItem)

    var length = ko.dependentObservable(function(){
        return (ko.utils.unwrapObservable(arrayOrObservableArray) || []).length;
    }, null, {'disposeWhenNodeIsRemoved': targetNode});
Ryan Niemeyer
Collaborator

Took another look at this one. Functionality works well and the simplified implementation is pretty straightforward. I do kind of wish that it was opt-in, so we can avoid creating an extra observable for each item when $index is not needed, although the performance overhead is likely minimal.

Steven Sanderson SteveSanderson merged commit f9db930 into from March 23, 2012
Steven Sanderson SteveSanderson closed this March 23, 2012
Steven Sanderson
Owner

Fantastic - this looks great. Thanks very much!

About perf, my quick foreach stress testing didn't show any significant extra cost to maintaining the $index observable. It was a lot slower if I actually referenced $index in my view (not surprising, because of course then a lot more of the view has to be redrawn whenever you mutate your array). But anyone who doesn't make use of $index shouldn't see any noticeable degradation of performance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 14 unique commits by 3 authors.

Oct 22, 2011
Mark Bradley added $index value in the binding context of foreach templates. ad5beb8
Mark Bradley forgot this function also requires the index. 3bb1adc
Oct 23, 2011
Mark Bradley new test, this one makes sure that the indices update as the relevant…
… nodes move around.
8de3398
Mark Bradley first guess at how to support modifying observable indices. 9be31b2
Mark Bradley comments. 0076b3c
Oct 24, 2011
Mark Bradley we need to create the new indices by tracking added and retained node…
…s, using i is insufficient.
23dfe19
Oct 25, 2011
Mark Bradley finally cracked it. the problem was duplicate context creation.
it is also necessary to perform double buffering of the stored contexts such
that we do not overwrite new nodes or move new nodes by mistake.
c8a3839
Oct 27, 2011
Mark Bradley Merge branch 'master' into foreach-context-index
* master:
  More indentation tweaks for tidiness
  Update the build
  Minor indentation tweaks
  Stylistic tweaks to previous commit
  Eliminate redundant IE6/7 workarounds for radio/checkbox issues. These problems no longer apply now that bindings are always applied to elements after they've been put into the DOM. Fixes issue #169
  Refactor bad code
  minor fix
  Fix render array with \"undefined\" and \"null\" items in \"foreach\"  template
  Updated remove and removeAll to modify their underlying arrays rather than creating new arrays. This makes them consistent with the rest of the array write functions. Added tests to verify original array is modified. Also added test to verify there is no notification when nothing is removed.
  changing IE detection to not rely on user agents
  Fixed a variable which should not have been global.
d190e51
Dec 22, 2011
Merge branch 'master' into foreach-context-index
Conflicts:
	spec/templatingBehaviors.js
	src/binding/editDetection/arrayToDomNodeChildren.js
	src/templating/templating.js
0535189
Merge branch 'master' into foreach-context-index 9e91edb
Feb 22, 2012
Michael Best Merge barkmadley:foreach-context-index into 182-foreach-index
Conflicts:
	spec/templatingBehaviors.js
	src/templating/templating.js
37ffbac
Michael Best Continuation of #182 - Simplify by having setDomNodeChildrenFromArray…
…Mapping manage the observable index.
2c3b918
Feb 25, 2012
Michael Best foreach $index: fix spec that failed in IE; help performance a bit
renderTemplateForEach now assumes that setDomNodeChildrenFromArrayMapping will call the mapping callback and then the afterAdding callback once for each new or changed item. Modify specs to verify this behavior.
3146a3a
Michael Best Extend the specs for setDomNodeChildrenFromArrayMapping to include re…
…placing a value through an observable item.
35704d9
This page is out of date. Refresh to see the latest.
40  spec/editDetectionBehaviors.js
@@ -62,7 +62,12 @@ describe('Compare Arrays', {
62 62
 
63 63
 describe('Array to DOM node children mapping', {
64 64
     before_each: function () {
  65
+        var existingNode = document.getElementById("testNode");
  66
+        if (existingNode != null)
  67
+            existingNode.parentNode.removeChild(existingNode);
65 68
         testNode = document.createElement("div");
  69
+        testNode.id = "testNode";
  70
+        document.body.appendChild(testNode);
66 71
     },
67 72
 
68 73
     'Should populate the DOM node by mapping array elements': function () {
@@ -154,31 +159,46 @@ describe('Array to DOM node children mapping', {
154 159
     },
155 160
 
156 161
     'Should handle sequences of mixed insertions and deletions': function () {
157  
-        var mappingInvocations = [];
  162
+        var mappingInvocations = [], countCallbackInvocations = 0;
158 163
         var mapping = function (arrayItem) {
159 164
             mappingInvocations.push(arrayItem);
160 165
             var output = document.createElement("DIV");
161  
-            output.innerHTML = arrayItem || "null";
  166
+            output.innerHTML = ko.utils.unwrapObservable(arrayItem) || "null";
162 167
             return [output];
163 168
         };
  169
+        var callback = function(arrayItem, nodes) {
  170
+            ++countCallbackInvocations;
  171
+            value_of(mappingInvocations[mappingInvocations.length-1]).should_be(arrayItem);
  172
+        }
164 173
 
165  
-        ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A"], mapping);
  174
+        ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A"], mapping, null, callback);
166 175
         value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["A"]);
167 176
         value_of(mappingInvocations).should_be(["A"]);
  177
+        value_of(countCallbackInvocations).should_be(mappingInvocations.length);
168 178
 
169  
-        mappingInvocations = [];
170  
-        ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["B"], mapping); // Delete and replace single item
  179
+        mappingInvocations = [], countCallbackInvocations = 0;
  180
+        ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["B"], mapping, null, callback); // Delete and replace single item
171 181
         value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["B"]);
172 182
         value_of(mappingInvocations).should_be(["B"]);
  183
+        value_of(countCallbackInvocations).should_be(mappingInvocations.length);
173 184
 
174  
-        mappingInvocations = [];
175  
-        ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A", "B", "C"], mapping); // Add at beginning and end
  185
+        mappingInvocations = [], countCallbackInvocations = 0;
  186
+        ko.utils.setDomNodeChildrenFromArrayMapping(testNode, ["A", "B", "C"], mapping, null, callback); // Add at beginning and end
176 187
         value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["A", "B", "C"]);
177 188
         value_of(mappingInvocations).should_be(["A", "C"]);
  189
+        value_of(countCallbackInvocations).should_be(mappingInvocations.length);
178 190
 
179  
-        mappingInvocations = [];
180  
-        ko.utils.setDomNodeChildrenFromArrayMapping(testNode, [1, null, "B"], mapping); // Add to beginning; delete from end
  191
+        mappingInvocations = [], countCallbackInvocations = 0;
  192
+        var observable = ko.observable(1);
  193
+        ko.utils.setDomNodeChildrenFromArrayMapping(testNode, [observable, null, "B"], mapping, null, callback); // Add to beginning; delete from end
181 194
         value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["1", "null", "B"]);
182  
-        value_of(mappingInvocations).should_be([1, null]);
  195
+        value_of(mappingInvocations).should_be([observable, null]);
  196
+        value_of(countCallbackInvocations).should_be(mappingInvocations.length);
  197
+
  198
+        mappingInvocations = [], countCallbackInvocations = 0;
  199
+        observable(2);      // Change the value of the observable
  200
+        value_of(ko.utils.arrayMap(testNode.childNodes, function (x) { return x.innerHTML })).should_be(["2", "null", "B"]);
  201
+        value_of(mappingInvocations).should_be([observable]);
  202
+        value_of(countCallbackInvocations).should_be(mappingInvocations.length);
183 203
     }
184 204
 });
25  spec/templatingBehaviors.js
@@ -361,6 +361,31 @@ describe('Templating', {
361 361
         ko.applyBindings({ myCollection: [1,2,3] }, testNode);
362 362
         value_of(initCalls).should_be(3); // 3 because there were 3 items in myCollection
363 363
     },
  364
+
  365
+    'Data binding \'foreach\' option should apply bindings with an $index in the context': function () {
  366
+        var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
  367
+        ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item # is <span data-bind='text: $index'></span>" }));
  368
+        testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
  369
+
  370
+        ko.applyBindings({ myCollection: myArray }, testNode);
  371
+        value_of(testNode.childNodes[0]).should_contain_html("<div>the item # is <span>0</span></div><div>the item # is <span>1</span></div>");
  372
+    },
  373
+
  374
+    'Data binding \'foreach\' option should update bindings that reference an $index if the list changes': function () {
  375
+        var myArray = new ko.observableArray([{ personName: "Bob" }, { personName: "Frank"}]);
  376
+        ko.setTemplateEngine(new dummyTemplateEngine({ itemTemplate: "The item <span data-bind='text: personName'></span>is <span data-bind='text: $index'></span>" }));
  377
+        testNode.innerHTML = "<div data-bind='template: { name: \"itemTemplate\", foreach: myCollection }'></div>";
  378
+
  379
+        ko.applyBindings({ myCollection: myArray }, testNode);
  380
+        value_of(testNode.childNodes[0]).should_contain_html("<div>the item <span>bob</span>is <span>0</span></div><div>the item <span>frank</span>is <span>1</span></div>");
  381
+
  382
+        var frank = myArray.pop(); // remove frank
  383
+        value_of(testNode.childNodes[0]).should_contain_html("<div>the item <span>bob</span>is <span>0</span></div>");
  384
+
  385
+        myArray.unshift(frank); // put frank in the front
  386
+        value_of(testNode.childNodes[0]).should_contain_html("<div>the item <span>frank</span>is <span>0</span></div><div>the item <span>bob</span>is <span>1</span></div>");
  387
+
  388
+    },
364 389
     
365 390
     'Data binding \'foreach\' option should accept array with "undefined" and "null" items': function () {
366 391
         var myArray = new ko.observableArray([undefined, null]);
31  src/binding/editDetection/arrayToDomNodeChildren.js
@@ -32,11 +32,11 @@
32 32
         }
33 33
     }
34 34
 
35  
-    function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes) {
  35
+    function mapNodeAndRefreshWhenChanged(containerNode, mapping, valueToMap, callbackAfterAddingNodes, index) {
36 36
         // Map this array value inside a dependentObservable so we re-map when any dependency changes
37 37
         var mappedNodes = [];
38 38
         var dependentObservable = ko.dependentObservable(function() {
39  
-            var newMappedNodes = mapping(valueToMap) || [];
  39
+            var newMappedNodes = mapping(valueToMap, index) || [];
40 40
             
41 41
             // On subsequent evaluations, just replace the previously-inserted DOM nodes
42 42
             if (mappedNodes.length > 0) {
@@ -69,6 +69,7 @@
69 69
         var newMappingResult = [];
70 70
         var lastMappingResultIndex = 0;
71 71
         var nodesToDelete = [];
  72
+        var newMappingResultIndex = 0;
72 73
         var nodesAdded = [];
73 74
         var insertAfterNode = null;
74 75
         for (var i = 0, j = editScript.length; i < j; i++) {
@@ -76,7 +77,8 @@
76 77
                 case "retained":
77 78
                     // Just keep the information - don't touch the nodes
78 79
                     var dataToRetain = lastMappingResult[lastMappingResultIndex];
79  
-                    newMappingResult.push(dataToRetain);
  80
+                    dataToRetain.indexObservable(newMappingResultIndex);
  81
+                    newMappingResultIndex = newMappingResult.push(dataToRetain);
80 82
                     if (dataToRetain.domNodes.length > 0)
81 83
                         insertAfterNode = dataToRetain.domNodes[dataToRetain.domNodes.length - 1];
82 84
                     lastMappingResultIndex++;
@@ -101,11 +103,17 @@
101 103
 
102 104
                 case "added": 
103 105
                     var valueToMap = editScript[i].value;
104  
-                    var mapData = mapNodeAndRefreshWhenChanged(domNode, mapping, valueToMap, callbackAfterAddingNodes);
  106
+                    var indexObservable = ko.observable(newMappingResultIndex);
  107
+                    var mapData = mapNodeAndRefreshWhenChanged(domNode, mapping, valueToMap, callbackAfterAddingNodes, indexObservable);
105 108
                     var mappedNodes = mapData.mappedNodes;
106  
-                    
  109
+
107 110
                     // On the first evaluation, insert the nodes at the current insertion point
108  
-                    newMappingResult.push({ arrayEntry: editScript[i].value, domNodes: mappedNodes, dependentObservable: mapData.dependentObservable });
  111
+                    newMappingResultIndex = newMappingResult.push({
  112
+                        arrayEntry: editScript[i].value,
  113
+                        domNodes: mappedNodes,
  114
+                        dependentObservable: mapData.dependentObservable,
  115
+                        indexObservable: indexObservable
  116
+                    });
109 117
                     for (var nodeIndex = 0, nodeIndexMax = mappedNodes.length; nodeIndex < nodeIndexMax; nodeIndex++) {
110 118
                         var node = mappedNodes[nodeIndex];
111 119
                         nodesAdded.push({
@@ -123,7 +131,7 @@
123 131
                         insertAfterNode = node;
124 132
                     } 
125 133
                     if (callbackAfterAddingNodes)
126  
-                        callbackAfterAddingNodes(valueToMap, mappedNodes);
  134
+                        callbackAfterAddingNodes(valueToMap, mappedNodes, indexObservable);
127 135
                     break;
128 136
             }
129 137
         }
@@ -142,10 +150,11 @@
142 150
                 invokedBeforeRemoveCallback = true;
143 151
             }
144 152
         }
145  
-        if (!invokedBeforeRemoveCallback)
146  
-            ko.utils.arrayForEach(nodesToDelete, function (node) {
147  
-                ko.removeNode(node.element);
148  
-            });
  153
+        if (!invokedBeforeRemoveCallback && nodesToDelete.length) {
  154
+            var commonParent = nodesToDelete[0].element.parentNode;
  155
+            for (var i = 0; i < nodesToDelete.length; i++)
  156
+                commonParent.removeChild(nodesToDelete[i].element);
  157
+        }
149 158
 
150 159
         // Store a copy of the array items we just considered so we can difference it next time
151 160
         ko.utils.domData.set(domNode, lastMappingResultDomDataKey, newMappingResult);
32  src/templating/templating.js
@@ -115,19 +115,27 @@
115 115
         }
116 116
     };
117 117
 
118  
-    ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) {   
119  
-        var createInnerBindingContext = function(arrayValue) {
120  
-            return parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue));
121  
-        };
  118
+    ko.renderTemplateForEach = function (template, arrayOrObservableArray, options, targetNode, parentBindingContext) {
  119
+        // Since setDomNodeChildrenFromArrayMapping always calls executeTemplateForArrayItem and then
  120
+        // activateBindingsCallback for added items, we can store the binding context in the former to use in the latter.
  121
+        var arrayItemContext;
  122
+
  123
+        // This will be called by setDomNodeChildrenFromArrayMapping to get the nodes to add to targetNode
  124
+        var executeTemplateForArrayItem = function (arrayValue, index) {
  125
+            // Support selecting template as a function of the data being rendered
  126
+            var templateName = typeof(template) == 'function' ? template(arrayValue) : template;
  127
+            arrayItemContext = parentBindingContext['createChildContext'](ko.utils.unwrapObservable(arrayValue));
  128
+            arrayItemContext['$index'] = index;
  129
+            return executeTemplate(null, "ignoreTargetNode", templateName, arrayItemContext, options);
  130
+        }
122 131
 
123 132
         // This will be called whenever setDomNodeChildrenFromArrayMapping has added nodes to targetNode
124  
-        var activateBindingsCallback = function(arrayValue, addedNodesArray) {
125  
-            var bindingContext = createInnerBindingContext(arrayValue);
126  
-            activateBindingsOnContinuousNodeArray(addedNodesArray, bindingContext);
  133
+        var activateBindingsCallback = function(arrayValue, addedNodesArray, index) {
  134
+            activateBindingsOnContinuousNodeArray(addedNodesArray, arrayItemContext);
127 135
             if (options['afterRender'])
128  
-                options['afterRender'](addedNodesArray, bindingContext['$data']);                                                
  136
+                options['afterRender'](addedNodesArray, arrayValue);
129 137
         };
130  
-         
  138
+
131 139
         return ko.dependentObservable(function () {
132 140
             var unwrappedArray = ko.utils.unwrapObservable(arrayOrObservableArray) || [];
133 141
             if (typeof unwrappedArray.length == "undefined") // Coerce single value into array
@@ -138,11 +146,7 @@
138 146
                 return options['includeDestroyed'] || item === undefined || item === null || !ko.utils.unwrapObservable(item['_destroy']);
139 147
             });
140 148
 
141  
-            ko.utils.setDomNodeChildrenFromArrayMapping(targetNode, filteredArray, function (arrayValue) {
142  
-                // Support selecting template as a function of the data being rendered
143  
-                var templateName = typeof(template) == 'function' ? template(arrayValue) : template;
144  
-                return executeTemplate(null, "ignoreTargetNode", templateName, createInnerBindingContext(arrayValue), options);
145  
-            }, options, activateBindingsCallback);
  149
+            ko.utils.setDomNodeChildrenFromArrayMapping(targetNode, filteredArray, executeTemplateForArrayItem, options, activateBindingsCallback);
146 150
             
147 151
         }, null, { 'disposeWhenNodeIsRemoved': targetNode });
148 152
     };
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.