ng-repeat-fast
Version:
Faster ng-repeat realization that caches DOM nodes.
295 lines (240 loc) • 9.72 kB
JavaScript
(function () {
'use strict';
chai.should();
var $compile, $rootScope;
var app = angular.module('app', ['ngRepeatFast']);
var fastRepeatTemplate =
'<div ng-repeat-fast="item in list">' +
'{{ ::item.value }}' +
'</div>';
/**
* @param {string?} customTemplate
* @returns {jQuery}
*/
function createElement(customTemplate) {
var template = customTemplate || fastRepeatTemplate;
var element = $compile(template)($rootScope);
$rootScope.$digest();
return $(element[0].parentNode); // documentFragment
}
/**
* @param container
* @param [includeHidden]
* @returns {jQuery[]}
*/
function getItems(container, includeHidden) {
if (includeHidden) {
return container.children();
} else {
return container.children(':not(.ng-hide)');
}
}
beforeEach(function () {
module('app');
inject(function ($injector) {
$compile = $injector.get('$compile', '');
$rootScope = $injector.get('$rootScope', '');
});
});
describe('common tests', function () {
it('parses ng-repeat expression', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var templates = [
'<div ng-repeat-fast="item in list"></div>',
'<div ng-repeat-fast="item in list | filter: 1"></div>',
'<div ng-repeat-fast="item in ::list"></div>',
'<div ng-repeat-fast="item in ::list track by value"></div>'
];
templates.forEach(function (template) {
$compile(template)($rootScope);
});
});
it('parses `track by` expression', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var template =
'<div ng-repeat-fast="item in list track by value">' +
'{{ ::item.value }}' +
'</div>';
var container = createElement(template);
$rootScope.list.forEach(function (x) {
x.should.not.have.property('$$hashKey');
});
var items = getItems(container);
items.length.should.eq(2);
items.eq(0).text().should.eq('0');
items.eq(1).text().should.eq('1');
});
it('throws if expression is invalid', function () {
var template = '<div ng-repeat-fast="!@#"></div>';
var action = function () {
$compile(template)($rootScope);
};
action.should.throw();
});
it('throws if ng-include is set on repeated element', function () {
$rootScope.list = [];
var template = '<div ng-repeat-fast="list" ng-include></div>';
var action = function () {
$compile(template)($rootScope);
};
action.should.throw();
});
it('throws if model is not an array of objects', function () {
var models = [{}, null, undefined, 123, 'lol'];
models.forEach(function (model) {
$rootScope.list = model;
var action = function () {
$compile(fastRepeatTemplate)($rootScope);
};
action.should.throw();
});
});
it('understands one-time binding ::', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var template =
'<div ng-repeat-fast="item in ::list">' +
'{{ ::item.value }}' +
'</div>';
var container = createElement(template);
$rootScope.list.push({ value: 2 });
$rootScope.$digest();
var items = getItems(container);
items.length.should.eq(2);
items.eq(0).text().should.eq('0');
items.eq(1).text().should.eq('1');
});
it('removes watchers when destroyed', function () {
var unwatchCalledTimes = 0;
var watchCollection = $rootScope.$watchCollection;
$rootScope.$watchCollection = function () {
var unwatch = watchCollection.apply($rootScope, arguments);
return function () {
++unwatchCalledTimes;
unwatch();
};
};
$rootScope.list = [];
createElement();
$rootScope.$destroy();
unwatchCalledTimes.should.eq(1);
});
});
describe('DOM sync.', function () {
it('creates nodes', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var container = createElement();
$rootScope.list.push({ value: 2 });
$rootScope.$digest();
var items = getItems(container);
items.length.should.eq(3);
items.eq(0).text().should.eq('0');
items.eq(1).text().should.eq('1');
items.eq(2).text().should.eq('2');
[].some.call(items, function (x, i) {
var scope = $(x).scope();
scope.$index.should.eq(i);
scope.$first.should.eq(i == 0);
scope.$last.should.eq(i == 2);
scope.$middle.should.eq(i == 1);
scope.$even.should.eq(i % 2 == 0);
scope.$odd.should.eq(i % 2 == 1);
});
});
it('removes nodes', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var container = createElement();
$rootScope.list = [];
$rootScope.$digest();
var items = getItems(container);
items.length.should.eq(0);
});
it('adds node to the begin', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var container = createElement();
$rootScope.list.unshift({ value: -1 });
$rootScope.$apply();
var items = getItems(container);
items.length.should.eq(3);
items.eq(0).text().should.eq('-1');
items.eq(1).text().should.eq('0');
items.eq(2).text().should.eq('1');
});
it('adds node to the end', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var container = createElement();
$rootScope.list.push({ value: 2 });
$rootScope.$digest();
var items = getItems(container);
items.length.should.eq(3);
items.eq(0).text().should.eq('0');
items.eq(1).text().should.eq('1');
items.eq(2).text().should.eq('2');
});
[2, 3, 7].forEach(function (n) {
it('reverses list of ' + n + ' nodes', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var container = createElement();
$rootScope.list = [];
$rootScope.$digest();
for (var i = 0; i < n; ++i) {
$rootScope.list.push({ value: i });
}
$rootScope.$digest();
$rootScope.list = $rootScope.list.reverse();
$rootScope.$digest();
var items = getItems(container);
items.length.should.eq(n);
[].forEach.call(items, function (x, i) {
x.textContent.should.eq(String(n-i-1));
});
}); // it
}); // forEach
it('does not affect sibling nodes', function () {
$rootScope.list = [];
var template =
'<span>-----</span>' +
'<div ng-repeat-fast="item in list">' +
'{{ ::item.value }}' +
'</div>' +
'<span>+++++</span>';
var container = createElement(template);
$rootScope.list.push({ value: 0 });
$rootScope.list.push({ value: 1 });
$rootScope.$digest();
var items = getItems(container);
items.length.should.eq(4);
items.eq(0).text().should.eq('-----');
items.eq(1).text().should.eq('0');
items.eq(2).text().should.eq('1');
items.eq(3).text().should.eq('+++++');
});
it('reuses nodes after deletion and recreation', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var container = createElement();
var listBackup = $rootScope.list;
var itemsBackup = getItems(container);
$rootScope.list = [];
$rootScope.$digest();
$rootScope.list = listBackup;
$rootScope.$digest();
var items = getItems(container);
items.should.eql(itemsBackup);
});
it('corrects node indexes if they added again', function () {
$rootScope.list = [{ value: 0 }, { value: 1 }];
var template =
'<div ng-repeat-fast="item in list track by value">' +
'{{ ::item.value }}' +
'</div>';
var container = createElement(template);
$rootScope.list = [];
$rootScope.$digest();
$rootScope.list = [{ value: 1 }, { value: 0 }];
$rootScope.$digest();
var items = getItems(container);
items.length.should.eq(2);
items.eq(0).text().should.eq('1');
items.eq(1).text().should.eq('0');
});
}); // describe 'DOM sync.'
})();