AngularJS: Remove the directive of lower priority? - angularjs

I'm writing a directive which can remove ng-disabled programmically (e.g.: when current user has no permission for editing, remove ng-disabled and add a static disabled attribute directly.)
I read the official doc about $compile, it says I can specify priority option to decide the compilation order. The source code of latest AngularJS 1.7.9 shows that the priority of ngDisabled is 100, so I wrote this :
app.directive('removeNgDisabled', function ($compile) {
return {
priority: 101, // The priority of ngDisabled is 100
compile: function ($el, $attrs) {
const el = $el[0]
el.removeAttribute('ng-disabled') // ng-disabled should be removed before it is compiled... isn't it?!
}
}
})
<input ng-model="foo" ng-disabled="true" remove-ng-disabled=""/>
I don't know where do I get wrong, ng-disabled disappeared from DOM indeed, but ng-disabled still works...

It seems impossible to remove the directives have already existed on an HTML element as soon as Angular start to $compile that element.
The official document only says that:
The priority is used to sort the directives before their compile functions get called.
but it seems not mean to you can do any thing before the sorting. That is to say, when the "higher-ordering" directive has been compiled, the lower-ordering directives had already been push in the execution queue (I guess, I didn't dig into the internal implementation.) and had become unable to be removed.
I didn't dig into the internal implementation of the mechanism of directive in AngularJS; but at least, in the official document, there's no way to "cancel" or "remove" existed directives. So the only way to achieve similar behavior is to implement a homebrewed ng-disabled by yourself.

Related

Angular translation not updating placeholder of Chosen plugin within ng-include $scope

I have an ng-view with multiple instances of the localytics (angular) Chosen plugin. I also have an ng-include with one instance of the plugin. Both rendered on the same page.
I'm using the data-placeholder attribute to render a value which is filtered through the angular-translate plugin.
Initially I was having issues with all Chosen instances rendering the translated text when the method to update the language was being called.
I got around this by calling $route.reload() at the end of the method (not ideal, but acceptable).
I tried:
binding the values for the translations and the translate filter inline
setting them in controllers
watching the properties on the $scope (which never
triggered)
destroying the template before reloading the route
However, the placeholder within the ng-include refuses to update without the use of a hard refresh. Calling $window.location.reload() at the end of the method allows all instances to show the correct translation, but short of this I've not been able to find a way to fix the issue.
I'm assuming it's a scoping issue. Perhaps the Chosen plugin (which is a directive) creates its own scope, then the ng-include has its own scope, as does the ng-view.
All properties that are being translated, outside of the Chosen plugins, are working as expected.
Currently the angular-translate objects look like this:
var translationEN = {
SEARCH: {
'SEARCH-BTN': 'Search'
}
}
So I'm binding them inline as per the following:
<div ng-bind="'SEARCH.SEARCH-BTN' | translate">
I've also attempted some of the methods on $translate, such as $translate.refresh() to no avail.
If anyone has any ideas, any help and / or comments are very much appreciated.
Thanks in advance.
You can use the chosen attribute to pass in some configurations instead of using the data-placeholder attribute, like this:
<select chosen="{'placeholder_text_single': 'Select the options'}"></select>
Or you can write custom attributes that the chosen directive will also accept as configurations. However, when using attributes, the directive will evaluate the expression instead of using the literal value, which won't work as expected for translation purposes, as '{{ ... }}' is not a valid expression. The attributes would be like this:
<select chosen placeholder-text-single="'Select the options'"></select>
A similar problem occurred for me when the options were loaded by a promise.
With just an empty array, the translation worked fine, but putting the promise back in the code caused this behaviour.
A quick debugging in the chosen directive showed that, the element, from which the chosen angular plugin takes the template for the chosen widget, is not linked (or compiled... I'm really new with angular), it still contains the {{placeholder.string | translate}} value for the data-placeholder attribute, however, the attr.placeholder contains the tranlated value.
So this line sets wrong value as the default text: https://github.com/localytics/angular-chosen/blob/master/chosen.js#L57
I extended the chosen directive with a preLink function, which modified the element's data-placeholder attribute with the right value:
angular.module('myModule').directive('chosen', function() {
return {
priority : 1,
restrict: 'A',
link : {
pre : function(scope, element, attr, ngModel) {
var defaultText = attr.placeholder;
angular.element(element[0]).attr('data-placeholder', defaultText);
}
}
}
});

How to make directives work on elements I insert into the DOM later?

I need to conditionally apply ng-disabled to an element that is enclosed by a controller but does not exists at time of original compile. It is created later on by an AJAX callback and needs to be enabled/disabled based on certain conditions. I guess it needs a directive, but how do I go about it?
I need to use ng-disabled and no other solution as it is well handled by IE<11, which do not support pointer-events.
The real code is too complicated to be quoted here, but let me rephrase the problem.
A jQuery lib does something like:
$.get(url, function(){
$('<a class="btn"/>').appendTo(myDiv)
});
myDiv is within an angular controller. The <a/> element does not exist at time of compilation/directive linkage. Right after it gets appended, I should call some code to test and apply ng-disabled condition. Should it be a directive or a watch?
You could create a directive with an ngIf (so that it's created just when ngIf condition equals true. You can enable this condition after the response returns). Then in the link function you could:
link: function( scope, element, attrs ){
element.removeAttr("name-of-the-directive");
element.attrs("ng-disabled", "{{myExpression}}");
$compile( element)(scope);
}
The line element.removeAttr("name-of-the-directive"); is necessary to avoid infinite compiling loop.
When you're inserting code into the DOM manually, you need to compile it and link it to your scope first.
If your use-case allows it, you could try using ng-bind-html in your template and then just put the loaded code into a $scope property without needing to $compile it yourself.

AngularJS: manual $compile vs natural $compile over a recursive directive

I've tried to create my own recursive directive with AngularJS, wich calls itself to transform an object in a view with a pretty JSON format. Well, first I used a ng-include calling a script with the template, with a ng-if inside of it verifing if the current value was an object, if is, the template call itself.
I always think this is a ungly way to doind that, so I transform in a directive, much more simple.
More and less... Because I discover the world of recursive directives and found a lot of things, and most interesting. I even gave it a read in source code of Angular in github (I recommend you to read: https://github.com/angular/angular.js), with was a good things.
I searched so hard, and I thinkg I'm almost finding the awser that will cherish my heart! Because I learn a lot of new things and you guys, will help me.
Look my code in the link below: https://github.com/Daymannovaes/htmljs/blob/master/js/directives/recursiveDataTemplateDirective.js
my directive are: recursive-data-template="data", where data are an object. This directive will loop over the key and values of this object, and if the value was an object, will do this again. The condition are made with ng-if="isObject(value)".
Ok, my first problem was the infinite loop. I need to remove the content in the compile phase and then imperatively compile the content in the postLink phase. My question:
** Why the manual compile do not falls on the same problem of the infinite loop? **
I'm compiling the same content, no condition are made (if the if(!compiledContent) was removed, the infinite loop still not occurring), the diference (I think) are only they are maded in diferent place, but I wasn't able to find in internet someone who awser my question!
So, thank you!
(if the link doesn't working, here are the important code):
compile: function(templateElement, templateAttributes) {
/*
in compile time, we need to remove the innerHTML of template(url) because of its recursive.
Because of its recusiveness, when the $compile start to read the DOM tree and find a
recursiveDataTemplate directive (even its not will be evaluated all time by link function
because the ng-if, whatever) its start the do all the process again. So, we need the .remove()
*/
var templateDirectiveContent = templateElement.contents().remove();
var compiledContent;
return function($scope, linkElement, linkAttributes) {
/*
This verification avoid to compile the content to all siblings, because
when you compile the siblings, don't work (I don't know why, yet).
So, doing this we get only the top level link function (from each iteration)
*/
if(!compiledContent) {
compiledContent = $compile(templateDirectiveContent);
}
/*
Calling the link function passing the actual scope we get a clone object
wich contains the finish viewed object, the view itself, the DOM!!
Then, we attach the new dom in the element wich contains the directive
*/
compiledContent($scope, function(clone) {
linkElement.append(clone);
});
};
},
}
<ul>
<li data-ng-repeat="(key, value) in fields">
<span data-ng-if="!isNumber(key)">
{{key}}:
</span>
<span data-ng-if="isObject(value)" recursive-data-template="value"></span>
<span data-ng-if="!isObject(value)">
{{value}}
</span>
</li>
</ul>
I believe this excerpt from the official documentation is relevant to what you're asking:
Note: The compile function cannot handle directives that recursively use themselves in their own templates or compile functions. Compiling these directives results in an infinite loop and a stack overflow errors. This can be avoided by manually using $compile in the postLink function to imperatively compile a directive's template instead of relying on automatic template compilation via template or templateUrl declaration or manual compilation inside the compile function.
And going from the code you've provided, you've seem to have done what this note is suggesting - that is, manually compiling inside the function (postLink) you're returning for the compile property of your directive.
About the reason why compiling during the postLink rather than the compile phase avoids the infinite recursion, that's because all the elements of the DOM are compiled wether they are actually used or not, while the link will only be triggered when the element is actually linked: for insteance if a higher ng-if is falsy, its children element will not be preLinked, and thus neithr postLinked... At least from my understanding!
I recommend this good article: http://www.jvandemo.com/the-nitty-gritty-of-compile-and-link-functions-inside-angularjs-directives/

How to wait till angularjs is done with everything?

Similar to this question, I want to set focus on the last <select> whenever it gets added. As there's a single method doing it, I need no directive and no watch and no events. My function
$scope.addNew = function() {
$scope.items.push({});
$timeout(function() {
$("select").focus();
});
};
works nicely, except when called directly from the controller function definition like
angular.module('myModule').controller('MyCtrl', function($scope, $timeout) {
$scope.items = {};
...
$scope.addNew();
}
It looks like the timeout happens before the DOM gets constructed and $("select") is empty. With a delay of some 100 ms it works again, but this is a bad hack.
Contrary to what's said in the answer to the linked question, timeout doesn't suffice.
So what's a reliable way to wait for angularjs being really done with the DOM and everything?
Update:
It probably doesn't work because of the select to be focused being embedded in directives (including ng-repeat and some own ones) That's why there initially was no DOM element to focus on.
According to the comments, I need a directive. What's unclear is how exactly to do it. I tried and failed and found out a simpler solution.
What I need
I wasn't very explicit with this, so let me clarify.
I'm working with a table where each row contains some editable fields.
In addNew, I want to set focus on the first editable field of the new row.
In my case this happens to be the very last select.
It worked except at the very beginning, when I was adding the very first row from the controller body.
Why I'm opposed to using a directive
To my limited understanding, it's completely backwards:
A directive modifies the look, behavior, or structure of a given element. But there's no element which should be modified. I tried to put a directive on everything from the select itself to the whole body.
It needs to watch something or listen to an event, but I only want to invoke a function manually.
It didn't work (for me and others as the comments to the linked question shows).
I am going to try and influence you to use a directive here, just to perform the behavior.
Here is a fiddle.
Basic premise is adding the behavioral directive to the element inside repeater:
<table>
<tr ng-repeat="item in items">
<td>{{item}}: <input type="text" auto-focus/></td>
</tr>
</table>
Then your directive would put focus on the last added element:
app.directive('autoFocus', function(){
return function link(scope, elem){
elem[0].focus();
}
});
No watchers or events needed unless I am missing something that you require.
Code that manipulates the DOM should go in a directive, but if you switch to a directive and still have reason to wait until Angular is finished updating the scope and the dom, use $scope.$evalAsync:
$scope.$evalAsync( function() {
// This will wait until Angular is done updating the scope
// Do some stuff here
//
});
The solution was very trivial: Instead of calling $scope.addNew(); directly, I put it in $scope.init invoked from <form ng-init="init()">.
According to the documentation
The only appropriate use of ngInit is for aliasing special properties of ngRepeat, as seen in the demo below. Besides this case, you should use controllers rather than ngInit to initialize values on a scope.
this seems to be wrong (or maybe not, as ngRepeat si involved). I'm only using it to postpone the call to $scope.addNew();, where neither timeout nor posting events worked.

Managing communication between independent AngularJS directives independently

This is more of an organizational approach to solving this issue rather than a direct solution. My question itself is that if I have two directives which are not dependent on each other and can both work independently to serve their purposes. But if one of the directives is present then the other one needs to execute once the other is ready. In this case then what would be the logical way to make sure that it works out that way without the need to hardcode any function calls or events?
Lets say for example you have one directive which builds a grid of some sort:
angular.module('App').directive('appGrid',function() {
return function($scope, element) {
$scope.rows = ...
};
});
Then I have another directive that makes the element horizontally scrollable:
angular.module('App').directive('appPane',function() {
return function($scope, element) {
element.attachHorizontalScroll();
};
});
So an example of my HTML would look like this:
<div data-app-grid data-app-pane>
<div data-ng-repeat="row in rows">
<div data-ng-repeat="cell in row.cells">
{{ cell.data }}
</div>
</div>
</div>
Basically the appPane directive needs to run after the appGrid directive has been executed and the table is ready.
One solution I can think of is to watch the data to see when it is ready using the $scope.$watch method, but this poses a problem since the change can occur multiple times and this would be bad design to redundantly update the page and it also poses a problem if multiple directives are writing to the same scope variable that is being watched.
Another solution is to have the first directive emit an event (something like elementReady) and then have the 2nd directive take over. But what about if the first directive isn't there? Then how would the 2nd directive know when to do it's job? There could be another directive which is basically an empty directive which just fires the event for all other elements, but this is a bit of hack. Also what happens if multiple other directives fire the elementReady event?
One more solution is to create a 3rd directive which shares the logic between the two directives via a shared service. But this makes the 3rd directive fully reliant on both other directives as well as the shared services in between. This also require more, unnecessary testing code as well as actual code to write the directive (much more code compared to the 2nd solution, which would require only one + one lines of code).
Any ideas?
Have a look at the priority attribute of the directives.
Here is a copy of the exact description from the angular docs :
priority - When there are multiple directives defined on a single DOM element, sometimes it is necessary to specify the order in which
the directives are applied. The priority is used to sort the
directives before their compile functions get called. Higher priority
goes first. The order of directives within the same priority is
undefined.
you should be able to find it in
http://docs.angularjs.org/guide/directive
under the Writing directives (long version) --- Directives definition Object section.
Hope this answers your question.
I had a similar problem. I couldn't use priority, since the wiring occurred after clicking on the element. I solved it using $rootScope. Here is a simplified example:
link : function (scope, element, attrs) {
element.on('click', function() {
$rootScope.$emit('myEvent', myData);
});
}
In the other directive you 'listen' for myEvent:
link : function (scope, element, attrs) {
$rootScope.$on('myEvent', function(data) {
// do sth
});
}
Great question. I would use a combination of attribute(s) and event(s).
Since only the user of the directives (i.e., the person writing the HTML) knows that he/she wants the two directives to interact (i.e., run dependently), I think he/she needs to specify this somehow, and attributes seem like a good way (the only way?). Once the directives know they need to interact, they can use events to do the signaling.
So, if directive B needs to wait for directive A, one or more attributes can be used to specify who should do the waiting, who should fire an event, and/or what the event name is. (This of course can be expanded to more than two directives, and more than one event.) Some possible implementations:
<div data-app-grid data-app-pane idc-wait="appPane" idc-event="idc-appGridDone">
<div data-app-grid data-app-pane idc-wait="appPane" idc-emit="appGrid" idc-event="idc-appGridDone">
By examining the attributes, the appGrid directive can determine that it doesn't need to wait, but it does need to emit event "idc-appGridDone". Similarly, by examining the attributes, the appPane directive can determine that it needs to wait for an "idc-appGridDone" event before it runs.
If the directives don't find any of these special "inter-directive communication"/"idc-" attributes, they run normally (no events, no waiting).
The "(in)dependent inter-directive communication pattern" is born. ☺

Resources