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
This commit is contained in:
Matias Niemelä 2018-05-31 14:51:30 -07:00 committed by Miško Hevery
parent 8aa70c2477
commit dc4a3d00d0
3 changed files with 40 additions and 15 deletions

View File

@ -38,8 +38,8 @@ export class AnimationTransitionFactory {
build( build(
driver: AnimationDriver, element: any, currentState: any, nextState: any, driver: AnimationDriver, element: any, currentState: any, nextState: any,
enterClassName: string, leaveClassName: string, currentOptions?: AnimationOptions, enterClassName: string, leaveClassName: string, currentOptions?: AnimationOptions,
nextOptions?: AnimationOptions, nextOptions?: AnimationOptions, subInstructions?: ElementInstructionMap,
subInstructions?: ElementInstructionMap): AnimationTransitionInstruction { skipAstBuild?: boolean): AnimationTransitionInstruction {
const errors: any[] = []; const errors: any[] = [];
const transitionAnimationParams = this.ast.options && this.ast.options.params || EMPTY_OBJECT; const transitionAnimationParams = this.ast.options && this.ast.options.params || EMPTY_OBJECT;
@ -55,9 +55,10 @@ export class AnimationTransitionFactory {
const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}}; const animationOptions = {params: {...transitionAnimationParams, ...nextAnimationParams}};
const timelines = buildAnimationTimelines( const timelines = skipAstBuild ? [] : buildAnimationTimelines(
driver, element, this.ast.animation, enterClassName, leaveClassName, currentStateStyles, driver, element, this.ast.animation, enterClassName,
nextStateStyles, animationOptions, subInstructions, errors); leaveClassName, currentStateStyles, nextStateStyles,
animationOptions, subInstructions, errors);
let totalTime = 0; let totalTime = 0;
timelines.forEach(tl => { totalTime = Math.max(tl.duration + tl.delay, totalTime); }); timelines.forEach(tl => { totalTime = Math.max(tl.duration + tl.delay, totalTime); });

View File

@ -740,10 +740,10 @@ export class TransitionAnimationEngine {
private _buildInstruction( private _buildInstruction(
entry: QueueInstruction, subTimelines: ElementInstructionMap, enterClassName: string, entry: QueueInstruction, subTimelines: ElementInstructionMap, enterClassName: string,
leaveClassName: string) { leaveClassName: string, skipBuildAst?: boolean) {
return entry.transition.build( return entry.transition.build(
this.driver, entry.element, entry.fromState.value, entry.toState.value, enterClassName, 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) { destroyInnerAnimations(containerElement: any) {
@ -962,17 +962,24 @@ export class TransitionAnimationEngine {
} }
} }
if (!bodyNode || !this.driver.containsElement(bodyNode, element)) { const nodeIsOrphaned = !bodyNode || !this.driver.containsElement(bodyNode, element);
player.destroy(); 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; return;
} }
const leaveClassName = leaveNodeMapIds.get(element) !; // even though the element may not be apart of the DOM, it may
const enterClassName = enterNodeMapIds.get(element) !; // still be added at a later point (due to the mechanics of content
const instruction = // projection and/or dynamic component insertion) therefore it's
this._buildInstruction(entry, subTimelines, enterClassName, leaveClassName) !; // important we still style the element.
if (instruction.errors && instruction.errors.length) { if (nodeIsOrphaned) {
erroneousTransitions.push(instruction); player.onStart(() => eraseStyles(element, instruction.fromStyles));
player.onDestroy(() => setStyles(element, instruction.toStyles));
skippedPlayers.push(player);
return; return;
} }

View File

@ -623,6 +623,23 @@ const DEFAULT_NAMESPACE_ID = 'id';
const TRIGGER = 'fooTrigger'; const TRIGGER = 'fooTrigger';
expect(() => { engine.trigger(ID, element, TRIGGER, 'something'); }).not.toThrow(); 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');
});
}); });
}); });
})(); })();