AngularJs: Run Directives after View is fully rendered - angularjs

I'm new to all Angular World, and i'm facing a problem managing the directives.
I'm working on a project that uses Tabs, and i want to extend its functionality to handle overflowed tabs when window size is narrower that the width of all my tabs, so i need to calculate some dimensions of elements to achieve this.
The tabs are built from an Object in $scope, the problem is that the directive that calculates the dimensions is run before the view is fully compiled.
Plnkr Link: http://plnkr.co/edit/LOT4sZsNxnfmQ8zHymvw?p=preview
What i've tried:
loading the template in directive using templateUrl
working with transclude
to use $last in ng-repeat
try to reorder directive and create a dummy directive in every tab to trigger an event
I think that there is some AngularJs event, or property to handle this situation.
Please Guys Help :)

I make a branch on your plunker, I I think this is what you were looking for
I change your directive to this
.directive('onFinishRenderFilters', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit('ngRepeatFinished');
});
}
}
}
});
Then on your HTML I added the directive
<li ng-repeat="tab in tabs" on-finish-render-filters>
And the last thing I put the code I want to run after the repeat finish
$scope.$on('ngRepeatFinished', function (ngRepeatFinished) {
$scope.tabs = [{
index: 1,
title: "Tab 1",
link: "/tab1/"
},{
index: 2,
title: "Tab 2",
link: "/tab2/"
},{
index: 3,
title: "Tab 3",
link: "/tab3/"
},{
index: 4,
title: "Tab 4",
link: "/tab4/"
}];
});
http://plnkr.co/edit/TGduPB8FV47QvjjLnFg2?p=preview

You can add a watch on the actual DOM elements in order to know when your ng-repeat is loaded into the DOM.
Give your ul an id, say #tabs
$scope.$watch(
function () { return document.getElementById('tabs').innerHTML },
function(newval, oldval){
//console.log(newval, oldval);
//do what you like
}, true);

AngularJS does not seem to have reliable post-render callbacks for directives used within ng-repeat. [1]
Maybe you can solve this on CSS level by adding a "responsive" overflow-control element for specific screen widths.
https://groups.google.com/forum/#!topic/angular/SCc45uVhTt8
Update: There now is a way to do this using nested $timeouts. See: http://lorenzmerdian.blogspot.de/2013/03/how-to-handle-dom-updates-in-angularjs.html

According to your comments, you already have a solution that works ... and its currently the only solution that I know of: $timeout.
The question is, what do you want to achieve? You want a callback when all rendering is done. Ok, good, but how can angular know this? In a modern app which you are apparently developing when using angular, a rerender can happen all time! A js event can occur where data is loaded from backend, even right after initializing the app, which leads to rerendering. A mouse movement by the user can trigger rerendering of elements, which are even outside of angulars scope because of CSS rules. Any time, a rerender can happen.
So, with these considerations what can angularJS tell you about when rendering is done? It can only tell you, when current $apply and/or $digest chain is processed aka the browser event queue is empty for now. Exactly that is the only information that angularJS knows of. And you use $timeout with a delay of 0 for this.
Yes, you are right, in a bigger appliataion, that can be tricky to a point where its not reliable anymore. But in this case, you should think about how you can solve this in another way. F.i. if a rerender happens later, there must be a cause, like new data that is loaded from the backend. If so, you know when the data is loaded and therefore a rerender happens and therefore you know exactly when you have to update your widths.

Related

How to manipulate DOM element after Angular digest cycle?

