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

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 :)

Related

Re-focus after click outside md-chips

After upgrading the Angular Material from version 1.1.1 to 1.1.4, the md-chips are not working as before.
Type a string in the entry and then click outside, the focus returns to the input.
I do not want this to happen.
With Angular Material 1.1.1:
https://youtu.be/LD2CxbuMxJg
With Angular Material 1.1.4:
https://youtu.be/dG1kKvU1Y0s
Can anybody help me, please?
In mdChipsCtrl there is a boolean variable responsible for returning focus to the entry called shouldFocusLastChip.
I overwritten the function by changing the value of this variable with the following directive:
angular.module('myApp').directive('mdChips', function () {
return {
restrict: 'E',
require: 'mdChips', // Extends the original mdChips directive
link: function (scope, element, attributes, mdChipsCtrl) {
mdChipsCtrl.appendChip = function (newChip) {
// Set to FALSE
this.shouldFocusLastChip = false;
if (this.useTransformChip && this.transformChip) {
var transformedChip = this.transformChip({'$chip': newChip});
// Check to make sure the chip is defined before assigning it, otherwise, we'll just assume
// they want the string version.
if (angular.isDefined(transformedChip)) {
newChip = transformedChip;
}
}
// If items contains an identical object to newChip, do not append
if (angular.isObject(newChip)){
var identical = this.items.some(function(item){
return angular.equals(newChip, item);
});
if (identical) return;
}
// Check for a null (but not undefined), or existing chip and cancel appending
if (newChip === null || this.items.indexOf(newChip) + 1) return;
// Append the new chip onto our list
var length = this.items.push(newChip);
var index = length - 1;
// Update model validation
this.ngModelCtrl.$setDirty();
this.validateModel();
// If they provide the md-on-add attribute, notify them of the chip addition
if (this.useOnAdd && this.onAdd) {
this.onAdd({ '$chip': newChip, '$index': index });
}
};
}
};

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.

$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);

Force angular to consider scope property "dirty"

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);

angularjs with ace code editor - forcing the editor to execute 'onblur' when model changes

I am using angularjs in conjunction with ui-ace, a library that has a directive for the popular ace library.
ui-ace
ace text editor
I have made some modifications to the directive because I need it to work with string[], instead of normal strings. Everything works fine except a strange situation when switching my core model. Here is how it is set up;
There is a grid with objects from my database.
When an item in the grid is clicked, the $scope.Model is populated with the information from the database
This includes a property called Scripting, which is a string[], and it is bound to the ace text editor.
the editor's text is set to $scope.Model.Scripting.join('\n')
this behavior is repeated in different ways in the editor's onChange and onBlur events.
Now, what is going wrong is that I have to actually click on the text editor to trigger the onBlur event before I click on an item in the grid. This has to be repeated each time, or the editor won't update. I cannot figure out why this is happening.
Here is the relevant code. I am going to link the whole directive, as well. The plunkr has everything needed to reproduce the issue, including exact instructions on how to do so.
Full Demonstration and Full Directive Live (Plunkr)
Relevant Directive Changes
return {
restrict: 'EA',
require: '?ngModel',
priority: 1,
link: function (scope, elm, attrs, ngModel) {
/**
* Corresponds to the ngModel, and will enable
* support for binding to an array of strings.
*/
var lines = scope.$eval(attrs.ngModel);
/*************************************************
* normal ui-ace code
************************************************/
/**
* Listener factory. Until now only change listeners can be created.
* #type object
*/
var listenerFactory = {
/**
* Creates a blur listener which propagates the editor session
* to the callback from the user option onBlur. It might be
* exchanged during runtime, if this happens the old listener
* will be unbound.
*
* #param callback callback function defined in the user options
* #see onBlurListener
*/
onBlur: function (callback) {
return function (e) {
if (angular.isArray(lines)) {
scope.$apply(function () {
ngModel.$setViewValue(acee.getSession().doc.$lines);
});
}
executeUserCallback(callback, acee);
};
}
};
// Value Blind
if (angular.isDefined(ngModel)) {
ngModel.$formatters.push(function (value) {
if (angular.isUndefined(value) || value === null) {
return '';
}
else if (angular.isArray(value)) {
return '';
}
// removed error if the editor is bound to array
else if (angular.isObject(value)) {
throw new Error('ui-ace cannot use an object as a model');
}
return value;
});
ngModel.$render = function () {
if (angular.isArray(lines)) {
session.setValue(scope.$eval(attrs.ngModel).join('\n'));
}
else {
// normal ui-ace $render behavior
}
};
}
// set the value when the directive first runs.
if (angular.isArray(lines)) {
ngModel.$setViewValue(acee.getSession().doc.$lines);
}
Looks like you set up ngModel.$formatters incorrectly for your array case.
Try changing:
else if (angular.isArray(value)) {
return '';
}
To:
else if (angular.isArray(value)) {
return value.join('');
}
Personally I think it would have been easier to pass joined arrays in to model and not modify the directive. Then you wouldn't have issues with future upgrades
DEMO

Resources