From dc4a3d00d0a408953866dda8ef86f1b73d3c6e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 31 May 2018 14:51:30 -0700 Subject: [PATCH] fix(animations): always render end-state styles for orphaned DOM nodes (#24236) This patch ensures that any destination animation styling (state values) are always applied even if the DOM node is not apart of the DOM. PR Close #24236 --- .../src/dsl/animation_transition_factory.ts | 11 ++++---- .../src/render/transition_animation_engine.ts | 27 ++++++++++++------- .../transition_animation_engine_spec.ts | 17 ++++++++++++ 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/animations/browser/src/dsl/animation_transition_factory.ts b/packages/animations/browser/src/dsl/animation_transition_factory.ts index 0c4f9785fe..f1499e862b 100644 --- a/packages/animations/browser/src/dsl/animation_transition_factory.ts +++ b/packages/animations/browser/src/dsl/animation_transition_factory.ts @@ -38,8 +38,8 @@ export class AnimationTransitionFactory { build( driver: AnimationDriver, element: any, currentState: any, nextState: any, enterClassName: string, leaveClassName: string, currentOptions?: AnimationOptions, - nextOptions?: AnimationOptions, - subInstructions?: ElementInstructionMap): AnimationTransitionInstruction { + nextOptions?: AnimationOptions, subInstructions?: ElementInstructionMap, + skipAstBuild?: boolean): AnimationTransitionInstruction { const errors: any[] = []; const transitionAnimationParams = this.ast.options && this.ast.options.params || EMPTY_OBJECT; @@ -55,9 +55,10 @@ export class AnimationTransitionFactory { const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}}; - const timelines = buildAnimationTimelines( - driver, element, this.ast.animation, enterClassName, leaveClassName, currentStateStyles, - nextStateStyles, animationOptions, subInstructions, errors); + const timelines = skipAstBuild ? [] : buildAnimationTimelines( + driver, element, this.ast.animation, enterClassName, + leaveClassName, currentStateStyles, nextStateStyles, + animationOptions, subInstructions, errors); let totalTime = 0; timelines.forEach(tl => { totalTime = Math.max(tl.duration + tl.delay, totalTime); }); diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index 4864388f53..d388ac1676 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -740,10 +740,10 @@ export class TransitionAnimationEngine { private _buildInstruction( entry: QueueInstruction, subTimelines: ElementInstructionMap, enterClassName: string, - leaveClassName: string) { + leaveClassName: string, skipBuildAst?: boolean) { return entry.transition.build( this.driver, entry.element, entry.fromState.value, entry.toState.value, enterClassName, - leaveClassName, entry.fromState.options, entry.toState.options, subTimelines); + leaveClassName, entry.fromState.options, entry.toState.options, subTimelines, skipBuildAst); } destroyInnerAnimations(containerElement: any) { @@ -962,17 +962,24 @@ export class TransitionAnimationEngine { } } - if (!bodyNode || !this.driver.containsElement(bodyNode, element)) { - player.destroy(); + const nodeIsOrphaned = !bodyNode || !this.driver.containsElement(bodyNode, element); + const leaveClassName = leaveNodeMapIds.get(element) !; + const enterClassName = enterNodeMapIds.get(element) !; + const instruction = this._buildInstruction( + entry, subTimelines, enterClassName, leaveClassName, nodeIsOrphaned) !; + if (instruction.errors && instruction.errors.length) { + erroneousTransitions.push(instruction); return; } - const leaveClassName = leaveNodeMapIds.get(element) !; - const enterClassName = enterNodeMapIds.get(element) !; - const instruction = - this._buildInstruction(entry, subTimelines, enterClassName, leaveClassName) !; - if (instruction.errors && instruction.errors.length) { - erroneousTransitions.push(instruction); + // even though the element may not be apart of the DOM, it may + // still be added at a later point (due to the mechanics of content + // projection and/or dynamic component insertion) therefore it's + // important we still style the element. + if (nodeIsOrphaned) { + player.onStart(() => eraseStyles(element, instruction.fromStyles)); + player.onDestroy(() => setStyles(element, instruction.toStyles)); + skippedPlayers.push(player); return; } diff --git a/packages/animations/browser/test/render/transition_animation_engine_spec.ts b/packages/animations/browser/test/render/transition_animation_engine_spec.ts index a76398d2e2..ad6fc89fcd 100644 --- a/packages/animations/browser/test/render/transition_animation_engine_spec.ts +++ b/packages/animations/browser/test/render/transition_animation_engine_spec.ts @@ -623,6 +623,23 @@ const DEFAULT_NAMESPACE_ID = 'id'; const TRIGGER = 'fooTrigger'; expect(() => { engine.trigger(ID, element, TRIGGER, 'something'); }).not.toThrow(); }); + + it('should still apply state-styling to an element even if it is not yet inserted into the DOM', + () => { + const engine = makeEngine(); + const orphanElement = document.createElement('div'); + orphanElement.classList.add('orphan'); + + registerTrigger( + orphanElement, engine, trigger('trig', [ + state('go', style({opacity: 0.5})), transition('* => go', animate(1000)) + ])); + + setProperty(orphanElement, engine, 'trig', 'go'); + engine.flush(); + expect(engine.players.length).toEqual(0); + expect(orphanElement.style.opacity).toEqual('0.5'); + }); }); }); })();