I am trying to create a simple System Notification service in Angular that basically toggles a notification to become visible at the top of my window in a fixed position until the user interacts with it. The problem I am having is trying to position the element after Angular updates the DOM based off of my $scope
app.directive('skSystemUpdate', function(SystemUpdate){
return {
replace:true,
restrict: 'A',
template:'<div class="sk-system-update-wrap">\
<div class="sk-system-update" ng-show="showSystemUpdate">\
<sk-img class="iGreenSystemCheck"></sk-img>\
<span class="sk-system-update-text">{{ message }}</span>\
<a>Undo</a>\
</div>\
</div>',
link: function(scope, elem, attrs){
scope.$watch('showSystemUpdate', function(val){
if(val){
angular.extend(scope, SystemUpdate.getScope());
}
});
}
}
});
I have a service called SystemUpdate which is used to set up a scope variable based off of parameters (i.e. SystemUpdate.create('This is some text for the notification') ) and then flip the $rootScope.showSystemUpdate flag to true.
My directive is $watching for this change and when the flag is set to true, I get the scope variable from the SystemUpdate service and Angular takes care of the rest by applying the updated scope.message variable to the DOM.
My problem is I cannot figure out how to center my notification AFTER the DOM is manipulated.
Update
I still cannot find a solution besides using $timeout which isn't a great solution at all because you can clearly see the div "jump" between positions if the message changes... This has got to be something people have done before! This is something anyone can do with jQuery in a matter of seconds, but Angular is making this a real pain
I still would love to know how someone would go about manipulating the DOM after an ng-show/ng-hide, but in the meantime I was able to produce the effect I am looking for by simply wrapping my element in another div that is position:fixed and then just using text-align:center to center the smaller div within that. This is essentially how Angular UI Bootstrap centers their modals, but I would still like to know a true solution to the original problem if any Angular gurus would oblige.

d3-driven directive transition doesn't work inside ng-repeat

I am trying to include my d3 code inside a directive.
However, when my directive is inside a ng-repeat, the transitions won't take place.
Here's a JSFiddle of the issue: http://jsfiddle.net/hLtweg8L/1/ : You can see that when you click on the button, the rectangles position doesn't change smoothly, and 'append' is logged to the console once again.
My directive is the following:
myMod.directive('chart',function(){
return {
restrict:'A',
scope:{
data:'=',
},
link:function(scope,elem,attrs){
a=d3.select(elem[0]);
rects=a.selectAll("rect").data(scope.data,function(d));
rects.enter().append("rect")
.attr("x",function(d,i){console.log('append');return i*50+"px"})
.attr("y",100)
.attr("width",35)
.attr("height",function(d){return d.age*10+"px"})
.attr("fill","blue")
rects.transition().duration(200)
.attr("x",function(d,i){console.log('append');return i*50+"px"})
.attr("y",100)
.attr("width",35)
.attr("height",function(d){return d.age*10+"px"})
.attr("fill","blue")
}
}
})
As far as I understand it, the problem is that the elem passed inside the link function is not the same when the ng-repeat gets updated, that's why the append gets called more than once for the same data.
My question is: How can I use d3 transitions inside ng-repeat ? (Corrected Jsfiddle would help a lot). Or why is the elem not the same between different calls ? Can I tell angular that the dom shouldn't be removed and added again ?
A couple things are needed:
If you don't want ng-repeat to create a new element, you need to use the track by option so that it knows how to identify new vs. changed items:
<div ng-repeat="set in sets track by set.group">
D3 will not automatically see that the data has changed unless your directive watches for changes.
a=d3.select(elem[0]);
scope.$watch('data', function() {
updateGraph();
});
Here is an an alternate Fiddle:
http://jsfiddle.net/63tze4Lv/1/

Exposing directive controller to parent controller

