diff --git a/bower.json b/bower.json index 4763c82e..c8aecfcc 100644 --- a/bower.json +++ b/bower.json @@ -37,7 +37,9 @@ "font-lato-2-subset": "~0.3.5", "packery": "~1.2.3", "draggabilly": "~1.1.0", - "angular-elastic": "~2.3.3" + "angular-elastic": "~2.3.3", + "quick-ng-repeat": "~0.0.1", + "angular-vs-repeat": "~1.0.0-rc2" }, "private": true, "resolutions": { diff --git a/res/app/app.js b/res/app/app.js index ec9d0a66..7e9cb754 100644 --- a/res/app/app.js +++ b/res/app/app.js @@ -17,7 +17,8 @@ angular.module('app', [ require('./menu').name, require('./settings').name, require('./help').name, - require('./../common/lang').name + require('./../common/lang').name, + require('./../test/samples/vs-repeat').name ]) .config(['$routeProvider', '$locationProvider', function ($routeProvider, $locationProvider) { diff --git a/res/test/samples/vs-repeat/app-controller.js b/res/test/samples/vs-repeat/app-controller.js new file mode 100644 index 00000000..00803a6c --- /dev/null +++ b/res/test/samples/vs-repeat/app-controller.js @@ -0,0 +1,53 @@ +module.exports = function ($scope, $location) { + + function getRegularArray(size) { + var res = []; + for (var i = 0; i < size; i++) { + res.push({text: i}); + } + return res; + } + + $scope.items = { + collection: getRegularArray(1000) + }; + $scope.$root.arraySize = 1000; + $scope.switchCollection = function () { + var parsed = parseInt($scope.newArray.size, 10); + if (!isNaN(parsed)) { + $scope.$root.arraySize = parsed; + $scope.items = { + collection: getRegularArray(parsed) + }; + } + }; + $scope.getDomElementsDesc = function () { + var arr = []; + var elems = document.querySelectorAll('.item-element.well'); + elems = Array.prototype.slice.call(elems); + elems.forEach(function (item) { + arr.push(item.innerHTML.trim()); + }); + return arr; + } + $scope.newArray = {}; + $scope.inputKeyDown = function (e) { + if (e.keyCode == 13) { + $scope.switchCollection(); + } + }; + + $scope.outerArray = getRegularArray(2); + $scope.thirdArray = getRegularArray(30); + + $scope.$watch(function () { + return $location.search(); + }, function (obj) { + if (obj.tab) + $scope.tab = obj.tab + ''; + }, true); + + $scope.$watch('tab', function (tab) { + $location.search({tab: tab}); + }); +} diff --git a/res/test/samples/vs-repeat/index.js b/res/test/samples/vs-repeat/index.js new file mode 100644 index 00000000..6f9ce521 --- /dev/null +++ b/res/test/samples/vs-repeat/index.js @@ -0,0 +1,14 @@ +require('./vs-repeat.less') + +require('angular-vs-repeat') + +module.exports = angular.module('stf.vs-repeat', [ + 'vs-repeat' +]) + .controller('PerfVsRepeatCtrl', require('./vs-repeat-controller')) + .controller('AppVsRepeatCtrl', require('./app-controller')) + .config(['$routeProvider', function ($routeProvider) { + $routeProvider.when('/test/samples/vs-repeat', { + template: require('./vs-repeat.html') + }) + }]) diff --git a/res/test/samples/vs-repeat/vs-repeat-controller.js b/res/test/samples/vs-repeat/vs-repeat-controller.js new file mode 100644 index 00000000..cd246ea7 --- /dev/null +++ b/res/test/samples/vs-repeat/vs-repeat-controller.js @@ -0,0 +1,26 @@ +module.exports = function ($scope) { + function getArray(size) { + var arr = []; + for (var i = 0; i < size; i++) { + arr.push({ + a: '', + b: '' + }) + } + return arr; + } + + $scope.$watch('arraySize', function (s) { + $scope.bar = getArray(+s); + }); + + var interval = setInterval(function interval() { + var t1 = Date.now(); + $scope.$digest(); + $scope.digestDuration = (Date.now() - t1); + }, 1000); + + $scope.$on('$destroy', function () { + clearInterval(interval); + }); +} diff --git a/res/test/samples/vs-repeat/vs-repeat.html b/res/test/samples/vs-repeat/vs-repeat.html new file mode 100644 index 00000000..e0bbbbfd --- /dev/null +++ b/res/test/samples/vs-repeat/vs-repeat.html @@ -0,0 +1,160 @@ + +
+ +
+

+ Angular-vs-repeat + angular virtual scroll for ng-repeat +

+
+
+

+ + scroll it vertically +  scroll it horizontally + + + +  scroll it in both directions + + Array size: 30 x {{items.collection.length}} +

+ +
+ +
+ + To show the contents check the checkbox above. Mind how the "actual elements in DOM" changes when you toggle the checkbox. The visibility of the content is controlled via ngShow directive. + +
+
+
+
+ {{item.text}} +
+
+
+
+
+
+
+
+ {{item.text}} +
+
+
+
+
+
+ + Top offset: + + +
+
+ + Bottom offset: + + +
+
+
+
+ {{item.text}} +
+
+
+
+
+
+
+ {{item.text}} +
+
+
+
+
+
+
+
+
+ {{item.text}} +
+
+
+
+
+
+
+
+
+

+ The following example shows the performance benefits of using angular-vs-repeat +

+

+ If you don't look under the hood both lists look exactly the same. On the left size the vs-repeat directive + was used, whereas on the right side there is a plain regular ng-repeat. Above each of those lists the $digest cycle duration + gauge is placed (updates every second) so you can see how many milliseconds it takes to run a $digest with and without vs-repeat. +

+

+ Try with a 10 000 elements array size (using the form below, and be careful, note that the UI will stall for a significant amout of time). + You can see that the $digest duration with vs-repeat stays on the same level (it actually depends on the container size), while the $digest duration on the right side (without vs-repeat) increases dramatically. Also note that each of those repeated elements consist of just two inputs and a binding, but if there were more complex elements inside, with other directives, watchers etc. the array size threshold for experiencing a 'laggy' UI would be much smaller. +

+
+
+
+
+
[with vs-repeat] $digest duration: {{digestDuration}} milliseconds
+
+
+ + + a+b: {{foo.a + ' ' + foo.b}} +
+
+
+
+
[without vs-repeat] $digest duration: {{digestDuration}} milliseconds
+
+
+ + + a+b: {{foo.a + ' ' + foo.b}} +
+
+
+
+
+
+
+

+ Actual elements in DOM ({{getDomElementsDesc().length}}): +

+
+
+                            
+                                
+ {{el}} +
+
+
+
+
+
+

+ You can try different array sizes using the form below +

+
+ + + + +
+
diff --git a/res/test/samples/vs-repeat/vs-repeat.less b/res/test/samples/vs-repeat/vs-repeat.less new file mode 100644 index 00000000..dc7ad160 --- /dev/null +++ b/res/test/samples/vs-repeat/vs-repeat.less @@ -0,0 +1,155 @@ +.vs-repeat-sample { + + .repeater-container { + height: 454px; + overflow: auto; + box-shadow: 0 0 10px; + border-radius: 5px; + -webkit-overflow-scrolling: touch; + } + + .enclosing-ng-repeat .repeater-container { + height: 216px; + } + + .enclosing-ng-repeat .repeater-container:first-child { + margin-bottom: 20px; + } + + .ranges-inner-wrapper { + padding: 10px 20px; + width: 100%; + } + + .repeater-container.ranges { + height: 345px; + margin-top: 20px; + background: hsl(60, 100%, 90%); + } + + .repeater-container.horizontal .item-element { + width: 50px; + height: 100%; + } + + .repeater-container.horizontal.combined { + height: 145px; + } + + .repeater-container.horizontal.combined .item-element { + width: 70px; + text-align: center; + } + + .third-array-ng-repeat { + padding: 10px 20px; + } + + .item-element { + -webkit-transition: background-color 0.3s ease; + -moz-transition: background-color 0.3s ease; + -ms-transition: background-color 0.3s ease; + -o-transition: background-color 0.3s ease; + transition: background-color 0.3s ease; + } + + .item-element:hover { + background-color: hsla(180, 100%, 50%, 0.3); + } + + #dom-preview-container pre { + max-height: 500px; + overflow: auto; + font-size: 11px; + } + + .repeater-container .item-element { + margin: 0 !important; + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; + } + + .array-switch-form { + width: 400px; + margin-top: 20px; + } + + pre > code > span { + display: block; + } + + pre { + padding-top: 0; + padding-bottom: 0; + } + + ul.nav > li { + cursor: pointer; + } + + li.imp a { + color: crimson; + font-weight: bold; + } + + li.imp.active a { + color: hsl(0, 100%, 35%); + font-weight: bold; + } + + li.imp a:hover { + color: hsl(0, 100%, 45%); + } + + li.imp.active a:hover { + color: hsl(0, 100%, 35%); + } + + .perf-container { + overflow: auto; + max-height: 300px; + min-height: 50px; + background-color: hsla(210, 75%, 50%, 0.2); + } + + .perf-elem { + padding: 10px 15px; + border-bottom: 1px solid hsla(0, 0%, 0%, 0.1); + width: 100%; + box-sizing: border-box; + } + + .perf-elem small { + letter-spacing: 5px; + color: hsla(0, 0%, 0%, 0.5); + } + + .perf-summary { + padding: 10px 0; + font-size: 18px; + } + + .danger { + color: crimson; + } + + .success { + color: hsl(160, 100%, 35%); + } + + .tab-1 label { + cursor: pointer; + font-size: 16px; + padding: 10px 0; + } + + .tab-1 label span { + margin-left: 5px; + } + + .tab-1 small { + color: hsl(0, 0%, 60%); + } + +}