Applying animation outside of the ngApp - angularjs

My goal is to create a directive that can be applied to a given element, so that when the element is clicked, a modal is created. I would like to have the modal created and appended to the body node, which is outside of my ng-app element. Due to requirements of more than one app on a page, I can't put ng-app on the <html> or <body> tags. Yet for proper z positioning, I would to place the modal element as high up in the body as I can.
My directive looks like this:
var module = angular.module('Test', ['ngAnimate']);
module.directive('modal', function($compile, $animate) {
function link(scope, element, attr) {
element.on('click', function () {
var modal = $compile('<div class="modal"></div>')(scope);
scope.$apply(function () {
$animate.enter(modal, angular.element(document.body));
});
});
}
return {
link: link,
scope: {}
};
});
When I use $animate.enter to append the modal to the body, it is appended but the animation does not run. My HTML looks like this:
<body>
<div ng-app="Test">
<button modal>Open Modal</button>
</div>
</body>
If I move the ng-app from the div to the body, then the animation works. But I can't do this because I need to have the option of placing more than one ng-app on a given page.
Is it possible?
Working (or not-working) example:
http://plnkr.co/edit/vUi2PmLjea36nrJ9i3R2?p=preview

The short answer is: No you can't.
(At least Angular seems not to be designed to allow it).
The somewhat longer answer is: No, you can't. Here is why:
The reason is how $animate is currently implemented:
In your case, it adds the elements= to the DOM (to document.body in particular) and then checks to see if it should proceed with the animation specific stuff (or if animations are "disabled").
According to the source code, the function that checks if animations are "disabled" is:
function animationsDisabled(element, parentElement) {
if (rootAnimateState.disabled) return true;
if(isMatchingElement(element, $rootElement)) {
return rootAnimateState.disabled || rootAnimateState.running;
}
do {
//the element did not reach the root element which means that it
//is not apart of the DOM. Therefore there is no reason to do
//any animations on it
if(parentElement.length === 0) break;
var isRoot = isMatchingElement(parentElement, $rootElement);
var state = isRoot ? rootAnimateState : parentElement.data(NG_ANIMATE_STATE);
var result = state && (!!state.disabled || state.running || state.totalActive > 0);
if(isRoot || result) {
return result;
}
if(isRoot) return true;
}
while(parentElement = parentElement.parent());
return true;
}
As you can see, since your modal is not a child (but a sibling) of the $rootElement, isRoot will always be false and the do-while loop will run until there is no parent-element. At that point if(parentElement.length === 0) break; will break the loop and the function will return true, thus cancelling the animation.
The longest answer is: Depends (on how badly you need it).
The available options (I can think of) are:
Accept your fate and find some other way (besides $animate) to perform your animations.
Create your own fork of angular-animate and change one line of code, so that animations are not cancelled even if the target-element is not a child of the $rootElement.
If you like to live dangerously: What if we "temporarily swapped the $rootElement" ?
I tried various approaches (which outside of the scope of this answer) and the only one I could make work(?), was swapping the HTML element associated with the jqLite object (i.e. $rootElement[0]).
I wrapped the functionality in a service (along with some convenience features, e.g. restoring the original element after a set period of time):
module.factory('myRootElement', function ($rootElement, $timeout) {
/* Save the original element for reference */
var original = $rootElement[0];
/* Fake the $rootElement for the specified
* period of time (in milliseconds) */
function fakeForMillis(millis) {
$rootElement[0] = document.body;
$timeout(function () {
$rootElement[0] = original;
}, millis || 0);
}
/* Return the Service object */
return {
fakeForMillis: fakeForMillis
};
});
Finally, you only need to temporarily swap the $rootElement fo the animation to take place. (Unfortunately, you have to specify the time required for the animation, but I bet there are better ways to find it out programmatically - again outside the scope of this answer.)
myRootElement.fakeForMillis(1000);
$animate.enter(modal, angular.element(document.body));
See, also, this short demo.
I have no idea what I am talking about and I have by no means investigated the consequences of this approach, so use at your own risk and don't be surprised if strange things come your way.

Related

Angular Directive Firing Multiple Times

