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

Commit e41faaa

Browse files
committed
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.
1 parent 89f081e commit e41faaa

File tree

5 files changed

+120
-0
lines changed

5 files changed

+120
-0
lines changed

src/ng/animate.js

+20
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ var $$CoreAnimateQueueProvider = function() {
7272
enabled: noop,
7373
on: noop,
7474
off: noop,
75+
pin: noop,
7576

7677
push: function(element, event, options, domOperation) {
7778
domOperation && domOperation();
@@ -272,6 +273,25 @@ var $AnimateProvider = ['$provide', function($provide) {
272273
// be interpreted as null within the sub enabled function
273274
on: $$animateQueue.on,
274275
off: $$animateQueue.off,
276+
277+
/**
278+
* @ngdoc method
279+
* @name $animate#pin
280+
* @kind function
281+
* @description Associates the provided element with a host parent element to allow the element to be animated even if it exists
282+
* outside of the DOM structure of the Angular application. By doing so, any animation triggered via `$animate` can be issued on the
283+
* element despite being outside the realm of the application or within another application. Say for example if the application
284+
* was bootstrapped on an element that is somewhere inside of the `<body>` tag, but we wanted to allow for an element to be situated
285+
* as a direct child of `document.body`, then this can be achieved by pinning the element via `$animate.pin(element)`. Keep in mind
286+
* that calling `$animate.pin(element, parentElement)` will not actually insert into the DOM anywhere; it will just create the association.
287+
*
288+
* Note that this feature is only active when the `ngAnimate` module is used.
289+
*
290+
* @param {DOMElement} element the external element that will be pinned
291+
* @param {DOMElement} parentElement the host parent element that will be associated with the external element
292+
*/
293+
pin: $$animateQueue.pin,
294+
275295
enabled: $$animateQueue.enabled,
276296

277297
cancel: function(runner) {

src/ngAnimate/.jshintrc

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"ELEMENT_NODE": false,
2323
"NG_ANIMATE_CHILDREN_DATA": false,
2424

25+
"assertArg": false,
2526
"isPromiseLike": false,
2627
"mergeClasses": false,
2728
"mergeAnimationOptions": false,

src/ngAnimate/animateQueue.js

+24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
var NG_ANIMATE_ATTR_NAME = 'data-ng-animate';
4+
var NG_ANIMATE_PIN_DATA = '$ngAnimatePin';
45
var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
56
var PRE_DIGEST_STATE = 1;
67
var RUNNING_STATE = 2;
@@ -167,6 +168,12 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
167168
}
168169
},
169170

171+
pin: function(element, parentElement) {
172+
assertArg(isElement(element), 'element', 'not an element');
173+
assertArg(isElement(parentElement), 'parentElement', 'not an element');
174+
element.data(NG_ANIMATE_PIN_DATA, parentElement);
175+
},
176+
170177
push: function(element, event, options, domOperation) {
171178
options = options || {};
172179
options.domOperation = domOperation;
@@ -490,6 +497,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
490497
var parentAnimationDetected = false;
491498
var animateChildren;
492499

500+
var parentHost = element.data(NG_ANIMATE_PIN_DATA);
501+
if (parentHost) {
502+
parent = parentHost;
503+
}
504+
493505
while (parent && parent.length) {
494506
if (!rootElementDetected) {
495507
// angular doesn't want to attempt to animate elements outside of the application
@@ -521,6 +533,18 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
521533
// there is no need to continue traversing at this point
522534
if (parentAnimationDetected && animateChildren === false) break;
523535

536+
if (!rootElementDetected) {
537+
// angular doesn't want to attempt to animate elements outside of the application
538+
// therefore we need to ensure that the rootElement is an ancestor of the current element
539+
rootElementDetected = isMatchingElement(parent, $rootElement);
540+
if (!rootElementDetected) {
541+
parentHost = parent.data(NG_ANIMATE_PIN_DATA);
542+
if (parentHost) {
543+
parent = parentHost;
544+
}
545+
}
546+
}
547+
524548
if (!bodyElementDetected) {
525549
// we also need to ensure that the element is or will be apart of the body element
526550
// otherwise it is pointless to even issue an animation to be rendered

src/ngAnimate/shared.js

+7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ var isPromiseLike = function(p) {
2222
return p && p.then ? true : false;
2323
}
2424

25+
function assertArg(arg, name, reason) {
26+
if (!arg) {
27+
throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required"));
28+
}
29+
return arg;
30+
}
31+
2532
function mergeClasses(a,b) {
2633
if (!a && !b) return '';
2734
if (!a) return b;

test/ngAnimate/animateSpec.js

+68
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,74 @@ describe("animations", function() {
11141114
}));
11151115
});
11161116

1117+
describe('.pin()', function() {
1118+
var capturedAnimation;
1119+
1120+
beforeEach(module(function($provide) {
1121+
capturedAnimation = null;
1122+
$provide.factory('$$animation', function($$AnimateRunner) {
1123+
return function() {
1124+
capturedAnimation = arguments;
1125+
return new $$AnimateRunner();
1126+
};
1127+
});
1128+
}));
1129+
1130+
it('should allow an element to pinned elsewhere and still be available in animations',
1131+
inject(function($animate, $compile, $document, $rootElement, $rootScope) {
1132+
1133+
var body = jqLite($document[0].body);
1134+
var innerParent = jqLite('<div></div>');
1135+
body.append(innerParent);
1136+
innerParent.append($rootElement);
1137+
1138+
var element = jqLite('<div></div>');
1139+
body.append(element);
1140+
1141+
$animate.addClass(element, 'red');
1142+
$rootScope.$digest();
1143+
expect(capturedAnimation).toBeFalsy();
1144+
1145+
$animate.pin(element, $rootElement);
1146+
1147+
$animate.addClass(element, 'blue');
1148+
$rootScope.$digest();
1149+
expect(capturedAnimation).toBeTruthy();
1150+
1151+
dealoc(element);
1152+
}));
1153+
1154+
it('should adhere to the disabled state of the hosted parent when an element is pinned',
1155+
inject(function($animate, $compile, $document, $rootElement, $rootScope) {
1156+
1157+
var body = jqLite($document[0].body);
1158+
var innerParent = jqLite('<div></div>');
1159+
body.append(innerParent);
1160+
innerParent.append($rootElement);
1161+
var innerChild = jqLite('<div></div>');
1162+
$rootElement.append(innerChild);
1163+
1164+
var element = jqLite('<div></div>');
1165+
body.append(element);
1166+
1167+
$animate.pin(element, innerChild);
1168+
1169+
$animate.enabled(innerChild, false);
1170+
1171+
$animate.addClass(element, 'blue');
1172+
$rootScope.$digest();
1173+
expect(capturedAnimation).toBeFalsy();
1174+
1175+
$animate.enabled(innerChild, true);
1176+
1177+
$animate.addClass(element, 'red');
1178+
$rootScope.$digest();
1179+
expect(capturedAnimation).toBeTruthy();
1180+
1181+
dealoc(element);
1182+
}));
1183+
});
1184+
11171185
describe('callbacks', function() {
11181186
var captureLog = [];
11191187
var capturedAnimation = [];

0 commit comments

Comments
 (0)