Executing code after AngularJS template is rendered - angularjs

I have a table that renders rows with ng-repeat. Inside one of the cells there is a select that is rendered with ng-options.
<tr ng-repeat="item in data.items" repeat-done>
<td >
...
<select class="selectpicker"
ng-model="person" ng-options="person.Surname for person in data.Persons track by person.Id">
<option value="">Introduce yourself...</option>
</select>
...
<td>
</tr>
When repeat is done I need to turn select into bootstrap-select (a nice looking dropdownlist). So after a little bit of researching I added the following directive:
app.directive('repeatDone', function () {
return function (scope, element, attrs) {
if (scope.$last) {
setTimeout(function() { $('.selectpicker').selectpicker(); }, 1);
};
};
});
which is specified at tr (see above).
It works. But I am a little bit worried whether there is a chance it will not work on slow PC/tablet/etc. As I understand AngularJS has an asynchronous nature. So while the last element of ng-repeat is being processed, it is still possible ng-options for this element (or may be for some previous element too) is not rendered. Or am I paranoiac?

You shouldn´t synchronize your directives using timeout. A lot of problems can appear.
You can use the option priority to sort when your directives are going to be used. Directives are executed on descendant order.
ngRepeat has priority 1000 (https://docs.angularjs.org/api/ng/directive/ngRepeat)
select has priority 0 (https://docs.angularjs.org/api/ng/directive/select)
If you declare your directive with a negative priority it will execute after ng-options.
app.directive('repeatDone', function () {
return {
priority: -5,
link: function (scope, element, attrs) {
$('.selectpicker').selectpicker();
}
}
});
According to Angular documentation:
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. Priority is defined as a number.
Directives with greater numerical priority are compiled first.
Pre-link functions are also run in priority order, but post-link
functions are run in reverse order. The order of directives with the
same priority is undefined. The default priority is 0.
https://docs.angularjs.org/api/ng/service/$compile

Related

Using Two-Way Binding with Custom Directive and ng-repeat

Starting this off by saying that I know a common answer for this is to put the ng-repeat inside the content of the directive, but in this case I can't figure out how that will work for this.
Basic Problem
As the charCounter increases throughout the letter spans, I need to access the offsetTop value of each span in the ng-repeat in order to do some things, (When the charCoutner gets to a new line (has offsetTop > 0), adjust the begin variable that is in the limitTo in the ng-repeat).
Struggling Point
I am not able to update a variable in the custom directive and make it accessible to the ng-repeat.
If I have the custom directive outside of the ng-repeat I have no access to the offsetTop of each span (but the begin variable updates).
<p><span shell counter="charCounter" begin="begin"><span ng-repeat="letter in data | limitTo: limit : begin track by $index"><span>{{letter}}</span></span></span></p>
What I'm thinking that needs to happen is this, but how do I get the begin variable to be updated?
<p><span positioner counter="charCounter" begin="begin" ng-repeat="letter in data | limitTo: limit : begin track by $index"><span>{{letter}}</span></span></p>
Code Pen Sample
I've been playing around with this on Code Pen, You can see this problem in code pen here:
UPDATE
I've taken the example one step further and also integrated r0m4n's feedback about upping the priority of this and #Amy Blankenship's about further clarifying what I am trying to do. Here is the updated CodePen. While this technically works, I'm not thinking I'd even need to do a custom directive now, since I'm accessing the element manually rather than using the element from the directive. I still have a delicate understanding of all this.
Your scope binding is having some priority issues. Take a look at using the priority property. Per the docs:
"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"
So you could do something like this:
myApp.directive('positioner', function($timeout) {
return {
restrict: 'A',
priority: 1001,
scope: {
letter: '=',
begin: '=',
counter: '='
},
link: function($scope, element, attr) {
$timeout(function() {
$scope.begin += 5;
console.info('begin', $scope.begin);
}, 1000);
}
};
});
https://docs.angularjs.org/api/ng/service/$compile

Angular, order of $formatters and $parsers

I have an existing 3rd party directive for which I need to modify model and view values before they are shown resp. saved to the model. As I would like to avoid modifying external code, I implemented an additional directive which is set via attribute and which is about to modify the data through the $formatters and $parsers pipeline.
Basically something like this:
app.directive('myModifyingDirective', function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, element, attrs, ngModelController) {
ngModelController.$formatters.push(function(modelValue) {
return 'modified_' + modelValue;
});
// similar for $parsers
}
};
});
Markup looks something like:
<third-party-directive my-modifying-directive ng-model='data'></third-party-directive>`
The problem is, that third-party-directive also contributes to the $formatters, and at the end, the third-party-directives's formatter is last entry in the $formatters array, and thus executed before my-modifying-directive.
However, I require my-modifying-directive to be executed first.
Is there any mechanism how I could influence the order of the $parsers?
You can set the priority of the directive so that it is higher or lower than the priority of the 3rd party directive:
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. Priority is defined as a number. Directives with greater numerical priority are compiled first. Pre-link functions are also run in priority order, but post-link functions are run in reverse order. The order of directives with the same priority is undefined. The default priority is 0.

Directive applied after filter (or bound to filtered result so Directive is called again)

See fiddle with html:
<div get-width class="output">{{ inputText | uppercase }}</div>
And directive:
myApp.directive('getWidth', [function () {
function link(scope, element, attrs) {
scope.textSize = getTextWidth(element.text(), "95px Arial");
}
return {
restrict: 'A',
link: link
};
}]);
Trying to have a directive calculate the width of text after all other child directives have been applied and filters have been applied. Unfortunately it always seems that the filters apply after directives. The width calculation is always run on "{{inputValue | uppercase}}" instead of on the evaluated values.
I'd be fine if the filter applied afterwards, but then caused the directive to be re-evaluated because the element changed. But I don't seem to be able to get the directive to bind to the element.
I'd prefer not to pass everything using arguments or pass the $filter service in and manually filter the input before calculating the width as I don't know what filters may be used. This directive may be used by other developers (it's just an example for now) and so it could contain any input and any number of chained filters. Is there a way evaluating the element before running getTextWidth()?
This function may be used to adjust elements in the page so I would like to avoid $timeout and causing extra paints (flicker).
Any help would be appreciated.

What is `priority` of ng-repeat directive can you change it?

Angular Documentation says: -
The compilation of the DOM is performed by the call to the $compile()
method. The method traverses the DOM and matches the directives. If a
match is found it is added to the list of directives associated with
the given DOM element. Once all directives for a given DOM element
have been identified they are sorted by priority and their
compile() functions are executed.
The ng-repeat directive I believe has a lower priority than custom directives, in certain use cases like dynamic id and custom directive. Does angular permit tinkering with priority of directives to choose execution of one before the other?
Yes, you can set the priority of a directive. ng-repeat has a priority of 1000, which is actually higher than custom directives (default priority is 0). You can use this number as a guide for how to set your own priority on your directives in relation to it.
angular.module('x').directive('customPriority', function() {
return {
priority: 1001,
restrict: 'E',
compile: function () {
return function () {...}
}
}
})
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. Priority is defined as a number. Directives with greater numerical priority are compiled first. The order of directives with the same priority is undefined. The default priority is 0.
AngularJS finds all directives associated with an element and processes it. This option tells angular to sort directives by priority so a directive having higher priority will be compiled or linked before others. The reason for having this option is that we can perform conditional check on the output of the previous directive compiled.
In the followed example, first add button and only after add class to current button:
Demo Fiddle
App.directive('btn', function() {
return {
restrict: 'A',
priority: 1,
link: function(scope, element, attrs) {
element.addClass('btn');
}
};
});
App.directive('primary', function() {
return {
restrict: 'A',
priority: 0,
link: function(scope, element, attrs) {
if (element.hasClass('btn')) {
element.addClass('btn-primary');
}
}
};
});

How to 'unwatch' an expression

Say I have an ng-repeat with a big array.
When ng-repeat runs, it adds every element of that array to an isolated scope, as well as having the array itself in a scope. That means that $digest checks the entire array for changes, and on top of that, it checks every individual element in that array for changes.
See this plunker as an example of what I'm talking about.
In my use case, I never change a single element of my array so I don't need to have them watched. I will only ever change the entire array, in which case ng-repeat would re-render the table in it's entirety. (If I'm wrong about this please let me know..)
In an array of (say) 1000 rows, that's 1000 more expressions that I don't need evaluated.
How can I deregister each element from the watcher while still watching the main array?
Perhaps instead of deregistering I could have more control of my $digest and somehow skip each individual row?
This specific case is actually an example of a more general issue. I know that $watch returns a 'deregisteration' function, but that doesn't help when a directive is registering the watches, which is most of the time.
To have a repeater with a large array that you don't watch to watch every item.
You'll need to create a custom directive that takes one argument, and expression to your array, then in the linking function you'd just watch that array, and you'd have the linking function programmatically refresh the HTML (rather than using an ng-repeat)
something like (psuedo-code):
app.directive('leanRepeat', function() {
return {
restrict: 'E',
scope: {
'data' : '='
},
link: function(scope, elem, attr) {
scope.$watch('data', function(value) {
elem.empty(); //assuming jquery here.
angular.forEach(scope.data, function(d) {
//write it however you're going to write it out here.
elem.append('<div>' + d + '</div>');
});
});
}
};
});
... which seems like a pain in the butt.
Alternate hackish method
You might be able to loop through $scope.$$watchers and examine $scope.$$watchers[0].exp.exp to see if it matches the expression you'd like to remove, then remove it with a simple splice() call. The PITA here, is that things like Blah {{whatever}} Blah between tags will be the expression, and will even include carriage returns.
On the upside, you might be able to just loop through the $scope of your ng-repeat and just remove everything, then explicitly add the watch you want... I don't know.
Either way, it seems like a hack.
To remove a watcher made by $scope.$watch
You can unregister a $watch with the function returned by the $watch call:
For example, to have a $watch only fire once:
var unregister = $scope.$watch('whatever', function(){
alert('once!');
unregister();
});
You can, of course call the unregister function any time you want... that was just an example.
Conclusion: There isn't really a great way to do exactly what you're asking
But one thing to consider: Is it even worth worrying about? Furthermore is it truly a good idea to have thousands of records loaded into dozens of DOMElements each? Food for thought.
EDIT 2 (removed bad idea)
$watch returns a function that unbinds the $watch when called. So this is all you need for "watchOnce":
var unwatchValue = scope.$watch('value', function(newValue, oldValue) {
// Do your thing
unwatchValue();
});
Edit: see the other answer I posted.
I've gone and implemented blesh's idea in a seperable way. My ngOnce directive just destroys the child scope that ngRepeat creates on each item. This means the scope doesn't get reached from its parents' scope.$digest and the watchers are never executed.
Source and example on JSFiddle
The directive itself:
angular.module('transclude', [])
.directive('ngOnce', ['$timeout', function($timeout){
return {
restrict: 'EA',
priority: 500,
transclude: true,
template: '<div ng-transclude></div>',
compile: function (tElement, tAttrs, transclude) {
return function postLink(scope, iElement, iAttrs, controller) {
$timeout(scope.$destroy.bind(scope), 0);
}
}
};
}]);
Using it:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
Note ng-once doesn't create its own scope which means it can affect sibling elements. These all do the same thing:
<li ng-repeat="item in contents" ng-once>
{{item.title}}: {{item.text}}
</li>
<li ng-repeat="item in contents">
<ng-once>
{{item.title}}: {{item.text}}
</ng-once>
</li>
<li ng-repeat="item in contents">
{{item.title}}: {{item.text}} <ng-once></ng-once>
</li>
Note this may be a bad idea
You can add the bindonce directive to your ng-repeat. You'll need to download it from https://github.com/pasvaz/bindonce.
edit: a few caveats:
If you're using {{}} interpolation in your template, you need to replace it with <span bo-text>.
If you're using ng- directives, you need to replace them with the right bo- directives.
Also, if you're putting bindonce and ng-repeat on the same element, you should try either moving the bindonce to a parent element (see https://github.com/Pasvaz/bindonce/issues/25#issuecomment-25457970 ) or adding track by to your ng-repeat.
If you are using angularjs 1.3 or above, you can use the single bind syntax as
<li ng-repeat="item in ::contents">{{item}}</li>
This will bind the value and will remove the watchers once the first digest cycle is run and the value changes from undefined to defined for the first time.
A very helpful BLOG on this.

Resources