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...
1 parent 89f081e commit e41faaa2a155a42bcc66952497a6f33866878508 @matsko matsko committed Apr 14, 2015
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) {
@@ -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
@@ -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.