Force angular to consider scope property "dirty" - angularjs

Is there a way to force angular to re-render things bound to a property, even though the property has not changed value? e.g:
$scope.size = 1;
<div some-prop="size"></div>
$scope.$needsRender('size') // psuedocode
$scope.$apply(); // re-renders the <div>
Unfortunately I can't manage the property entirely in angular for performance reasons, which is why I need this "reset".

Angular DOM beeing manipulated outside of angular is ugly. Well to say the truth, it's more than that. People doing this probably deserves to die. slowly. Painfully.
But anyway yes, it is possible.
Short answer : You can do this be forcing the execution of a scope's watcher to fire.
module.factory("scopeUtils", function($parse) {
var scopeUtils = {
/**
* Apply watchers of given scope even if a digest progress is already in process on another level.
* This will only do a one-time cycle of watchers, without cascade digest.
*
* Please note that this is (almost) a hack, behaviour may be hazardous so please use with caution.
*
* #param {Scope} scope : scope to apply watchers from.
*/
applyWatchers : function(scope) {
scopeUtils.traverseScopeTree(scope, function(scope) {
var watchers = scope.$$watchers;
if(!watchers) {
return;
}
var watcher;
for(var i=0; i<watchers.length; i++) {
watcher = watchers[i];
var value = watcher.get(scope);
watcher.fn(value, value, scope);
}
});
},
traverseScopeTree : function(parentScope, traverseFn) {
var next,
current = parentScope,
target = parentScope;
do {
traverseFn(current);
if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while((current = next));
}
};
return scopeUtils;
});
Use it simply like that :
scopeUtils.applyWatchers(myScope);

Related

Advanced boosting of angular digest loop

I'm researching my options for boosting angular's digest loop.
I have a fairly complex app with tens of thousands active watchers at any given time.
As part of the functionality of the app I'm registering on scroll events (and handling them in an animation frame), so the digest is basically executed on each scroll, which results in an occasional drop in fps.
I've reduced the watchers count by one-time bindings and I'm now left with a few thousands watchers.
Currently I'm attempting to write a directive to suspend all watchers of out of viewport elements.
So I started playing with angular internals and came up with the following directive:
app.directive('ngSuspendable', function () {
return {
restrict: 'A',
scope: true,
link: function (scope, element, attr) {
var _watchersMap = {};
var _suspended = true;
suspendWatchersTree();
function suspendWatchersTreeRecursive(currentScope, includeSiblings) {
while (currentScope != null) {
if ((currentScope.$$watchers != null) &&
(currentScope.$$watchers.length > 0) &&
(typeof _watchersMap[currentScope.$id] == "undefined")) {
_watchersMap[currentScope.$id] = currentScope.$$watchers;
currentScope.$$watchers = [];
}
if (currentScope.$$childHead != null) {
suspendWatchersTreeRecursive(currentScope.$$childHead, true);
}
if (includeSiblings) {
currentScope = currentScope.$$nextSibling;
}
else {
currentScope = null;
}
}
}
function unsuspendWatchersTreeRecursive(currentScope, includeSiblings) {
while (currentScope != null) {
if ((typeof _watchersMap[currentScope.$id] != "undefined") &&
(_watchersMap[currentScope.$id].length > 0)) {
if ((currentScope.$$watchers != null) &&
(currentScope.$$watchers.length > 0)) {
currentScope.$$watchers = currentScope.$$watchers.concat(_watchersMap[currentScope.$id]);
}
else {
currentScope.$$watchers = _watchersMap[currentScope.$id];
}
}
if (currentScope.$$childHead != null) {
unsuspendWatchersTreeRecursive(currentScope.$$childHead, true);
}
if (includeSiblings) {
currentScope = currentScope.$$nextSibling;
}
else {
currentScope = null;
}
}
}
function suspendWatchersTree() {
suspendWatchersTreeRecursive(scope, false);
}
function unsuspendWatchersTree() {
unsuspendWatchersTreeRecursive(scope, false);
}
scope.inView = function(evnt, model, htmlElementId, triggeringEvent, isInView, inViewPart) {
if (!isInView) {
suspendWatchersTree();
_suspended = true;
}
if ((isInView) && (_suspended)) {
unsuspendWatchersTree();
_watchersMap = {};
_suspended = false;
}
}
}
}
});
Upon initialization this directive removes all watchers from current scope and all child scopes (not sure if it captures also isolated scopes).
Then, when the element is in view it adds back the watchers, and removes them when out of view.
I know that it doesn't necessarily captures all watchers, as some watchers might be added post linking, but that seems negligible, and they will be removed once the element comes in view and out of view again. If I could somehow hook to the watchers addition and add them to the map when suspsended that could be nice but I guess its not a must.
This seems to be working well but I'm not sure what are the caveats for such approach. I'm uncomfortable with playing around with angular internals and messing up things and reaching unstable conditions.
Any ideas and remarks will be greatly appreciated.
I've updated the above code with the most recent changes.
Further testing showed it works fairly well, some watches might not be suspended at initialization but that's a price I can live with. It greatly enhanced my digest loop and boosted the app performance by removing unrequired watches for out of viewport elements.

How to trigger $destroy event of already removed element scope?

Consider the following directive:
class HasPermissionDirective {
constructor(PermissionService) {
this.restrict = 'A';
this.priority = 1005;
this.PermissionService = PermissionService;
}
compile(element, attrs) {
let permission = _.trim(attrs.hasPermission),
hide = _.get(attrs, 'hideOnly'),
$element = angular.element(element);
if (permission && !this.PermissionService.hasPermission(permission)) {
if (!_.isUndefined(hide)) {
hide = _.trim(hide);
if (hide === 'visibility') {
$element.css('visibility', 'hidden');
} else {
$element.hide();
}
} else {
$element.remove();
}
}
}
link($scope, $element, attrs, ctrl) {
$scope.$destroy();
}
}
HasPermissionDirective.$inject = ['PermissionService'];
The problem is now, the $scope.$destroy() is executed always, for every element-scope the directive is attached to (of course).
When I'm now add a "isRemoved" member variable and set it to true in case the element was removed and do the following in the link function:
if (this.isRemoved) {
$scope.$destroy();
}
Of course the $scope.$destroy() is triggered for every element-scope as soon as at least one element is removed, cause the directive is handled as a singleton not as an instance.
I cannot add any information to the element node since it seems to be removed after compile and is only a comment node "ngInclude: undefined" (no, I don't remove the node, I add an data-attribute and want to get it inside of the link function: $element.data('remove', true) and then want to $destroy and remove()). EDIT: This seems to be the transclude-behavior of the ngInclude-directive.
If I remove the $scope.$destroy() from the link-function and just remove the node, the ngInclude-directive is still running ...
Waht I want? I just want to remove the element from DOM while compile time since the current user has not the permission to see this element/directive/view and I also want to avoid further processing of directives (in my case the ng-include which should not unnecessary request templates (since our server will respond 401 anyway) and so on).
UPDATE: I guess i need a way to set the terminal option inside of the compile function to stop the processing of the upcoming directives. My problem is that ngInclude is running even though the element was removed before.
Solution found! I also have to to use transclusion (it was clear after I was checking the impl of ngIf) and its only possible inside of (pre/post)link function(s), so here the impl. for someone who runs into similar problems:
class HasPermissionDirective {
constructor(PermissionService) {
this.restrict = 'A';
this.priority = 1011; // high prio so the element is removed before all other directives can be processed
this.transclude = 'element';
this.$$tlb = true; // BAD! But w/o, transclusion for different directives won't work :(
this.PermissionService = PermissionService;
}
preLink($scope, $element, attrs, ctrl, $transclude) {
let $newScope = $scope.$new(),
hide = _.get(attrs, 'hideOnly');
$transclude($newScope, ($clone) => {
if (!this.PermissionService.hasPermission(_.trim(attrs.hasPermission))) {
if (!_.isUndefined(hide)) {
hide = _.trim(hide);
if (hide === 'visibility') {
$clone.css('visibility', 'hidden');
} else {
$clone.hide();
}
} else {
$newScope.$destroy();
$newScope = null;
$clone.remove();
$clone = null;
}
} else {
// in case the user has the permission we have to attach the element to the DOM (cause of transclusion)
$element.after($clone);
}
});
}
}
HasPermissionDirective.$inject = ['PermissionService'];
I did also outsource the implementation to a controller so I can reuse the logic, but I wan't to provide a complete example for clarification :)

$compile an HTML template and perform a printing operation only after $compile is completed

In a directive link function, I want to add to document's DIV a compiled ad-hoc template and then print the window. I try the following code and printer preview appears, but the data in preview is still not compiled.
// create a div
printSection = document.createElement('div');
printSection.id = 'printSection';
document.body.appendChild(printSection);
// Trying to add some template to div
scope.someVar = "This is print header";
var htmlTemplate = "<h1>{{someVar}}</h1>";
var ps = angular.element(printSection);
ps.append(htmlTemplate);
$compile(ps.contents())(scope);
// What I must do to turn information inside printSection into compiled result
// (I need later to have a table rendered using ng-repeat?)
window.print();
// ... currently shows page with "{{someVar}}", not "This is print header"
Is it also so that $compile is not synchronous? How I can trigger window.print() only after it finished compilation?
you just need to finish the current digestion process to be able to print
so changing
window.print();
to
_.defer(function() {
window.print();
});
or $timeout, or any deferred handler.
will do the trick.
The other way (probably the 'right' approach) is to force the newly compilated content's watchers to execute before exiting the current $apply phase :
module.factory("scopeUtils", function($parse) {
var scopeUtils = {
/**
* Apply watchers of given scope even if a digest progress is already in process on another level.
* This will only do a one-time cycle of watchers, without cascade digest.
*
* Please note that this is (almost) a hack, behaviour may be hazardous so please use with caution.
*
* #param {Scope} scope : scope to apply watchers from.
*/
applyWatchers : function(scope) {
scopeUtils.traverseScopeTree(scope, function(scope) {
var watchers = scope.$$watchers;
if(!watchers) {
return;
}
var watcher;
for(var i=0; i<watchers.length; i++) {
watcher = watchers[i];
var value = watcher.get(scope);
watcher.fn(value, value, scope);
}
});
},
traverseScopeTree : function(parentScope, traverseFn) {
var next,
current = parentScope,
target = parentScope;
do {
traverseFn(current);
if (!(next = (current.$$childHead ||
(current !== target && current.$$nextSibling)))) {
while(current !== target && !(next = current.$$nextSibling)) {
current = current.$parent;
}
}
} while((current = next));
}
};
return scopeUtils;
});
use like this :
scopeUtils.applyWatchers(myFreshlyAddedContentScope);

KO.Computed equivalent in Angular / Breeze Initializer

Trying to get a more in-depth understanding of how Angular treats data binding and understanding it better and one thing is difficult to get my head around -
In Knockout I use a computed to keep track of changes to a property. In Angular it to move this logic into the view, which is a it trivial to me, but if that is the way to do it I understand.
My question is when I am initializing a new entity with Breeze/Angular how do I create computed-like properties that are notified when changes occur to the entities property?
myEntity.fullName = ko.computed(function () {
return myEntity.firstName + ' ' + myEntity.LastName;
});
in Angular would the equivalent be
myEntity.fullName = function () {
return myEntity.firstName + ' ' + myEntity.LastName;
};
And does that properly track the entity?
You are correct to simply make it a function. If your entity as shown is added to the $scope, then you would access the property like so:
<span class="fullname">{{ user.fullName() }}</span>
Whenever Angular runs a $digest cycle, it will check for a change to the bound property. In this instance, it means it will call the fullName() function and check to see if the result has changed. If it has, anything that has a $watch attached to that item — including simple binding — will be notified of the change.
One caveat of this technique, however, is to make sure that the operations being performed within your function are relatively fast, and also have no side effects. Bound functions like this will be called many times throughout the application.
If you need to have a more complex function, it would be better to handle that within the controller, and update a property on the object manually when it changes.
I found the answer on the following website. If you don't do something similar, what you will find is that all functions are ran during the digest phase and are not triggered by the change of a dependent observable or property. The solution below allows you to only trigger the function when a value it uses is changed.
http://www.jomendez.com/2015/02/06/knockoutjs-computed-equivalent-angularjs/
Explains how to duplicate the subscribe and computed feature in knockoutjs
var myViewModel = {
personName: ko.observable('Bob')
};
myViewModel.personName.subscribe(function(oldValue) {
alert("The person's previous name is " + oldValue);
}, null, "beforeChange");
This is what I found as result of my research (this is the AngularJs equivalent) Using the $scope.$watch method see the AngularJs life cycle https://docs.angularjs.org/guide/scope
$scope.myViewModel = {
personName: 'Bob'
};
$scope.$watch(‘myViewModel.personName’, function(newValue, oldValue){
//we are able to have both the old and the new value
alert("The person's previous name is " + oldValue);
});
//knockout computed
var isVendorLoading = ko.observable(),
isCustomerLoading = ko.observable(),
isProgramLoading = ko.observable(),
isWaiting = ko.observable();
var isDataLoading = ko.computed(function () {
return isVendorLoading() || isCustomerLoading() || isProgramLoading() || isPositionLoading() || isWaiting();
});
This is the AngularJs Equivalent for KnockoutJs computed:
$scope.isDataLoading = false;
$scope.$watch(
function (scope) {
//those are the variables to watch
return { isVendorLoading: scope.isVendorLoading, isCustomerLoading: scope.isCustomerLoading, isProgramLoading: scope.isProgramLoading, isWaiting: scope.isWaiting };
},
function (obj, oldObj) {
$timeout(function () {
//$timeout used to safely include the asignation inside the angular digest processs
$scope.isDataLoading = obj.isVendorLoading || obj.isCustomerLoading || obj.isProgramLoading || obj.isPositionLoading || obj.isWaiting;
});
},
true
);

Looking for a better way to loop through and display one element at a time in AngularJS

I'm building an app that has animations in it and I need it to work better. I want to be able to go through a series of divs at a specified animation interval and display one at a time. And I want each series to have its own speed and its own divs.
Here's what I have so far (also copied below): http://jsfiddle.net/ollerac/shkq7/
Basically, I'm looking for a way to put the setInterval on a property of of the animatedBox so I can create a new animatedBox with custom properties. But every time I try to do this it breaks.
HTML
<div ng-app ng-controller="BoxController">
<div class="layer" ng-repeat="layer in animatedBox.layers" ng-style="{ 'backgroundColor': layer.color}" ng-show="layer == animatedBox.selectedLayer"></div>
</div>
JAVASCRIPT
function buildBox () {
return {
color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
}
}
function BoxController ($scope) {
$scope.animatedBox = {
layers: [],
selectedLayer: null
};
for (var i = 0; i < 5; i++) {
$scope.animatedBox.layers.push(buildBox());
}
var i = -1;
setInterval(function () {
$scope.$apply(function() {
i++;
if (i < $scope.animatedBox.layers.length) {
$scope.animatedBox.displayThisLayer = $scope.animatedBox.layers[i];
} else {
i = 0;
$scope.animatedBox.selectedLayer = $scope.animatedBox.layers[i];
}
});
}, 500);
}
CSS
.layer {
width: 30px;
height: 30px;
position: absolute;
}
*Update*
Here's more along the lines of what I want to do:
updated jsFiddle: http://jsfiddle.net/ollerac/shkq7/2/
function buildBox () {
return {
color: '#' + (Math.random() * 0xFFFFFF << 0).toString(16)
}
}
function BoxController ($scope) {
$scope.animatedBox = {
layers: [],
selectedLayer: null,
selectedLayerIndex: -1,
updateSelectedLayer: function () {
var self = this;
if (self.layers.length) {
$scope.$apply(function() {
self.selectedLayerIndex++;
if (self.selectedLayerIndex < self.layers.length) {
self.selectedLayer = self.layers[self.selectedLayerIndex];
} else {
self.selectedLayerIndex = 0;
self.selectedLayer = self.layers[self.selectedLayerIndex];
}
});
}
}
};
for (var i = 0; i < 5; i++) {
$scope.animatedBox.layers.push(buildBox());
}
setInterval(function () {
$scope.animatedBox.updateSelectedLayer();
}, 500);
}
So now the object updates its own selectedLayer property. But I still need to call the setInterval that calls the update separately in order to get it to update. But I'd like the object to update itself and be completely independent. Can you think of a good way to do this because I'm really stuggling with it...
I guess this is more of a general javascript question, but I thought there might be an Angular way to handle this type of situation, like maybe using a directive or something would be appropriate.
Any suggestions would be much appreciated.
You are correct, I believe a directive is the right solution here. (This one was a fun one to work on, by the way. :)
When approaching a problem like this, I usually start by writing the HTML and controller that I'd wish I could write, if everything already worked. For this example, here's what I ended up with.
<div ng-controller="BoxController">
<div animated-boxes="colors"></div>
</div>
app.value('randomColor', function() {
var red = Math.floor(Math.random() * 255);
var green = Math.floor(Math.random() * 255);
var blue = Math.floor(Math.random() * 255);
return "rgb(" + red + "," + green + "," + blue + ")";
});
app.controller('BoxController', function($scope, randomColor) {
$scope.colors = [ randomColor(), randomColor() ];
});
Here, the controller is only responsible for setting some basic data on the scope--an array of colors; the DOM is very simple, only passing in that array to something called animated-boxes. randomColor has been moved into a service so it can be reused and tested more easily. (I also changed it a bit so it doesn't result in bad hex values.)
Now, the only part that doesn't already work is this thing called animated-boxes. Any time you want to interact with the DOM, or to trigger some behavior when an HTML attribute is used, we move to a directive.
We'll define our directive, injecting the $timeout service since we know we want to do timer-based stuff. The result of the directive will just be an object.
app.directive('animatedBoxes', function($timeout) {
return {
};
});
Since we want the directive to be self-contained and not mess up the outer scope in which it's contained, we'll give it an isolate scope (see the directive docs for more information, but basically this just means we have a scope that's not attached to the scope in which the directive lives except through variables we specify.)
Since we want to have access to the value passed in to the directive via the HTML attribute, we'll set up a bi-directional scope binding on that value; we'll call it colors.
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
}
};
});
We'll give it a simple template that loops over colors and outputs one of our divs per each color. Our ng-show indicates that the div should only be shown if the scope value selected is equal to $index, which is the array index of the current iteration of the ng-repeat loop.
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
},
template: "<div><div class='layer' ng-repeat='color in colors' " +
"ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
"</div></div>"
};
});
Now for the link function--the function that will handle the directive's logic. First, we want keep track of which box we're showing; in our ng-show, we used selected for this. We also want to keep track of how many boxes we have; we'll use $watch on our directive's scope to keep up with this.
link: function(scope, elem, attrs) {
scope.selected = 0;
var count = 0;
scope.$watch('colors', function(value) {
// whenever the value of `colors`, which is the array
// of colors passed into the directive, changes, update
// our internal count of colors
if (value) count = value.length;
else count = 0; // if `colors` is falsy, set count to 0
}, true); // `true` ensures we watch the values in the array,
// not just the object reference
}
Finally, we need to cycle through each box every so often. We'll do this with $timeout, which is a version of setTimeout that includes a scope $apply call (it does some other stuff, but we don't care about that now).
var nextBox = function() {
if (scope.selected >= count - 1) scope.selected = 0;
else scope.selected++;
// recursively use `$timeout` instead of `setInterval`
$timeout(nextBox, 500);
};
// kick off the directive by launching the first `nextBox`
nextBox();
If you put the entire directive so far together, you'll end up with this code (comments removed):
app.directive('animatedBoxes', function($timeout) {
return {
scope: {
colors: '=animatedBoxes'
},
template: "<div><div class='layer' ng-repeat='color in colors' " +
"ng-style='{backgroundColor: color}' ng-show='selected == $index'>" +
"</div></div>",
link: function(scope, elem, attrs) {
scope.selected = 0;
var count = 0;
scope.$watch('colors', function(value) {
if (value) count = value.length;
else count = 0;
}, true);
var nextBox = function() {
if (scope.selected >= count - 1) scope.selected = 0;
else scope.selected++;
$timeout(nextBox, 500);
};
nextBox();
}
};
});
A full working example, including comments and a little debugging area where you can see the value of colors and interact with it (so you can see how the directive responds to changes in the controller) can be found here: http://jsfiddle.net/BinaryMuse/g6A6Y/
Now that you have this, consider trying to apply this knowledge to allow the directive to have a variable speed by passing it in via the DOM, like we did with colors. Here's my result: http://jsfiddle.net/BinaryMuse/cHHKn/

Resources