How to manipulate DOM element after Angular digest cycle? - angularjs

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.

Related

Why element is not available in directive's link function

You have probably heard it as many times as I have. "Do all your DOM manipulation in directives". But no one ever seems to say what could happen if you actually do DOM manipulation outside a directive in Angular.
I have a problem that I managed to reproduce in this Plunk
I have made a very simple directive that just outputs the element to the console.
app.directive('dirre', function(){
return {
link: function(scope, element, attrs){
console.log({message:"dirrens linkFn", element: element, count: element.length})
}
}
});
I have two identical jquery UI accordions, the only difference is the way they are called. One is called in a controller and the other one in a directive. Calling accordion from a controller is of course something bad.
As you can see if you run the application there is a situation where one of the dirre-directives does not seem to have an element but there are no errors.
The same thing happens in a big application I'm working with right now. The problem seems to be that someone in our team decided to call Jquery UI's accordion in a controller and not in a directive.
I haven't been able to step through the code to see what actually happens but I strongly suspect that the DOM is modified while Angular is compiling and something goes wrong.
Is this a plausible explanation?
Is this an example of what can go wrong if you do DOM manipulations outside a directive?
The controller and the directive links function are called asynchronously.
Usually you can see directives being built before the main Controller complete. When the controller terminates, the directives update their watched variable (ngModel, $watch(something)...). Basically this is done with promises.
The link/compile function however is not called again. You have to compile, watch, apply the new DOM. Which basically means writing the similar code to angularjs.

Angular UI Bootstrap - Collapsing tab content

Using the AngularJS UI Directives for bootstrap, is there any way to collapse the tab content using the tag?
I have several tabs/pills with content, which will start collapsed (hidden). When any of the tabs is activated, the tab content should collapse open, and stay open until a close button is clicked, which will close the collapsable section.
In the controller, I set $scope.isCollapsed to true. Each of the tabs have the ng-click which calls openCollapse(), which sets isCollapsed to false. If I add the collapse="isCollapsed" directive right to the tag, then the tabs disappear too, not just the content.
How can I fix this?
It took some figuring out, but it is possible!
The main problem I had was with scoping issues and transclusion. I hadn't run into transclusion yet (I'm fairly new to Angular), so I'm still wrapping my head around it a bit.
I tried a few different ways, and a couple others might have worked, if I understood transclusion a bit better, but in the end, this was the simplest for me, and I got it working.
So basically I had to do 4 main things to get this working.
Open up ui.bootstrap-tpls-0.11.0.js (or whichever version # you're using). Do a search for angular.module("template/tabs/tabset.html". In the <div class=\"tab-content\">\n" tag, add collapse=\"isCollapsed\".
The tag (and everything in it) gets replaced when compiled, and this is the code that it is replaced with, so this way we can directly put the collapse directive where we need to.
Also in ui.bootstrap-tpls-0.11.0.js, do a search for .directive('tabset'. Inside the link: function(scope, element, attrs) {, add: scope.isCollapsed = scope.$parent.isCollapsed'
Here we're linking the transcluded scope's isCollapsed to the isCollapsed that is being set in your initial controller (you could also just put initialize isCollapsed in the controller in the next step, instead of just linking it, but I'd already initialized it in my controller and I'd linked it trying to do another method)
Still in ui.bootstrap-tpls-0.11.0.js, do a search for .controller('TabsetController'. Inside this controller, add:
$scope.$on('openCol', function(event){
$scope.isCollapsed = false;
});
$scope.$on('closeCol', function(event){
$scope.isCollapsed = true;
});
What we're doing here is adding event listeners inside the tabset's transcluded scope. What we're going to do at the end, is create an event broadcast. I also added a .$watch(), just so I could see if it was changing:
$scope.$watch('isCollapsed', function(){
console.log("isCollapsed has changed, it is now: " + $scope.isCollapsed);
});
Lastly, in the view's controller (or whichever controller contains the .openCollapse() and .closeCollapse()), change your functions from editing this scopes isCollapsed, to:
$scope.openCollapse = function(){
$rootScope.$broadcast("openCol");
}
$scope.closeCollapse = function($event){
$rootScope.$broadcast("closeCol");
}
This will broadcast the events that are being listened for in the TabsetController. So now the proper scope of the isCollapsed is being changed, and is reflected in the code. Now watch that lovely tab-content collapsing.
Please let me know if I haven't got my terminology quite right, or if there's something inherently wrong with the way I'm doing it. Or, simply, if there are other ways. Always open to suggestions :)

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.

Creating an AngularJS Directive for jQuery UI Button

Update: Fiddle w/ full solution: http://jsfiddle.net/langdonx/VXBHG/
In efforts to compare and contrast KnockoutJS and AngularJS, I ran through the KnockoutJS interactive tutorial, and after each section, I'd rewrite it in AngularJS using what little I already knew + the AngularJS reference.
When I got to step 3 of the Creating custom bindings tutorial, I figured it would be a good time to get spun up on Angular Directives and write a custom tag. Then I failed miserably.
I'm up against two issues that I haven't been able to figure out. I created a new Fiddle to try and wrap my head around what was going on...
1 (fiddle): I figured out my scoping issue, but, is it possible to just passthrough ng-click? The only way I could get it to work is to rename it to jqb-click which is a little annoying.
2 (fiddle): As soon as I applied .button() to my element, things went weird. My guess is because both Angular and jQuery UI are manipulating the HTML. I wouldn't expect this, but Angular seems to be providing its own span for my button (see line 21 of the JavaScript), and of course so is jQuery UI, which I would expect. I hacked up the HTML to get it looking right, but even before that, none of the functionality works. I still have the scope issue, and there's no template binding. What am I missing?
I understand that there's an AngularUI project I should be taking a look at and I can probably pull off what I'm trying to do with just CSS, but at this point it's more about learning how to use Directives rather than thinking this is a good idea.
You can create an isolated scope in a directive by setting the scope parameter, or let it use the parent scope by not setting it.
Since you want the ng-click from parent scope it is likely easiest for this instance to use the parent scope within directive:
One trick is to use $timeout within a directive before maniplulatig the DOM within a templated directive to give the DOM time to repaint before the manipulation, otherwise it seems that the elements don't exist in time.
I used an attribute to pass the text in, rather than worrying about transclusion compiling. In this manner the expression will already have been compiled when the template is added and the link callback provides easy access to the attributes.
<jqbutton ng-click="test(3)" text="{{title}} 3"></jqbutton>
angular.module('Components', [])
.directive('jqbutton', function ($timeout) {
return {
restrict: 'E', // says that this directive is only for html elements
replace: true,
template: '<button></button>',
link: function (scope, element, attrs) {
// turn the button into a jQuery button
$timeout(function () {
/* set text from attribute of custom tag*/
element.text(attrs.text).button();
}, 10);/* very slight delay, even using "0" works*/
}
};
});
Demo: http://jsfiddle.net/gWjXc/8/
Directives are very powerful, but also have a bit of a learning curve. Also in comparison of angular to knockout, angular is more of a meta framework that in the long run has far more flexibilty than knockout
Very helpful reading for understanding scope in directives:
https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance

AngularJs: Run Directives after View is fully rendered

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.

Resources