From f42d317d2f17a9948d038bdd410324b4096d2597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 12 Oct 2017 15:19:10 -0700 Subject: [PATCH] fix(animations): ensure animateChild() works with all inner leave animations (#19006) (#19532) (#19693) PR Close #19693 --- .../src/render/transition_animation_engine.ts | 42 ++++++++--- .../animation_query_integration_spec.ts | 71 +++++++++++++++++++ 2 files changed, 102 insertions(+), 11 deletions(-) diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index dbb2ce4f16..ce0ca4231f 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -68,7 +68,7 @@ export class StateValue { get params(): {[key: string]: any} { return this.options.params as{[key: string]: any}; } - constructor(input: any) { + constructor(input: any, public namespaceId: string = '') { const isObj = input && input.hasOwnProperty('value'); const value = isObj ? input['value'] : input; this.value = normalizeTriggerValue(value); @@ -192,7 +192,7 @@ export class AnimationTransitionNamespace { } let fromState = triggersWithStates[triggerName]; - const toState = new StateValue(value); + const toState = new StateValue(value, this.id); const isObj = value && value.hasOwnProperty('value'); if (!isObj && fromState) { @@ -306,16 +306,13 @@ export class AnimationTransitionNamespace { } private _destroyInnerNodes(rootElement: any, context: any, animate: boolean = false) { + // emulate a leave animation for all inner nodes within this node. + // If there are no animations found for any of the nodes then clear the cache + // for the element. this._engine.driver.query(rootElement, NG_TRIGGER_SELECTOR, true).forEach(elm => { - if (animate && containsClass(elm, this._hostClassName)) { - const innerNs = this._engine.namespacesByHostElement.get(elm); - - // special case for a host element with animations on the same element - if (innerNs) { - innerNs.removeNode(elm, context, true); - } - - this.removeNode(elm, context, true); + const namespaces = this._engine.fetchNamespacesByElement(elm); + if (namespaces.size) { + namespaces.forEach(ns => ns.removeNode(elm, context, true)); } else { this.clearElementCache(elm); } @@ -603,6 +600,29 @@ export class TransitionAnimationEngine { private _fetchNamespace(id: string) { return this._namespaceLookup[id]; } + fetchNamespacesByElement(element: any): Set { + // normally there should only be one namespace per element, however + // if @triggers are placed on both the component element and then + // its host element (within the component code) then there will be + // two namespaces returned. We use a set here to simply the dedupe + // of namespaces incase there are multiple triggers both the elm and host + const namespaces = new Set(); + const elementStates = this.statesByElement.get(element); + if (elementStates) { + const keys = Object.keys(elementStates); + for (let i = 0; i < keys.length; i++) { + const nsId = elementStates[keys[i]].namespaceId; + if (nsId) { + const ns = this._fetchNamespace(nsId); + if (ns) { + namespaces.add(ns); + } + } + } + } + return namespaces; + } + trigger(namespaceId: string, element: any, name: string, value: any): boolean { if (isElementNode(element)) { this._fetchNamespace(namespaceId).trigger(element, name, value); diff --git a/packages/core/test/animation/animation_query_integration_spec.ts b/packages/core/test/animation/animation_query_integration_spec.ts index a414458014..08ab6edb46 100644 --- a/packages/core/test/animation/animation_query_integration_spec.ts +++ b/packages/core/test/animation/animation_query_integration_spec.ts @@ -2309,6 +2309,77 @@ export function main() { expect(childCmp.childEvent.totalTime).toEqual(1000); })); + it('should emulate a leave animation on a sub component\'s inner elements when a parent leave animation occurs with animateChild', + () => { + @Component({ + selector: 'ani-cmp', + template: ` +
+ +
+ `, + animations: [ + trigger( + 'myAnimation', + [ + transition( + ':leave', + [ + query('@*', animateChild()), + ]), + ]), + ] + }) + class ParentCmp { + public exp: boolean = true; + } + + @Component({ + selector: 'child-cmp', + template: ` +
+
+
+ `, + animations: [ + trigger( + 'myChildAnimation', + [ + transition( + ':leave', + [ + style({opacity: 0}), + animate('1s', style({opacity: 1})), + ]), + ]), + ] + }) + class ChildCmp { + } + + TestBed.configureTestingModule({declarations: [ParentCmp, ChildCmp]}); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(ParentCmp); + const cmp = fixture.componentInstance; + + cmp.exp = true; + fixture.detectChanges(); + + cmp.exp = false; + fixture.detectChanges(); + + let players = getLog(); + expect(players.length).toEqual(1); + const [player] = players; + + expect(player.element.classList.contains('inner-div')).toBeTruthy(); + expect(player.keyframes).toEqual([ + {opacity: '0', offset: 0}, + {opacity: '1', offset: 1}, + ]); + }); + it('should only mark outermost *directive nodes :enter and :leave when inserts and removals occur', () => { @Component({