diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index 74dc90c3fa..6581527dc3 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -13,13 +13,24 @@ import {AnimationTransitionInstruction} from '../dsl/animation_transition_instru import {AnimationTrigger} from '../dsl/animation_trigger'; import {ElementInstructionMap} from '../dsl/element_instruction_map'; import {AnimationStyleNormalizer} from '../dsl/style_normalization/animation_style_normalizer'; -import {ENTER_CLASSNAME, LEAVE_CLASSNAME, LEAVE_SELECTOR, NG_ANIMATING_CLASSNAME, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util'; +import {ENTER_CLASSNAME, LEAVE_CLASSNAME, NG_ANIMATING_CLASSNAME, NG_TRIGGER_CLASSNAME, NG_TRIGGER_SELECTOR, copyObj, eraseStyles, setStyles} from '../util'; import {AnimationDriver} from './animation_driver'; import {getOrSetAsInMap, listenOnPlayer, makeAnimationEvent, normalizeKeyframes, optimizeGroupPlayer} from './shared'; const EMPTY_PLAYER_ARRAY: AnimationPlayer[] = []; -const NOOP_FN = () => {}; +const NULL_REMOVAL_STATE: ElementAnimationState = { + namespaceId: '', + setForRemoval: null, + hasAnimation: false, + removedBeforeQueried: false +}; +const NULL_REMOVED_QUERIED_STATE: ElementAnimationState = { + namespaceId: '', + setForRemoval: null, + hasAnimation: false, + removedBeforeQueried: true +}; interface TriggerListener { name: string; @@ -37,6 +48,15 @@ export interface QueueInstruction { isFallbackTransition: boolean; } +export const REMOVAL_FLAG = '__ng_removed'; + +export interface ElementAnimationState { + setForRemoval: any; + hasAnimation: boolean; + namespaceId: string; + removedBeforeQueried: boolean; +} + export class StateValue { public value: string; public options: AnimationOptions; @@ -245,7 +265,7 @@ export class AnimationTransitionNamespace { }); } - private _onElementDestroy(element: any) { + clearElementCache(element: any) { this._engine.statesByElement.delete(element); this._elementListeners.delete(element); const elementPlayers = this._engine.playersByElement.get(element); @@ -267,14 +287,13 @@ export class AnimationTransitionNamespace { this.removeNode(elm, context, true); } else { - this._onElementDestroy(elm); + this.clearElementCache(elm); } }); } removeNode(element: any, context: any, doNotRecurse?: boolean): void { const engine = this._engine; - engine.markElementAsRemoved(element); if (!doNotRecurse && element.childElementCount) { this._destroyInnerNodes(element, context, true); @@ -295,12 +314,8 @@ export class AnimationTransitionNamespace { }); if (players.length) { - optimizeGroupPlayer(players).onDone(() => { - engine.destroyInnerAnimations(element); - this._onElementDestroy(element); - engine._onRemovalComplete(element, context); - }); - + engine.markElementAsRemoved(this.id, element, true, context); + optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element)); return; } } @@ -365,15 +380,11 @@ export class AnimationTransitionNamespace { // whether or not a parent has an animation we need to delay the deferral of the leave // operation until we have more information (which we do after flush() has been called) if (containsPotentialParentTransition) { - engine.queuedRemovals.set(element, () => { - engine.destroyInnerAnimations(element); - this._onElementDestroy(element); - engine._onRemovalComplete(element, context); - }); + engine.markElementAsRemoved(this.id, element, false, context); } else { // we do this after the flush has occurred such // that the callbacks can be fired - engine.afterFlush(() => this._onElementDestroy(element)); + engine.afterFlush(() => this.clearElementCache(element)); engine.destroyInnerAnimations(element); engine._onRemovalComplete(element, context); } @@ -447,7 +458,6 @@ export interface QueuedTransition { export class TransitionAnimationEngine { public players: TransitionAnimationPlayer[] = []; - public queuedRemovals = new Map any>(); public newHostElements = new Map(); public playersByElement = new Map(); public playersByQueriedElement = new Map(); @@ -462,6 +472,7 @@ export class TransitionAnimationEngine { public namespacesByHostElement = new Map(); public collectedEnterElements: any[] = []; + public collectedLeaveElements: any[] = []; // this method is designed to be overridden by the code that uses this engine public onRemovalComplete = (element: any, context: any) => {}; @@ -572,8 +583,9 @@ export class TransitionAnimationEngine { // special case for when an element is removed and reinserted (move operation) // when this occurs we do not want to use the element for deletion later - if (this.queuedRemovals.has(element)) { - this.queuedRemovals.delete(element); + const details = element[REMOVAL_FLAG] as ElementAnimationState; + if (details && details.setForRemoval) { + details.setForRemoval = false; } // in the event that the namespaceId is blank then the caller @@ -592,19 +604,27 @@ export class TransitionAnimationEngine { collectEnterElement(element: any) { this.collectedEnterElements.push(element); } removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void { - if (namespaceId) { - const ns = this._fetchNamespace(namespaceId); - if (!isElementNode(element) || !ns) { - this._onRemovalComplete(element, context); - } else { - ns.removeNode(element, context, doNotRecurse); - } + if (!isElementNode(element)) { + this._onRemovalComplete(element, context); + return; + } + + const ns = namespaceId ? this._fetchNamespace(namespaceId) : null; + if (ns) { + ns.removeNode(element, context, doNotRecurse); } else { - this.queuedRemovals.set(element, () => this._onRemovalComplete(element, context)); + this.markElementAsRemoved(namespaceId, element, false, context); } } - markElementAsRemoved(element: any) { this.queuedRemovals.set(element, NOOP_FN); } + markElementAsRemoved(namespaceId: string, element: any, hasAnimation?: boolean, context?: any) { + this.collectedLeaveElements.push(element); + element[REMOVAL_FLAG] = { + namespaceId, + setForRemoval: context, hasAnimation, + removedBeforeQueried: false + }; + } listen( namespaceId: string, element: any, name: string, phase: string, @@ -653,6 +673,22 @@ export class TransitionAnimationEngine { }); } + processLeaveNode(element: any) { + const details = element[REMOVAL_FLAG] as ElementAnimationState; + if (details && details.setForRemoval) { + // this will prevent it from removing it twice + element[REMOVAL_FLAG] = NULL_REMOVAL_STATE; + if (details.namespaceId) { + this.destroyInnerAnimations(element); + const ns = this._fetchNamespace(details.namespaceId); + if (ns) { + ns.clearElementCache(element); + } + } + this._onRemovalComplete(element, details.setForRemoval); + } + } + flush(microtaskId: number = -1) { let players: AnimationPlayer[] = []; if (this.newHostElements.size) { @@ -660,15 +696,19 @@ export class TransitionAnimationEngine { this.newHostElements.clear(); } - if (this._namespaceList.length && (this.totalQueuedPlayers || this.queuedRemovals.size)) { + if (this._namespaceList.length && + (this.totalQueuedPlayers || this.collectedLeaveElements.length)) { players = this._flushAnimations(microtaskId); } else { - this.queuedRemovals.forEach(fn => fn()); + for (let i = 0; i < this.collectedLeaveElements.length; i++) { + const element = this.collectedLeaveElements[i]; + this.processLeaveNode(element); + } } this.totalQueuedPlayers = 0; this.collectedEnterElements.length = 0; - this.queuedRemovals.clear(); + this.collectedLeaveElements.length = 0; this._flushFns.forEach(fn => fn()); this._flushFns = []; @@ -704,7 +744,17 @@ export class TransitionAnimationEngine { const enterNodes: any[] = allEnterNodes.length ? collectEnterElements(this.driver, allEnterNodes) : []; - this.queuedRemovals.forEach((fn, element) => addClass(element, LEAVE_CLASSNAME)); + const leaveNodes: any[] = []; + for (let i = 0; i < this.collectedLeaveElements.length; i++) { + const element = this.collectedLeaveElements[i]; + if (isElementNode(element)) { + const details = element[REMOVAL_FLAG] as ElementAnimationState; + if (details && details.setForRemoval) { + addClass(element, LEAVE_CLASSNAME); + leaveNodes.push(element); + } + } + } for (let i = this._namespaceList.length - 1; i >= 0; i--) { const ns = this._namespaceList[i]; @@ -788,10 +838,6 @@ export class TransitionAnimationEngine { allPreviousPlayersMap.forEach(players => players.forEach(player => player.destroy())); - const leaveNodes: any[] = bodyNode && allPostStyleElements.size ? - this.driver.query(bodyNode, LEAVE_SELECTOR, true) : - []; - // PRE STAGE: fill the ! styles const preStylesMap = allPreStyleElements.size ? cloakAndComputeStyles(this.driver, enterNodes, allPreStyleElements, PRE_STYLE) : @@ -861,14 +907,18 @@ export class TransitionAnimationEngine { // run through all of the queued removals and see if they // were picked up by a query. If not then perform the removal // operation right away unless a parent animation is ongoing. - this.queuedRemovals.forEach((fn, element) => { + for (let i = 0; i < leaveNodes.length; i++) { + const element = leaveNodes[i]; const players = queriedElements.get(element); if (players) { - optimizeGroupPlayer(players).onDone(fn); + removeNodesAfterAnimationDone(this, element, players); } else { - fn(); + const details = element[REMOVAL_FLAG] as ElementAnimationState; + if (details && !details.hasAnimation) { + this.processLeaveNode(element); + } } - }); + } rootPlayers.forEach(player => { this.players.push(player); @@ -888,7 +938,8 @@ export class TransitionAnimationEngine { elementContainsData(namespaceId: string, element: any) { let containsData = false; - if (this.queuedRemovals.has(element)) containsData = true; + const details = element[REMOVAL_FLAG] as ElementAnimationState; + if (details && details.setForRemoval) containsData = true; if (this.playersByElement.has(element)) containsData = true; if (this.playersByQueriedElement.has(element)) containsData = true; if (this.statesByElement.has(element)) containsData = true; @@ -979,7 +1030,8 @@ export class TransitionAnimationEngine { const element = timelineInstruction.element; // FIXME (matsko): make sure to-be-removed animations are removed properly - if (element['REMOVED']) return new NoopAnimationPlayer(); + const details = element[REMOVAL_FLAG]; + if (details && details.removedBeforeQueried) return new NoopAnimationPlayer(); const isQueriedElement = element !== rootElement; let previousPlayers: AnimationPlayer[] = EMPTY_PLAYER_ARRAY; @@ -1229,7 +1281,7 @@ function cloakAndComputeStyles( // there is no easy way to detect this because a sub element could be removed // by a parent animation element being detached. if (!value || value.length == 0) { - element['REMOVED'] = true; + element[REMOVAL_FLAG] = NULL_REMOVED_QUERIED_STATE; } }); valuesMap.set(element, styles); @@ -1280,17 +1332,14 @@ function removeClass(element: any, className: string) { } } -function setAttribute(element: any, attr: string, value: any) { - if (element.setAttribute) { - element.setAttribute(attr, value); - } else { - element[attr] = value; - } -} - function getBodyNode(): any|null { if (typeof document != 'undefined') { return document.body; } return null; } + +function removeNodesAfterAnimationDone( + engine: TransitionAnimationEngine, element: any, players: AnimationPlayer[]) { + optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element)); +}