I am newer to AngularJS and having an issue that I hope someone can point me in the right direction to figuring out. I have created a directive called sizeWatcher which is placed as an attribute into the HTML which essentially just gets the height of the element it's placed on and echos that height into a scope variable named style that I set onto another element through the use of ng-style="style".
I'm finding that whenever I open my accordion, the $watch fires on the directive but it's firing multiple times. I have a console.log in my $watch and am seeing 3 log entries, the first 2 are the same (guessing this happens on click before the accordion opens, and then the accordion opens and the 3rd log entry is the final height after the accordion is opened). The main issue is that the style variable is only getting set to the smaller heights before the accordion is expanded even though the log is registering the greater height as the last time the directive is hit -- How can I ignore the first $watch event firings and only act accordingly on the last and final run-through of the directive? Any insight into this would be greatly appreciated. Relevant code attached below:
TEMPLATE:
<div class="content-wrap" id="height-block" ng-style="style">
<!-- Other HTML etc... -->
<uib-accordion size-watcher close-others="oneAtATime">
<!-- Accordion Directive HTML.. -->
</uib-accordion>
</div>
JavaScript:
.directive("sizeWatcher", function () { //add size-watcher attribute to element on the page to have it echo its' height to the {{style}} scope
function link(scope, element, attrs) {
scope.$watch(function () { //watch element for changes
var height = element[0].offsetHeight;
console.log(height);
if (height > 150) {
scope.style = {
height: height + 'px'
};
}
});
}
return {
restrict: "AE", //attribute & element declarations
link: link
};
})
How can I ignore the first $watch event firings and only act
accordingly on the last and final run-through of the directive?
You can ignore watcher when new or old values are undefined and not equal to each other:
$scope.$watch(function () {
return element.height(); // or something else
},
function (newVal, oldVal) {
if (newVal !== undefined && oldVal !== undefined && newVal !== oldVal) {
// your stuff
if (newVal > 150) {
scope.style = {
height: newVal + 'px'
};
}
}
});
Anyways you can play with if statement regards to your needs
FYI, to improve performance $watch returns cancel callback so you can stop watcher whenever you want:
var cancelWatch = $scope.$watch(function () {
return element.height();
},
function (newVal, oldVal) {
if (<some condition>) {
cancelWatch();
}
});
Obviously an answer to this needs a link to the Angular-Documentation for $watch ;)
it states the following:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.
which probably explains your first call.
I'm guessing the second call happens because the accordion is rerendered after initialization (with a title/ or label or anything) which triggers the $digest and thus the $watch expression on the height.
Finally the third call happens when you open the accordion and the height actually changes.
To fix this you can compare the newValue and oldValue of the watched expression like Maxim Shoustin said in his answer. Here is an example (again taken from the Angular-docs)
scope.$watch(
// This function returns the value being watched. It is called for each turn of the $digest loop
function() { return food; },
// This is the change listener, called when the value returned from the above function changes
function(newValue, oldValue) {
if ( newValue !== oldValue ) {
// Only increment the counter if the value changed
scope.foodCounter = scope.foodCounter + 1;
}
}
);
However if you actually want to change the style of the element you might want to take a look into ng-class instead of manually registering any watchers!
This is happening because you are not using $watch correct way,
The first parameter to $watch is a variable which you want to watch(this can be a callback).
The second parameter to $watch is a callback which performs the desired action on change
So in your case it would be something like this
scope.$watch(
function () {
return element[0].offsetHeight;
},
function () { //watch element for changes
var height = element[0].offsetHeight;
console.log(height);
if (height > 150) {
scope.style = {
height: height + 'px'
};
}
}
)
Please notice the first function, so whenever the value it is returning changes, the second callback will execute
Hope this helps you

AngularJS: Pause $digest & watchers on hidden DOM elements

