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(
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); });

View File

@ -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;
}

View File

@ -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');
});
});
});
})();