UNPKG

angular-scalyr

Version:
189 lines (172 loc) 8.93 kB
/** * @fileoverview * Module: slyRepeat * * Contains the slyRepeat directive, which is is a modified version of the * ngRepeat directive that is meant to be more efficient for creating and * recreating large lists of bound elements. In particular, it has an * optimization that will prevent DOM elements from being constantly created * and destroyed as the contents of the repeated elements change. It does this * by not destroying DOM elements when they are no longer needed, but instead, * just hiding them. This might not work for all use cases, but for it does * for the ones we do wish to heavily optimize. For eample, through profiling, * we found that destroying DOM elements when flipping through log view pages * represented a large chunk of CPU time. * * Cavaets: The collection expression must evaluate to an array. Animators * will not work. Track By does not work. Use at your own peril. * * @author Steven Czerwinski <czerwin@scalyr.com> */ defineScalyrAngularModule('slyRepeat', ['gatedScope']) .directive('slyRepeat', ['$animate', '$parse', function ($animate, $parse) { /** * Sets the scope contained in elementScope to gate all its * watchers based on the isActiveForRepeat proprety. * * @param {Object} elementScope The object containing the * scope and isActiveForRepeat properties. */ function gateWatchersForScope(elementScope) { elementScope.scope.$addWatcherGate(function() { return elementScope.isActiveForRepeat; }); } return { restrict: 'A', scope: true, transclude: 'element', priority: 1000, terminal: true, compile: function(element, attr, linker) { // Most of the work is done in the post-link function. return function($scope, $element, $attr) { // This code is largely based on ngRepeat. // Parse the expression. It should look like: // x in some-expression var expression = $attr.slyRepeat; var match = expression.match(/^\s*(.+)\s+in\s+(.*?)$/); if (!match) { throw Error("Expected slyRepeat in form of '_item_ in _collection_' but got '" + expression + "'."); } var iterVar = match[1]; var collectionExpr = match[2]; match = iterVar.match(/^(?:([\$\w]+))$/); if (!match) { throw Error("'item' in 'item in collection' should be identifier but got '" + lhs + "'."); } // previousElements will store references to the already existing (DOM) elements // that were last used for the last rendering of this repeat and were visible. // We will re-use these elements when executing the next rendering of the repeat when // the iteration value changes. var previousElements = []; // previousElementsBuffer will store references to the already existing (DOM) elements // that are in the page but were not used for the last rendering of this repeat and were // therefore marked as inactive and not visible. This happens if the length of the repeat // iteration goes down over time, since we do not remove the elements. If the repeat length // was first 10, then 5, we will end up with the last 5 elements in the previousElementBuffer. // We keep this in case the length increases again. var previousElementBuffer = []; var deregisterCallback = $scope.$watchCollection(collectionExpr, function(collection) { if (!collection) return; if (!isArray(collection)) throw Error("'collection' did not evaluate to an array. expression was " + collectionExpr); var originalPreviousElementsLength = previousElements.length; // First, reconcile previousElements and collection with respect to the previousElementBuffer. // Basically, try to grow previousElements to collection.length if we can. if ((previousElements.length < collection.length) && (previousElementBuffer.length > 0)) { var limit = previousElements.length + previousElementBuffer.length; if (limit > collection.length) limit = collection.length; previousElements = previousElements.concat(previousElementBuffer.splice(0, limit - previousElements.length)); } var currentElements = null; var currentElementBuffer = []; var newElements = []; if (collection.length > previousElements.length) { // Add in enough elements to account for the larger collection. for (var i = previousElements.length; i < collection.length; ++i) { // Need to add in an element for each new item in the collection. var newElement = { scope: $scope.$new(), isActiveForRepeat: true, }; gateWatchersForScope(newElement); newElement.scope.$index = i; newElement.scope.$first = (i == 0); newElements.push(newElement); } currentElements = previousElements.concat(newElements); currentElementBuffer = previousElementBuffer; } else if (collection.length < previousElements.length) { for (var i = collection.length; i < previousElements.length; ++i) previousElements[i].isActiveForRepeat = false; currentElementBuffer = previousElements.splice(collection.length, previousElements.length - collection.length).concat( previousElementBuffer); currentElements = previousElements; } else { currentElements = previousElements; currentElementBuffer = previousElementBuffer; } // We have to fix up the last and middle values in the scope for each element in // currentElements, since their roles may have changed with the new length. // We always have to fix the last element. if (currentElements.length > 0) { var firstIndexToFix = currentElements.length - 1; var lastIndexToFix = currentElements.length - 1; // We also have to fix any new elements that were added. if (originalPreviousElementsLength < currentElements.length) { firstIndexToFix = originalPreviousElementsLength; } // And we usually have to fix the element before the first element we modified // in case it used to be last. if (firstIndexToFix > 0) { firstIndexToFix = firstIndexToFix - 1; } for (var i = firstIndexToFix; i <= lastIndexToFix; ++i) { currentElements[i].scope.$last = (i == (currentElements.length - 1)); currentElements[i].scope.$middle = ((i != 0) && (i != (currentElements.length - 1))); if (!currentElements[i].isActiveForRepeat) { // If it is not marked as active, make it active. This is also indicates that // the element is currently hidden, so we have to unhide it. currentElements[i].isActiveForRepeat = true; currentElements[i].element.css('display', ''); } } } // Hide all elements that have recently become inactive. for (var i = 0; i < currentElementBuffer.length; ++i) { if (currentElementBuffer[i].isActiveForRepeat) break; currentElementBuffer[i].element.css('display', 'none'); } // Assign the new value for the iter variable for each scope. for (var i = 0; i < currentElements.length; ++i) { currentElements[i].scope[iterVar] = collection[i]; } // We have to go back now and clone the DOM element for any new elements we // added and link them in. We clone the last DOM element we had created already // for this Repeat. var prevElement = $element; if (previousElements.length > 0) prevElement = previousElements[previousElements.length - 1].element; for (var i = 0; i < newElements.length; ++i) { linker(newElements[i].scope, function(clone) { $animate.enter(clone, null, prevElement); prevElement = clone; newElements[i].element = clone; }); } previousElements = currentElements; previousElementBuffer = currentElementBuffer; }); $scope.$on('$destroy', function() { deregisterCallback(); }); }; } }; }]);