We're building a single page application which has multiple pages loaded as tabs. Only the content of one tab is visible at any given time (much like a browser), so we want to temporarily pause $digest and watchers from executing on those DOM nodes of the hidden tabs, until the user switches to that tab.
Is there a way to achieve this, so that the model continues to be updated for the background tabs, but the view updates based on a condition.
The following code illustrates the problem:
<div ng-repeat="tab in tabs" ng-show="tab.id == current_tab.id">
<!-- tab content with bindings -->
</div>
The goal is optimization.
I'm already aware of Scalyr directives, but I want a more specific solution without the extra features contained in Scalyr.
After some trial and error I've figured out the following directive which pauses all the children's $$watchers if the expression on the attribute evaluates to true, on false it restores any backed up $$watchers
app.directive('pauseChildrenWatchersIf', function(){
return {
link: function (scope, element, attrs) {
scope.$watch(attrs.pauseChildrenWatchersIf, function (newVal) {
if (newVal === undefined) {
return;
}
if (newVal) {
toggleChildrenWatchers(element, true)
} else {
toggleChildrenWatchers(element, false)
}
});
function toggleChildrenWatchers(element, pause) {
angular.forEach(element.children(), function (childElement) {
toggleAllWatchers(angular.element(childElement), pause);
});
}
function toggleAllWatchers(element, pause) {
var data = element.data();
if (data.hasOwnProperty('$scope') && data.$scope.hasOwnProperty('$$watchers') && data.$scope.$$watchers) {
if (pause) {
data._bk_$$watchers = [];
$.each(data.$scope.$$watchers, function (i, watcher) {
data._bk_$$watchers.push($.extend(true, {}, watcher))
});
data.$scope.$$watchers = [];
} else {
if (data.hasOwnProperty('_bk_$$watchers')) {
$.each(data._bk_$$watchers, function (i, watcher) {
data.$scope.$$watchers.push($.extend(true, {}, watcher))
});
}
}
}
toggleChildrenWatchers(element, pause);
}
}
}
});
Ok, the reason I asked you to show some code was because of the reason #Rouby stated.
For performance purposes, you can use ng-if instead of ng-show. ng-if removes or restores the element from the DOM.
<div ng-repeat="tab in tabs" ng-if="tab.id == current_tab.id">
<!-- tab content with bindings -->
</div>
ng-show is good to use when you want to style the hiding differently. For instance, you might want that a hidden element would only have its "body" hidden, with the header still appearing. It is possible with ng-show, you just have to define a CSS style for the class ng-hide.
If you want to keep the values of your $scope, you can bind those with a parent scope who would keep your variables intact.

AngularJS setting model value from directive and calling a parent scope function holds on to the previous value inside that function

