Permalink
Browse files

feat($animate): provide support for animations on elements outside of…

… $rootElement

Beforehand it was impossible to issue an animation via $animate on an
element that is outside the realm of an Angular app. Take for example a
dropdown menu where the menu is positioned with absolute positioning...
The element will most likely need to be placed by the `<body>` tag, but
if the angular application is bootstrapped elsewhere then it cannot be
animated.

This fix provides support for `$animate.pin()` which allows for an
external element to be virtually placed in the DOM structure of a host
parent element within the DOM of an angular app.
  • Loading branch information...
matsko committed Apr 14, 2015
1 parent 89f081e commit e41faaa2a155a42bcc66952497a6f33866878508
Showing with 120 additions and 0 deletions.
  1. +20 −0 src/ng/animate.js
  2. +1 −0 src/ngAnimate/.jshintrc
  3. +24 −0 src/ngAnimate/animateQueue.js
  4. +7 −0 src/ngAnimate/shared.js
  5. +68 −0 test/ngAnimate/animateSpec.js
View
@@ -72,6 +72,7 @@ var $$CoreAnimateQueueProvider = function() {
enabled: noop,
on: noop,
off: noop,
pin: noop,
push: function(element, event, options, domOperation) {
domOperation && domOperation();
@@ -272,6 +273,25 @@ var $AnimateProvider = ['$provide', function($provide) {
// be interpreted as null within the sub enabled function
on: $$animateQueue.on,
off: $$animateQueue.off,
/**
* @ngdoc method
* @name $animate#pin
* @kind function
* @description Associates the provided element with a host parent element to allow the element to be animated even if it exists
* outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the
* element despite being outside the realm of the application or within another application. Say for example if the application
* was bootstrapped on an element that is somewhere inside of the `<body>` tag, but we wanted to allow for an element to be situated
* as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind
* that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association.
*
* Note that this feature is only active when the `ngAnimate` module is used.
*
* @param {DOMElement} element the external element that will be pinned
* @param {DOMElement} parentElement the host parent element that will be associated with the external element
*/
pin: $$animateQueue.pin,
enabled: $$animateQueue.enabled,
cancel: function(runner) {
View
@@ -22,6 +22,7 @@
"ELEMENT_NODE": false,
"NG_ANIMATE_CHILDREN_DATA": false,
"assertArg": false,
"isPromiseLike": false,
"mergeClasses": false,
"mergeAnimationOptions": false,
@@ -1,6 +1,7 @@
'use strict';
var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
var PRE_DIGEST_STATE = 1;
var RUNNING_STATE = 2;
@@ -167,6 +168,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
},
pin: function(element, parentElement) {
assertArg(isElement(element), 'element', 'not an element');
assertArg(isElement(parentElement), 'parentElement', 'not an element');
element.data(NG_ANIMATE_PIN_DATA, parentElement);
},
push: function(element, event, options, domOperation) {
options = options || {};
options.domOperation = domOperation;
@@ -490,6 +497,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
var parentAnimationDetected = false;
var animateChildren;
var parentHost = element.data(NG_ANIMATE_PIN_DATA);
if (parentHost) {
parent = parentHost;
}
while (parent && parent.length) {
if (!rootElementDetected) {
// angular doesn't want to attempt to animate elements outside of the application
@@ -521,6 +533,18 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
// there is no need to continue traversing at this point
if (parentAnimationDetected && animateChildren === false) break;
if (!rootElementDetected) {
// angular doesn't want to attempt to animate elements outside of the application
// therefore we need to ensure that the rootElement is an ancestor of the current element
rootElementDetected = isMatchingElement(parent, $rootElement);
if (!rootElementDetected) {
parentHost = parent.data(NG_ANIMATE_PIN_DATA);
if (parentHost) {
parent = parentHost;
}
}
}
if (!bodyElementDetected) {
// we also need to ensure that the element is or will be apart of the body element
// otherwise it is pointless to even issue an animation to be rendered
View
@@ -22,6 +22,13 @@ var isPromiseLike = function(p) {
return p && p.then ? true : false;
}
function assertArg(arg, name, reason) {
if (!arg) {
throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required"));
}
return arg;
}
function mergeClasses(a,b) {
if (!a && !b) return '';
if (!a) return b;
@@ -1114,6 +1114,74 @@ describe("animations", function() {
}));
});
describe('.pin()', function() {
var capturedAnimation;
beforeEach(module(function($provide) {
capturedAnimation = null;
$provide.factory('$$animation', function($$AnimateRunner) {
return function() {
capturedAnimation = arguments;
return new $$AnimateRunner();
};
});
}));
it('should allow an element to pinned elsewhere and still be available in animations',
inject(function($animate, $compile, $document, $rootElement, $rootScope) {
var body = jqLite($document[0].body);
var innerParent = jqLite('<div></div>');
body.append(innerParent);
innerParent.append($rootElement);
var element = jqLite('<div></div>');
body.append(element);
$animate.addClass(element, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeFalsy();
$animate.pin(element, $rootElement);
$animate.addClass(element, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
dealoc(element);
}));
it('should adhere to the disabled state of the hosted parent when an element is pinned',
inject(function($animate, $compile, $document, $rootElement, $rootScope) {
var body = jqLite($document[0].body);
var innerParent = jqLite('<div></div>');
body.append(innerParent);
innerParent.append($rootElement);
var innerChild = jqLite('<div></div>');
$rootElement.append(innerChild);
var element = jqLite('<div></div>');
body.append(element);
$animate.pin(element, innerChild);
$animate.enabled(innerChild, false);
$animate.addClass(element, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeFalsy();
$animate.enabled(innerChild, true);
$animate.addClass(element, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
dealoc(element);
}));
});
describe('callbacks', function() {
var captureLog = [];
var capturedAnimation = [];

0 comments on commit e41faaa

Please sign in to comment.