Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
feat($animate): provide support for animations on elements outside of…
Browse files Browse the repository at this point in the history
… $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 16, 2015
1 parent 89f081e commit e41faaa
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 0 deletions.
20 changes: 20 additions & 0 deletions src/ng/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ var $$CoreAnimateQueueProvider = function() {
enabled: noop,
on: noop,
off: noop,
pin: noop,

push: function(element, event, options, domOperation) {
domOperation && domOperation();
Expand Down Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions src/ngAnimate/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"ELEMENT_NODE": false,
"NG_ANIMATE_CHILDREN_DATA": false,

"assertArg": false,
"isPromiseLike": false,
"mergeClasses": false,
"mergeAnimationOptions": false,
Expand Down
24 changes: 24 additions & 0 deletions src/ngAnimate/animateQueue.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions src/ngAnimate/shared.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
68 changes: 68 additions & 0 deletions test/ngAnimate/animateSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand Down

0 comments on commit e41faaa

Please sign in to comment.