I want to expose some of my directive's functionality through its controller (think a public API for this directive).
return {
restrict: 'E',
scope: {},
controller: function($scope) {
this.method1 = ...;
this.method2 = ...;
},
controllerAs: 'dir',
link: function (scope, element, attrs) { ... }
}
Then in my parent controller or template call dir.method1 to get stuff accomplished inside the directive. Any ideas if this is possible as of Angular 1.3?
I'd like to refrain from event passing or even function passing, I have heard this is possible although I have never seen an implementation of this.
It is possible, but your issue isn't to figure out how to get the API out. It's how to get TO it from the parent. You're creating an isolate scope through your use of the 'scope' option. You're also making an element-type directive, so I'm guessing you're doing something like this:
<my-parent>
<my-child></my-child>
</my-parent>
where <my-parent> is the parent directive, and <my-child> is the directive with the API you want to expose.
The real question is what you're trying to achieve here. There is totally a way to do what you're asking. Just because the scope is isolated doesn't mean you can't get to it. You can just iterate through the parent $scope's $$childHead/etc list to find the child whose API you want to access. Anything you define in the child like this:
$scope.myApiFunction = function() {
};
will be visible here. (Things you put into 'this' will not - use the $scope storage bucket instead.)
That means if you only had ONE child you could do something like this from the parent controller:
$scope.$$childHead.myApiFunction();
Simple. Also, very crude. There are lots of problems here: what if you have many children? What if this child with its API ends up one level down? Etc. It's breaking all kinds of OO patterns and it's going to get messy, fast.
Your question is very abstract - it might be good if you updated it with an exact example. Without that, let me guess at your goal. There are two ways to do something "like this" that are encouraged within Angular:
Services. Whenever you say "API", think Service first. A service is a singleton (automatically) so it's tailor-made for creating APIs. And services can use the Factory pattern to return objects of a type, so THOSE are tailor made for doing things like having a manager service handle, say, a buddy list in an IM client, with API methods for creating, removing, and finding buddies.
Items that add "optional" functionality to their parents when they're defined. Let's say we have three possible types of tooltips: tooltips that have a hover effect, those that have a click effect, and those that are triggered by a "walkthrough" system in some order. For this kind of thing, the easy thing to do is just reverse the API, like this:
Parent Controller:
$scope.tooltipHandler = {
showTooltip: function() {},
hideTooltip: function() {}
};
Child Controller:
$scope.$parent.tooltipHandler = {
showTooltip: function() {
// Do some real work
},
hideTooltip: function() {
// Do some real work
},
}
What happens here is if there's no tooltip defined, when the parent runs its walkthrough, nothing happens. If you add the blue tooltip display module, when the parent runs its walkthrough now, it's going to show blue tooltips.
Make sense?
I arrived here looking for a similar response. So far the best that I can figure is to do what Angular does with ngForm.
In the documentation clearly states
If the name attribute is specified, the form controller is published onto the current scope under this name.
This basically makes the form controller accessible from anywhere.
If you have the following DOM
<div ng-controller="MyCtrl as parentCtrl">
<form name="parentCtrl.frmCtrl">
<my-child-directive>
</form>
</div>
You can use require: 'ngForm' in my-child-directive to get access from an inside directive. If you are in the parent controller you can access it trough the frmCtrl variable.
Not sure if this is best practice. In ngForm the name attribute works well, but I don't even know how to call such an attribute for a custom directive.
Thats why I arrived here, I wanted to know if this is "The Angular way" and what types of convetions are on the subject.
Hope it helps!

AngularJS - Hook into Angular UI Bootstrap - Carousel Custom Next()?

