So, I've come up with a few solutions to this and I'm still not quite sure what is best. First for reference there is one similar question I could find, though it's a bit old. Here it is for anyone reading this later: Watching dimension changes in Angular
The Goal
I have portions of my app in which responsive height elements are needed. I want the fastest, most visually appealing way to do this at the UI/directive layer without the need of explicitly broadcasted change events.
Option One: Directive
A simple directive can record the dimensions on every digest loop (and resize event)
return {
scope: {
size: '=ngSize'
},
link: function($scope, element, attrs) {
var windowEl = angular.element($window),
handler = function() {
if ($scope.size &&
element.outerWidth() === $scope.size.width &&
element.outerHeight() === $scope.size.height) return false;
$scope.size = {
width: element.outerWidth(),
height: element.outerHeight()
};
};
windowEl.on('resize', function() {
$scope.$apply(handler);
});
$root.$watch(function() { return [element.outerWidth(), element.outerHeight()] }, handler, true);
}
};
Problem: The change doesn't propagate quickly enough and visual lag is noticeable.
Solution: Using an interval call
Option Two: $interval
I tried the same thing with an $interval call and it worked, but CPU usage was surprisingly high, even after I inverted control and kept track of elements in a simple root collection watched by value (avoiding concurrent timers produced by multiple instances of the directive).
Aside from some GC-related issue in my environment (I don't see anything that suggests this currently), might there be a better approach to creating this kind of fluid layout?
My first thought would be a way to create a concurrent $digest loop of sorts, to efficiently monitor the DOM as a whole for any visual changes. Is it possible to efficiently iterate through all computed styles, for example, to produce a cheap hash that can be watched by value? Something that can be triggered relatively inexpensively every time a relevant computed style is added and/or changed?
Before I build and profile it, could someone comment as to whether it's even realistic, or if it simply makes more sense to abstract/refactor the resize triggers in the first place?
Any other ideas on the preferred way to accomplish this in 1.2.9?
[edit] An alternate, perhaps simpler question: is it even possible to provide a realistic refresh rate of 1-200ms via Javascript in a computationally-efficient manner? If so, would that way be Angular's $interval, or could a 'VanillaJS' implementation be more efficient?
Unfortunately this question didn't get as much attention as I would have liked... And I don't have time to write a detailed explanation with background, profiler screenshots, etc. But I did find a solution, and I hope this helps someone else dealing with the same issue.
The $digest loop in any realistic medium-large application will not be able to handle a refresh of 100ms.
Given the requirement of setInterval
I simply bypassed the loop entirely, instead opting to broadcast a state change only when differing dimensions are detected (using the offsetWidth/Height
native properties).
Running at a 100ms interval with a single $root
scope manifest is giving the best results of anything I've tested, with ~10.2% active-tab CPU on my 2.4Ghz i7 2013 MBP--acceptable compared to the ~84% with $interval
.
Any comments and critiques are welcome, otherwise, here is the self-contained directive! Obviously you can get creative with the scope and/or attributes to customize the watchers, but in the interest of staying on topic I've tried to omit any superfluous code.
It will monitor & bind size changes on an element to a scope property of your choosing, for N elements with a linear complexity. I can't guarantee it's the fastest/best way to do this, but it's the fastest implementation I could develop quickly that tracks state-agnostic DOM-level dimension changes:
app.directive('ngSize', ['$rootScope', function($root) {
return {
scope: {
size: '=ngSize'
},
link: function($scope, element, attrs) {
$root.ngSizeDimensions = (angular.isArray($root.ngSizeDimensions)) ? $root.ngSizeDimensions : [];
$root.ngSizeWatch = (angular.isArray($root.ngSizeWatch)) ? $root.ngSizeWatch : [];
var handler = function() {
angular.forEach($root.ngSizeWatch, function(el, i) {
// Dimensions Not Equal?
if ($root.ngSizeDimensions[i][0] != el.offsetWidth || $root.ngSizeDimensions[i][1] != el.offsetHeight) {
// Update Them
$root.ngSizeDimensions[i] = [el.offsetWidth, el.offsetHeight];
// Update Scope?
$root.$broadcast('size::changed', i);
}
});
};
// Add Element to Chain?
var exists = false;
angular.forEach($root.ngSizeWatch, function(el, i) { if (el === element[0]) exists = i });
// Ok.
if (exists === false) {
$root.ngSizeWatch.push(element[0]);
$root.ngSizeDimensions.push([element[0].offsetWidth, element[0].offsetHeight]);
exists = $root.ngSizeWatch.length-1;
}
// Update Scope?
$scope.$on('size::changed', function(event, i) {
// Relevant to the element attached to *this* directive
if (i === exists) {
$scope.size = {
width: $root.ngSizeDimensions[i][0],
height: $root.ngSizeDimensions[i][1]
};
}
});
// Refresh: 100ms
if (!window.ngSizeHandler) window.ngSizeHandler = setInterval(handler, 100);
// Window Resize?
// angular.element(window).on('resize', handler);
}
};
}]);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With