Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backported TKO changes, implemented 'as' paramter & tests #53

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ for other browsers supported by Karma.

Changes
---
6 Jan 2017 – 🚡 TKO - master

- Make compatible with tko style packages.
- Deprecate `noContext`; if `as` is provided, then the current context is extended.
- Improve performance of the context generator.

16 Dec 2015 – 🔭 0.6.0
- Reuse DOM nodes when array items move [#33, #34]
- Improve internal nodes handling [#31, #32]
Expand Down Expand Up @@ -52,4 +58,4 @@ Changes
License
---

*MIT* Licensed.
*MIT* Licensed.
55 changes: 55 additions & 0 deletions README.md.orig
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
knockout-fast-foreach
=====================

## NOTICE: This plugin has been incorporated into [tko](https://github.com/knockout/tko) as [tko.binding.foreach](https://github.com/knockout/tko/tree/master/packages/tko.binding.foreach)

An experiment in faster foreach binding.

Include in your project in the usual ways, then instead of `foreach` use
`fastForEach`.

[Demo on JSBin](http://jsbin.com/dakihezega/2)

Testing
---

Run tests from the command line with `npm test`, or on Windows `npm run test_win`.


Run tests in Chrome by installing `karma-chrome-launcher` then
`$ ./node_modules/karma/bin/karma start --browsers Chrome`; the same applies
for other browsers supported by Karma.

Changes
---
16 Dec 2015 – 🔭 0.6.0
- Reuse DOM nodes when array items move [#33, #34]
- Improve internal nodes handling [#31, #32]

27 Sep 2015 – 📇 0.5.5
- Improved batch addition (closes #30)

27 Sep 2015 - ⛵️ 0.5.4
- add `afterAdd` and `beforeRemove`

25 Sep 2015 – 🍭 0.5.3
- fix `$index` when list is made from virtual elements

23 Sep 2015 - 👽 0.5.2
- fix `$index` not working when template starts with a text node

22 Sep 2015 – 🐝 0.5.0
- add `$index()` support (disable by passing `noIndex: true`)

16 Jul 2015 - 🌕 0.4.1
- fix `push.apply` not working on `NodeList` in older Webkit versions

14 Jul 2015 – 🎂 0.4.0
- uses `documentFragment` when possible
- use karma for testing
- add `.eslintrc` and clean up source

License
---

*MIT* Licensed.
72 changes: 51 additions & 21 deletions dist/knockout-fast-foreach.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,8 @@ function FastForEach(spec) {
this.element.parentNode : this.element;
this.$context = spec.$context;
this.data = spec.data;
this.as = spec.as;
this.noContext = spec.noContext;
this.noIndex = spec.noIndex;
this.generateContext = this.createContextGenerator(spec.as, !spec.noIndex);
if (spec.noIndex) { this.updateIndexes = false; }
this.afterAdd = spec.afterAdd;
this.beforeRemove = spec.beforeRemove;
this.templateNode = makeTemplateNode(
Expand Down Expand Up @@ -205,17 +204,13 @@ FastForEach.prototype.processQueue = function () {
if (typeof changeItem.index === 'number') {
lowestIndexChanged = Math.min(lowestIndexChanged, changeItem.index);
}
// console.log(self.data(), "CI", JSON.stringify(changeItem, null, 2), JSON.stringify($(self.element).text()))
self[changeItem.status](changeItem);
// console.log(" ==> ", JSON.stringify($(self.element).text()))
});
this.flushPendingDeletes();
this.rendering_queued = false;

// Update our indexes.
if (!this.noIndex) {
this.updateIndexes(lowestIndexChanged);
}
if (this.updateIndexes) { this.updateIndexes(lowestIndexChanged); }

// Callback so folks can do things.
if (typeof this.afterQueueFlush === 'function') {
Expand All @@ -225,11 +220,52 @@ FastForEach.prototype.processQueue = function () {
};


// Extend the given context with a $index (passed in via the createChildContext)
function extendWithIndex(context) {
context.$index = ko.observable();
};


/**
* Return a function that generates the context for a given node.
*
* We generate a single function that reduces our inner-loop calculations,
* which has a good chance of being optimized by the browser.
*
* @param {string} as The name given to each item in the list
* @param {bool} index Whether to calculate indexes
* @return {function} A function(dataValue) that returns the context
*/
FastForEach.prototype.createContextGenerator = function (as, index) {
var $context = this.$context;
switch ((as && 1) | (index && 2)) {
case 0: // no-as & no-index
return function(v) {
return $context.createChildContext(v, null, undefined);
};

case 1: // as + no-index
return function(v) {
var obj = { $index: undefined };
obj[as] = v;
return $context.extend(obj);
};

case 2: // no-as + index
return function(v) {
return $context.createChildContext(v, null, extendWithIndex);
};

case 3: // as + index
return function(v) {
var obj = { $index: ko.observable() };
obj[as] = v;
return $context.extend(obj);
};
}
};


// Process a changeItem with {status: 'added', ...}
FastForEach.prototype.added = function (changeItem) {
var index = changeItem.index;
Expand All @@ -247,19 +283,12 @@ FastForEach.prototype.added = function (changeItem) {
childNodes = pendingDelete.nodesets.pop();
} else {
var templateClone = this.templateNode.cloneNode(true);
var childContext;

if (this.noContext) {
childContext = this.$context.extend({
$item: valuesToAdd[i],
$index: this.noIndex ? undefined : ko.observable()
});
} else {
childContext = this.$context.createChildContext(valuesToAdd[i], this.as || null, this.noIndex ? undefined : extendWithIndex);
}

// apply bindings first, and then process child nodes, because bindings can add childnodes
ko.applyBindingsToDescendants(childContext, templateClone);
// Apply bindings first, and then process child nodes,
// because bindings can add childnodes.
ko.applyBindingsToDescendants(
this.generateContext(valuesToAdd[i]), templateClone
);

childNodes = ko.virtualElements.childNodes(templateClone);
}
Expand Down Expand Up @@ -459,7 +488,8 @@ ko.bindingHandlers.fastForEach = {
ffe = new FastForEach({
element: element,
data: ko.unwrap(context.$rawData) === value ? context.$rawData : value,
$context: context
$context: context,
as: bindings.get('as')
});
}

Expand Down
47 changes: 45 additions & 2 deletions spec/knockout-fast-foreach-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -719,12 +719,55 @@ describe("observable array changes", function () {
assert.equal(ko.contextFor(target.children()[0]).$index, undefined)
})

it("is present with noContext", function () {
var target = $("<ul data-bind='fastForEach: {data: $data, noContext: true}'><li data-bind='text: $data'></li></div>");
it("is present with `as`", function () {
var target = $("<ul data-bind='fastForEach: {data: $data, as: \"$item\"}'><li data-bind='text: $item'></li></div>");
var list = ko.observableArray(['a', 'b']);
ko.applyBindings(list, target[0])
assert.equal(ko.contextFor(target.children()[0]).$index(), 0)
assert.equal(ko.contextFor(target.children()[1]).$index(), 1)
})
})

describe('`as` parameter', function () {
it("is used when present", function () {
var target = $("<ul data-bind='foreach: { data: $data, as: \"xyz\" }'><li data-bind='text: xyz'></li></div>");
var list = ['a', 'b', 'c'];
applyBindings(list, target[0])
assert.equal(target.text(), 'abc')
})

it("each item has the same $data as its parent", function () {
var target = $("<ul data-bind='foreach: { data: $data, as: \"xyz\" }'><li data-bind='text: xyz'></li></div>");
var list = ['a', 'b', 'c'];
applyBindings(list, target[0])
assert.strictEqual(dataFor(target.children()[0]).$data, dataFor(target))
assert.strictEqual(dataFor(target.children()[1]).$data, dataFor(target))
assert.strictEqual(dataFor(target.children()[2]).$data, dataFor(target))
})

it("has an $index", function () {
var target = $("<ul data-bind='foreach: { data: $data, as: \"xyz\" }'><li data-bind='text: xyz'></li></div>");
var list = ['a', 'b', 'c'];
applyBindings(list, target[0])
assert.equal(contextFor(target.children()[0]).$index(), 0)
assert.equal(contextFor(target.children()[1]).$index(), 1)
assert.equal(contextFor(target.children()[2]).$index(), 2)
})

it("respects `noIndex`", function () {
var target = $("<ul data-bind='foreach: { data: $data, as: \"xyz\", noIndex: true }'><li data-bind='text: xyz'></li></div>");
var list = ['a', 'b', 'c'];
applyBindings(list, target[0])
assert.equal(contextFor(target.children()[0]).$index, undefined)
assert.equal(contextFor(target.children()[1]).$index, undefined)
assert.equal(contextFor(target.children()[2]).$index, undefined)
})

it("reads `as` from peer binding parameters", function () {
var target = $("<ul data-bind='foreach: $data, as: \"xyz\"'><li data-bind='text: xyz'></li></div>");
var list = ['a', 'b', 'c'];
applyBindings(list, target[0])
assert.equal(target.text(), 'abc')
})
})
})