Skip to content

Commit

Permalink
Merge branch 'master' of github.com:mbest/knockout-repeat
Browse files Browse the repository at this point in the history
  • Loading branch information
mbest committed Jun 22, 2012
2 parents 2521fd4 + 27043cd commit 434fd5d
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 25 deletions.
51 changes: 31 additions & 20 deletions README.md
Expand Up @@ -5,60 +5,71 @@
For example, say you are creating a data table. Here's the html using `foreach`:

```html
<table>
<tbody data-bind="foreach: data">
<tr data-bind="foreach: $parent.columns">
<td data-bind="text: $parent[$data.propertyName]"></td>
</tr>
</tbody>
</table>
<table>
<tbody data-bind="foreach: data">
<tr data-bind="foreach: $parent.columns">
<td data-bind="text: $parent[$data.propertyName]"></td>
</tr>
</tbody>
</table>
```
Here is the equivalent html using `repeat`:

```html
<table>
<table>
<tbody>
<tr data-bind="repeat: {foreach: data, item: '$row'}">
<td data-bind="repeat: {foreach: columns, item: '$col',
<tr data-bind="repeat: {foreach: data, item: '$row'}">
<td data-bind="repeat: {foreach: columns, item: '$col',
bind: 'text: $row()[$col().propertyName]'}"></td>
</tr>
</tr>
</tbody>
</table>
</table>
```
In my tests with about 400 rows, the `repeat` version was twice as fast.

`repeat` can take either a single parameter (the number of repetitions [count]) or an object literal with
the following properties:
`repeat` can take either a single parameter (the number of repetitions [count] or an array to iterate [foreach])
or an object literal with the following properties:

* `count` the number of repetitions
* `foreach` an array or observableArray over which to iterate
(either *count* or *foreach* is required)
* `index` the name of the property that will store the index (default is `$index`)
* `item` the name of the property used to access the indexed item in the array (default is `$item`)
(*item* is only used when an array is supplied with *foreach*) `$item` is a psuedo-observable and
(*item* is only used when an array is supplied with *foreach*) `$item` is a psuedo-observable and
can be passed directly to bindings that accept observables (most do) or the item value can be
accessed using observable syntax: `$item()`.
* `bind` the binding used for the repeated elements (optional); *index* and *item* will be available
in this binding
in this binding. Binding can be either a string (see above) or a function (see below) that returns
an object. If using the function syntax with a array, the first parameter is *item* and the second
is *index*; with just a count, the only parameter is *index*. The last parameter to the function is
the context object, which is useful if you want to define your function externally (in your view
model) and want access to context properties such as `$parent`. The repeated binding can also be
specified using its own attribute, `data-repeat-bind` (see below).

Here are some more examples:

```html
<span data-bind="repeat: {count: 5, bind: 'text: $index'}">
<span data-bind="repeat: {count: 5, bind: function($index) { return { text: $index } } }">
```

This will display 01234.

```html
<div data-bind="repeat: {foreach: availableCountries, item: '$country',
bind: 'css: { sel: $country() == selectedCountry()}'}">
<div data-bind="repeat: {foreach: availableCountries, item: '$country',
bind: function($country) { return { css: { sel: $country() == selectedCountry() } } } }">
<span data-bind="text: $index+1"></span>. <span data-bind="text: $country"></span>
</div>
```

This will display a list of countries with numbering supplied by the repeat binding's $index. The selected
This will display a list of countries with numbering supplied by the repeat binding's $index. The selected
country will have the `selected` class.

Example using the `data-repeat-bind` attribute:

```html
<span data-bind="repeat: 5" data-repeat-bind="text: $index">
```