I'm trying to implement a Angular UI Bootstrap carousel, but I'm using it for a Quiz. Therefore, I don't need normal Prev() and Next() buttons.
Rather, I need a custom Next() button that makes sure they've selected an answer before continuing on to next "slide" of question/answers.
How do I hook into the carousel directive functions to run my code and then use the carousel.next() function?
Thanks,
Scott
There is no official possibility to achieve this. but this can be hacked, if you want. But i think it is better grab the bootstrap original one, have a look the at angular bootstrap ui sources (carousel) and write your own wrapper.
Here comes the hack:
The first problem we have to solve is, how to access the CarouselController. There is no API that exposes this and the carousel directive creates an isolated scope. To get access to this scope wie need the element that represents the carousel after the directive has been instantiated by angular. To achieve this we may use a directive like this one, that must be put at the same element as our ng-controller:
app.directive('carouselControllerProvider', function($timeout){
return {
link:function(scope, elem, attr){
$timeout(function(){
var carousel = elem.find('div')[1];
var carouselCtrl = angular.element(carousel).isolateScope();
var origNext = carouselCtrl.next;
carouselCtrl.next = function(){
if(elem.scope().interceptNext()){
origNext();
}
};
});
}
};
});
We must wrap our code in a $timeout call to wait until angular has created the isolated scope (this is our first hack - if we don't want this, we had to place our directive under the carousel. but this is not possible, because the content will be replaced). The next step is to find the element for the carousel after the replacement. By using the function isolateScope we have access to the isolated Scope - e.g. to the CarouselController.
The next hack is, we must replace the original next function of the CarouselController with our implementation. But to call the original function later we have to keep this function for later use. Now we can replace the next function. In this case we call the function interceptNext of our own controller. We may access this function through the scope of the element that represents our controller. If the interceptNext returns true we call the original next function of the carousel. For sure you can expose the complete original next function to our controller - but for demonstration purposes this is sufficient. And we define our interceptNext function like this:
$scope.intercept = false;
$scope.interceptNext = function(){
console.log('intercept next');
return !$scope.intercept;
}
We can now control the next function of the carousel by a checkbox, that is bound to $scope.intercept. A PLUNKR demonstrates this.
I knew this is not exactly what you want, but how you can do this is demonstrated.
That hack is neat michael, I started working on something similar for my needs. But then realized I might as well finally dip my toe into contributing to the open source community.
I just submitted a pull request to update the library so the index of the current slide is exposed to the Carousel scope.
https://github.com/angular-ui/bootstrap/pull/2089
This change allows you to have per-slide behavior in the carousel template.
This change allowed me to override the base carousel template so that for instance on the first slide the "prev" button would not show or the "next" button would not show for the final slide.
You can add more complex logic for your own personal needs, but exposing the current index in this manner to the $scope is part of making this part of the framework more flexible.
EDIT
I made more changes for my personal use, but don't want quite yet to contribute this change which is closer to what you are needing.
I modified the carousel directive, adding the "finish" property to scope.
.directive('carousel', [function () {
return {
restrict: 'EA',
transclude: true,
replace: true,
controller: 'CarouselController',
require: 'carousel',
templateUrl: 'template/carousel/carousel.html',
scope: {
interval: '=',
noTransition: '=',
noPause: '=',
finish: '='
}
};
}])
Then, when I declare the carousel, I can pass in a method to that directive attribute which is a method in the scope of the controller containing the carousel.
<carousel interval="-1" finish="onFinish">
...
</carousel>
This allows me to modify my template to have a button that looks like this:
<button ng-hide="slides().length-1 != currentIndex" ng-click="finish()" class="btn next-btn">finish<span class="glyphicon glyphicon-stats"></span></button>
So it only shows conditionally on the correct slide and with ng-click it is calling the carousel's $scope.finish() which is a pointer to a method in the controller I created for this application.
Make sense?
edit: This only works if you don't use sort functionality with ng-repeat. There is a bug which breaks the indexing of the slides for this kind of functionality.

Ways to improve AngularJS performance even with large number of DOM elements

I'm evaluating whether or not to use AngularJS for a web project, and I'm worried about the performance for a feature I need to implement. I would like to know if there's a better way to implement the functionality I'm trying to in AngularJS.
Essentially, it seems to me the time it takes AngularJS to react to an event is dependent on the number of DOM elements in the page, even when the DOM elements aren't being actively changed, etc. I'm guessing this is because the $digest function is traversing the entire DOM.. at least from my experiments, that seems to be the case.
Here's the play scenario (this isn't exactly what I'm really trying to do, but close enough for testing purposes).
I would like to have angularJS highlight a word as I hover over it. However, as the number of words in the page increases, there's a larger delay between when you hover over the word and when it is actually highlighted.
The jsfiddle that shows this: http://jsfiddle.net/czerwin/5qFzg/4/
(Credit: this code is based on a post from Peter Bacon Darwin on the AngularJS forum).
Here's the HTML:
<div ng-app="myApp">
<div ng-controller="ControllerA">
<div >
<span ng-repeat="i in list" id="{{i}}" ng-mouseover='onMouseover(i)'>
{{i}},
</span>
<span ng-repeat="i in listB">
{{i}},
</span>
</div>
</div>
</div>
Here's the javascript:
angular.module('myApp', [])
.controller('ControllerA', function($scope) {
var i;
$scope.list = [];
for (i = 0; i < 500; i++) {
$scope.list.push(i);
}
$scope.listB = [];
for (i = 500; i < 10000; i++) {
$scope.listB.push(i);
}
$scope.highlightedItem = 0;
$scope.onMouseover = function(i) {
$scope.highlightedItem = i;
};
$scope.$watch('highlightedItem', function(n, o) {
$("#" + o).removeClass("highlight");
$("#" + n).addClass("highlight");
});
});
Things to note:
- Yes, I'm using jquery to do the DOM manipulation. I went this route because it was a way to register one watcher. If I do it purely in angularJS, I would have to register a mouseover handler for each span, and that seemed to make the page slow as well.
- I implemented this approach in pure jquery as well, and the performance was fine. I don't believe it's the jquery calls that are slowing me down here.
- I only made the first 500 words to have id's and classes to verify that it's really just having more DOM elements that seems to slow them down (instead of DOM elements that could be affected by the operation).
Although an accepted answer exists allready, I think its important to understand why angularJS reacts so slow at the code you provided. Actually angularJS isnt slow with lots of DOM elements, in this case it's slow because of the ng-mouseover directive you register on each item in your list. The ng-mouseover directive register an onmouseover event listener, and every time the listener function gets fired, an ng.$apply() gets executed which runs the $diggest dirty comparision check and walks over all watches and bindings.
In short words: each time an element gets hovered, you might consume e.g. 1-6 ms for the internal
angular dirty comparision check (depending on the count of bindings, you have established). Not good :)
Thats the related angularJS implementation:
var ngEventDirectives = {};
forEach('click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(name) {
var directiveName = directiveNormalize('ng-' + name);
ngEventDirectives[directiveName] = ['$parse', function($parse) {
return {
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
return function(scope, element, attr) {
element.on(lowercase(name), function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
};
}
};
}];
}
);
In fact, for highlighting a hovered text, you probably would use CSS merely:
.list-item:hover {
background-color: yellow;
}
It is likely that with newer Angular Versions, your code as is, will run significantly faster. For angular version 1.3 there is the bind-once operator :: which will exclude once-binded variables from the digest loop. Having thousands of items, exluded will reduce the digest load significantly.
As with ECMAScript 6, angular can use the Observe class, which will make the dirty comparisiion check totaly obsolete. So a single mouseover would result internally in a single event callback, no more apply or diggesting. All with the original code. When Angular will apply this, I dont know. I guess in 2.0.
I think that the best way to solve performance issues is to avoid using high level abstractions (AngularJS ng-repeat with all corresponding background magic) in such situations. AngularJS is not a silver bullet and it's perfectly working with low level libraries. If you like such functionality in a text block, you can create a directive, which will be container for text and incapsulate all low level logic. Example with custom directive, which uses letteringjs jquery plugin:
angular.module('myApp', [])
.directive('highlightZone', function () {
return {
restrict: 'C',
transclude: true,
template: '<div ng-transclude></div>',
link: function (scope, element) {
$(element).lettering('words')
}
}
})
http://jsfiddle.net/j6DkW/1/
This is an old question now, but I think it's worth adding to the mix that Angular (since v1.3) now supports one time binding which helps slim down the digest loop. I have worked on a few applications where adding one time binding reduced the number of watches dramatically which led to improved performance.
ng-repeat is often responsible for adding a lot of watches, so you could potentially consider adding one time binding to the ng-repeat.
ng-repeat="i in ::list"
Here is a summary of a few techniques that can be used to avoid adding unnecessary watches
http://www.syntaxsuccess.com/viewarticle/547a8ba2c26c307c614c715e
Always profile first to find the real bottleneck. Sometimes it might be not something you initially suspect. I would suspect your own code first, then Angular (high number of watchers being the main feature leading to sluggish performance). I described how to profile and solve different performance problems in an Angular app in detailed blog post https://glebbahmutov.com/blog/improving-angular-web-app-performance-example/
Get zone.js from btford and run all functions in a zone to check their times, then create zones to handle ajax code (crud) and other zones for static code (apis).
Alternatively, limiting ng-repeat and/or disabling two-way binding on objects goes a long way atm.. a problem that web components already covers by using shadow dom leaving the top crispy to touch. still zone.js - watch the video through the link on plausibilities.
Well, I can see you are using $watch. Angular recomends $watch whenever it is very much needed. SCenarios like updating a variable through ng-model
http://jsfiddle.net/5qFzg/10/
Suraj

Resources