js fiddle http://jsfiddle.net/suras/JzaV9/4/
This is my directive
'use strict';
barterApp.directive('autosuggest', function($timeout, $http) {
return {
restrict: "E",
scope: {
modelupdate:"=",
suggestions:"=",
urlsend:"#"
},
template: '<ul><li ng-repeat="suggest in suggestions" ng-click="updateModel(suggest)">{{suggest}}</li></ul>',
link: function (scope, element) {
scope.$watch('modelupdate', function() {
$timeout(function(){
$http.post(scope.urlsend, {q:scope.modelupdate}).then(function(data){
scope.suggestions = data.data;
console.log(data.data);
});
}, 3000);
});
scope.updateModel = function(value){
scope.modelupdate = value;
scope.$parent.getBookInfo();
}
}
};
});
controller is
barterApp.controller('openLibraryCtrl', ['$scope','$http',function ($scope,$http) {
$scope.title = "";
$scope.getBookInfo = function(value){
if($scope.title == "" || $scope.title == " ") //here title is 'r'(previous value)
{
return;
}
$http.get('/book_info.json?q='+$scope.title).then(function(res){
if(Object.keys(res).length !== 0)
{
data = res.data
console.log(data);
}
});
}
//here title is 'rails' (updated value from directive).
//( used a watch function here on model update
// and checked it but inside getBookInfo it is still 'r' )
}]);
in the update model function i set the model value and call the getBookInfo function on parent scope. but the thing here is when (this is a autocomplete) i enter the value in a input field that contains ng-model say for example 'r' then triggers the watch and i get suggestions from a post url (lets say "rails", "rock") and show it through the template as in the directive. when i click one of the suggestions (say 'rails') it triggers the updatemodel function in directive and sets the model value. its fine upto this but when i call the getBookInfo function in parent scope then $scope.title is 'r' inside the function (i checked with console log outside the function the model value was updated correctly as 'rails' ). again when i click 'rock' the model value inside getBookInfo is 'rails'.
i have no clue whats going on. (i also tested with watch function in controller the model gets updated correctly but the function call to getBookInfo holds back to the previous value)
view
<form ng-controller="openLibraryController">
<input type="text" ng-model="title" id="title" name="book[title]" />
<autosuggest modelupdate = "title" suggestions = "book_suggestions" urlsend="/book_suggestions.json"> </autosuggest>
</form>
I didn't look deep into it, but I suspect (with a high degree of confidence) that the parent scope has not been updated at the time of calling getBookInfo() (since we are still in the middle of a $digest cycle).
Not-so-good Solution 1:
You could immediately update the parent scope as well (e.g. scope.$parent.title = ...), but this is clearly a bad idea (for the same reasons as nr 2, but even more so).
Not-so-good Solution 2:
You could pass the new title as a parameter to getBookInfo().
Both solutions result in mixing controller code with directive code and creating a tight coupling between your components, which as a result become less reusable and less testable.
Not-so-bad Solution:
You could watch over the title and call getBookInfo() whenever it changes:
$scope.$watch('title', function (newValue, oldValue) {
getBookInfo();
});
This would be fine, except for the fact that it is totally unnecessary.
Better Solution:
Angular is supposed to take care of all that keep-in-sync stuff for us and it actually does. You don't have given much context on what is the purpose of calling getBookInfo(), but I am guessing you intend to update the view with some info on the selected book.
In that case you could just bind it to an element (using ng-bind) and Angular will make sure it is executed properly and timely.
E.g.:
<div>Book info: <span ng-bind="getBookInfo()"></span></div>
Further more, the autosuggest directive doesn't have to know anything about it. It should only care about displaying suggestions, manipulating the DOM (if necessary) and updating the specified model property (e.g. title) whenever a suggestion is clicked. What you do with the updated value should be none of its business.
(BTW, ideally the suggestions should be provided by a service.)
Below is a modified example (based on your code) that solves the problem. As stated above there are several methods of solving the problem, I just feel this one tobe cleaner and more aligned to the "Angular way":
Book title: <input type="text" ng-model="book.title" />
<autosuggest modelupdate="book.title"
suggestions="book.suggest()"></autosuggest>
Book info: <span ng-bind="book.getInfo()"></span>
Just by looking at the HTML (without knowing what is in JS), one can easily tell what is going on:
There is a text-field bound to book.title.
There is a custom autosuggest thingy that offers suggestions provided by book.suggest() and updates book.title.
There is a span that displays info about the book.
The corresponding directive looks like this:
app.directive('autosuggest', function() {
return {
restrict: 'E',
scope: {
modelupdate: '=',
suggestions: '&'
},
template:
'<ul><li ng-repeat="suggest in suggestions()" ' +
'ng-click="modelupdated = suggest">' +
'{{suggest}}</li></ul>'
};
});
As you can see, all the directive knows about is how to retrieve suggestions and what to update.
Note that the same directive can be used with any type of "suggestables" (even ones that don't have getBookInfo()); just pass in the right attributes (modelupdated, suggestions).
Note also, that we could remove the autosuggest element and the app would continue to work as expected (no suggestions of cource) without any further modification in HTML or JS (while in your version the book info would have stopped updating).
You can find the full version of this short demo here.

AngularJS - adding directive dynamically to an element

I have created a directive that check if data was entered to an HTML element in the following way:
var myApp = angular.module('myApp', []);
myApp.directive("uiRequired", function () {
return function (scope, elem, attrs) {
elem.bind("blur", function () {
var $errorElm = $('#error_testReq');
$errorElm.empty();
if (angular.isDefined(attrs) && angular.isDefined(attrs.uiRequired) && attrs.uiRequired == "true" && elem.val() == "") {
$errorElm.append("<li>Field value is required.</li>");
$errorElm.toggleClass('nfx-hide', false);
$errorElm.toggleClass('nfx-block', true);
}
else
{
$errorElm.toggleClass('nfx-hide', true);
$errorElm.toggleClass('nfx-block', false);
}
});
};
});
A working example can be seen here
My question:
Is there a way of adding the directive (uiRequired) I have created dynamically to elements on screen on document ready.
I want to put the new directive on selected HTML elements according to pre-defined list I have. I can not know in advance on which field this directive has to be on.
So I have to put it while page is rendering.
I have tried putting it dynamically myself while page is loading, however AngularJS did interpret it.
I could not find an example on the internet that does that.
Can anyone help me?
You can dynamically add directives to a page during the compilation phase when Angular is walking the DOM and matching elements to directives. Each step of the compilation process may transform the DOM tree ahead of it, but you should never modify elements that Angular has already compiled. This point is important to remember because adding directives to elements that have already been walked will have no effect. Of course, there ways around this. For example, you could re-compile and re-link previously walked elements. However I strongly advise against this as it may lead to unintended side effects such as memory leaks, and slow performance.
To dynamically add uiRequired directives, you can create a parent directive - let's call it uiRequiredApplier.
app.directive('uiRequiredApplier', function($scope) {
return {
restrict: 'A',
compile: function(element, attr) {
// you can apply complex logic figure out which elements
// you want to add the uiRequired attribute to
$('input', element).attr('uiRequired','');
return function(scope, element, attr) {
}
}
}
});
You can then apply the attribute like this:
<div ui-required-applier>
<input type="text" ng-model="name" />
</div>
When the uiRequiredApplier is compiled, it will dynamically add uiRequired attributes to selected elements using jQuery that have not been compiled yet. And when Angular walks the DOM, eventually it will compile and link the uiRequired attributes, which will add the desired validation behavior.

Clearing an interval when an element housing an angular directive is removed

I've built a simple directive that adds a javascript-based loading animation. It is operating with a window.setInterval() loop. This works great, but when loading is complete, I use ngSwitch to swap in my content, which removes the element housing the loading directive attribute from the page.
Ideally, I'd like to watch for this change and clear my interval so the animation calculations are not still running in the background. I have tried watching a custom function that evaluates the presence of the element on the page. I know the function works at detecting this, but it seems timing is an issue -- namely, as far as I can tell, the $watch itself is cleared when the directive attribute's element leaves the page. My $watch'ed expression therefore never detects a change and never calls its callback that clears the animation interval function.
Is there a recommended pattern for dealing with this type of situation?
Relevant snippet from my template:
<div ng-switch on="dataStatus">
<div ng-switch-when="loading">
<div loading-spinner></div>
</div>
<div ng-switch-when="haveData">
<!-- data dependent on content we were loading -->
</div>
</div>
Simplified version of my directive:
myModule.directive('loadingSpinner', function () {
var updateMySweetAnimation = function (element) { /* ... */ };
return {
link: function (scope, iElement, iAttrs) {
var spinner = window.setInterval(function () {
updateMySweetAnimation(iElement);
}, 100);
scope.$watch(function () {
return $(document).find(iElement).length;
}, function (present) {
if (!present) {
clearInterval(spinner);
}
});
}
};
});
When the element is cleared from the page by ng-switch, two things should happen:
The scope created for ng-switch-when, the element with your directive on, is destroyed. This kills your $watch and generates a $destroy event across the scope that you can watch with scope.$on('$destroy', ...).
The element is removed from the DOM. This generates a separate destroy event that you can watch with iElement.on('$destroy', ...).
They should happen in this order, looking at the latest stable release (1.0.8 - https://github.com/angular/angular.js/blob/v1.0.8/src/ng/directive/ngSwitch.js), so your scope and thus your watch should always be dead when the element is removed from the DOM.
You could avoid this problem by watching from the outer scope, where ng-switch is defined. Or you could watch dataStatus, the same condition as in your ng-switch, rather than looking for the results of the ng-switch seeing your condition change.
Both of these would probably work, but actually all you need to do, and in fact the normal pattern for this, is to just watch for one of the $destroy events and clean everything up there. As the interval feels more relevant to the view than the model, I would use the DOM event and replace your $watch with
iElement.on('$destroy', function(){
clearInterval(spinner);
});

Resources