License: MIT (http://www.opensource.org/licenses/mit-license.php)

Michael Best<br>
Expand Down
31 changes: 26 additions & 5 deletions knockout-repeat.js
@@ -1,6 +1,7 @@
// REPEAT binding for Knockout http://knockoutjs.com/
// (c) Michael Best
// License: MIT (http://www.opensource.org/licenses/mit-license.php)
// Version 1.2.1

(function() {
if (!ko.bindingFlags) { ko.bindingFlags = {}; }
Expand Down Expand Up @@ -29,10 +30,19 @@ ko.bindingHandlers['repeat'] = {
ko.cleanNode(element);
element.removeAttribute('data-bind');

// extract and remove a data-repeat-bind attribute, if present
if (!repeatBind) {
repeatBind = element.getAttribute('data-repeat-bind');
if (repeatBind)
element.removeAttribute('data-repeat-bind');
}

// Make a copy of the element node to be copied for each repetition
var cleanNode = element.cloneNode(true);
if (repeatBind)
if (typeof repeatBind == "string") {
cleanNode.setAttribute('data-bind', repeatBind);
repeatBind = null;
}

// Original element is no longer needed: delete it and create a placeholder comment
var parent = element.parentNode, placeholder = document.createComment('ko_repeatplaceholder');
Expand All @@ -44,8 +54,6 @@ ko.bindingHandlers['repeat'] = {
repeatArray;

var subscribable = ko.dependentObservable(function() {
var repeatCount = ko.utils.unwrapObservable(valueAccessor());

function makeArrayItemAccessor(index) {
var f = function() {
var item = repeatArray[index];
Expand All @@ -61,13 +69,23 @@ ko.bindingHandlers['repeat'] = {
return f;
}

function makeBinding(item, index, context) {
return repeatArray
? function() { return repeatBind.call(viewModel, item, index, context); }
: function() { return repeatBind.call(viewModel, index, context); }
}

var repeatCount = ko.utils.unwrapObservable(valueAccessor());
if (typeof repeatCount == 'object') {
if ('count' in repeatCount) {
repeatCount = ko.utils.unwrapObservable(repeatCount['count']);
} else if ('foreach' in repeatCount) {
repeatArray = ko.utils.unwrapObservable(repeatCount['foreach']);
repeatCount = repeatArray && repeatArray['length'] || 0;
} else if ('length' in repeatCount) {
repeatArray = repeatCount;
}
if (repeatArray)
repeatCount = repeatArray['length'] || 0;
}
// Remove nodes from end if array is shorter
for (; lastRepeatCount > repeatCount; lastRepeatCount--) {
Expand Down Expand Up @@ -95,7 +113,10 @@ ko.bindingHandlers['repeat'] = {
newContext[repeatData] = makeArrayItemAccessor(lastRepeatCount);
}
newContext[repeatIndex] = lastRepeatCount;
ko.applyBindings(newContext, newNode);
if (repeatBind)
var shouldBindDescendants = ko.applyBindingsToNode(newNode, makeBinding(newContext[repeatData], lastRepeatCount, newContext), newContext).shouldBindDescendants;
if (!repeatBind || shouldBindDescendants)
ko.applyBindings(newContext, newNode);
}
}, null, {'disposeWhenNodeIsRemoved': placeholder});

Expand Down
57 changes: 57 additions & 0 deletions spec/repeatBinding.js
Expand Up @@ -41,6 +41,50 @@ describe('Binding: Repeat', {
value_of(testNode).should_contain_text('first childsecond child');
},

'Should be able to specify sub-binding using a function with index': function() {
testNode.innerHTML = "<span data-bind=\"repeat: {count: 5, bind: function($index) { return { text: $index }}}\"></span>";
ko.applyBindings(null, testNode);
value_of(testNode).should_contain_text('01234');
},

'Should be able to specify sub-binding using a function with item and index': function() {
testNode.innerHTML = "<span data-bind='repeat: {foreach: someItems, bind: function($item, $index) {return { text: $index + $item().childProp }}}'></span>";
var someItems = [
{ childProp: 'first child' },
{ childProp: 'second child' }
];
ko.applyBindings({ someItems: someItems }, testNode);
value_of(testNode).should_contain_text('0first child1second child');
},

'Should be able to specify sub-binding using a function on the view model and have access to the context': function() {
testNode.innerHTML = "<span data-bind='repeat: {foreach: someItems, bind: itemBinding }'></span>";
var someItems = [
{ childProp: 'first child' },
{ childProp: 'second child' }
];
var vm = {
someItems: someItems,
itemBinding: function($item, $index, context) {
value_of(this).should_be(vm);
value_of(context.$data).should_be(vm);
return { text: $index + $item().childProp };
}
}
ko.applyBindings(vm, testNode);
value_of(testNode).should_contain_text('0first child1second child');
},

'Should be able to specify sub-binding using a data-repeat-bind attribute': function() {
testNode.innerHTML = "<span data-bind='repeat: someItems' data-repeat-bind='text: $index + $item().childProp'></span>";
var someItems = [
{ childProp: 'first child' },
{ childProp: 'second child' }
];
ko.applyBindings({ someItems: someItems }, testNode);
value_of(testNode).should_contain_text('0first child1second child');
},

'Should be able to use \'with\' to create a child context': function() {
testNode.innerHTML = "<div data-bind='repeat: {foreach: someItems, bind: \"with: $item\"}'><span data-bind='text: childProp'></span></div>";
var someItems = ko.observableArray([
Expand All @@ -54,6 +98,19 @@ describe('Binding: Repeat', {
value_of(testNode).should_contain_text('first childsecond childlast child');
},

'Should be able to use \'with\' to create a child context using function syntax': function() {
testNode.innerHTML = "<div data-bind='repeat: {foreach: someItems, bind: function($item) { return { with: $item }}}'><span data-bind='text: childProp'></span></div>";
var someItems = ko.observableArray([
{ childProp: 'first child' },
{ childProp: 'second child' }
]);
ko.applyBindings({ someItems: someItems }, testNode);
value_of(testNode).should_contain_text('first childsecond child');
// add an item
someItems.push({ childProp: 'last child' });
value_of(testNode).should_contain_text('first childsecond childlast child');
},

'Should be able to set item to \'$data\' to create a child context (if supported)': function() {
testNode.innerHTML = "<div data-bind='repeat: {foreach: someItems, item: \"$data\"}'><span data-bind='text: childProp'></span></div>";
var someItems = ko.observableArray([
Expand Down

0 comments on commit 434fd5d

Please sign in to comment.