fix(core): animations no longer silently exits if the element is not apart of the DOM (#13763)

This commit is contained in:
Matias Niemelä 2017-01-05 11:33:40 -08:00 committed by Igor Minar
parent 889b48d85f
commit 21030e9a1c
5 changed files with 52 additions and 39 deletions

View File

@ -25,6 +25,7 @@ import {AnimationPlayer, NoOpAnimationPlayer} from '../../src/animation/animatio
import {AnimationStyles} from '../../src/animation/animation_styles'; import {AnimationStyles} from '../../src/animation/animation_styles';
import {AnimationTransitionEvent} from '../../src/animation/animation_transition_event'; import {AnimationTransitionEvent} from '../../src/animation/animation_transition_event';
import {AUTO_STYLE, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata'; import {AUTO_STYLE, animate, group, keyframes, sequence, state, style, transition, trigger} from '../../src/animation/metadata';
import {Input} from '../../src/core';
import {isPresent} from '../../src/facade/lang'; import {isPresent} from '../../src/facade/lang';
import {TestBed, fakeAsync, flushMicrotasks} from '../../testing'; import {TestBed, fakeAsync, flushMicrotasks} from '../../testing';
import {MockAnimationPlayer} from '../../testing/mock_animation_player'; import {MockAnimationPlayer} from '../../testing/mock_animation_player';
@ -2243,38 +2244,52 @@ function declareTests({useJit}: {useJit: boolean}) {
}); });
describe('error handling', () => { describe('error handling', () => {
it('should recover if an animation driver or player throws an error during an animation', if (!getDOM().supportsWebAnimation()) return;
it('should not throw an error when an animation exists within projected content that is not bound to the DOM',
fakeAsync(() => { fakeAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [DummyIfCmp], declarations: [DummyIfCmp, DummyLoadingCmp],
providers: [{provide: AnimationDriver, useClass: ErroneousAnimationDriver}], providers: [{provide: AnimationDriver, useClass: WebAnimationsDriver}],
imports: [CommonModule] imports: [CommonModule]
}); });
TestBed.overrideComponent(DummyIfCmp, { TestBed.overrideComponent(DummyIfCmp, {
set: { set: {
template: ` template: `
<div [@myAnimation]="exp" (@myAnimation.start)="callback1($event)" (@myAnimation.done)="callback2($event)"></div> <dummy-loading-cmp [exp2]="exp">
<div [@myAnimation]="exp ? 'true' : 'false'" (@myAnimation.done)="callback()">world</div>
</dummy-loading-cmp>
`, `,
animations: [trigger('myAnimation', [transition( animations: [trigger('myAnimation', [transition(
'* => *', '* => *',
[ [
animate(1000, style({transform: 'noooooo'})), style({opacity: 0}),
animate(1000, style({opacity: 1})),
])])] ])])]
} }
}); });
TestBed.overrideComponent(
DummyLoadingCmp, {set: {template: `hello <ng-content *ngIf="exp2"></ng-content>`}});
const fixture = TestBed.createComponent(DummyIfCmp); const fixture = TestBed.createComponent(DummyIfCmp);
const cmp = fixture.componentInstance; const cmp = fixture.componentInstance;
let started = false; const container = fixture.nativeElement;
let done = false; let animationCalls = 0;
cmp.callback1 = (event: AnimationTransitionEvent) => started = true; cmp.callback = () => animationCalls++;
cmp.callback2 = (event: AnimationTransitionEvent) => done = true;
cmp.exp = false;
fixture.detectChanges();
flushMicrotasks();
expect(animationCalls).toBe(1);
expect(getDOM().getText(container).trim()).toEqual('hello');
cmp.exp = true; cmp.exp = true;
fixture.detectChanges(); fixture.detectChanges();
flushMicrotasks(); flushMicrotasks();
expect(started).toBe(true); expect(animationCalls).toBe(2);
expect(done).toBe(true); expect(getDOM().getText(container).trim()).toMatch(/hello[\s\r\n]+world/m);
})); }));
}); });
@ -2460,6 +2475,9 @@ class DummyIfCmp {
class DummyLoadingCmp { class DummyLoadingCmp {
exp: any = false; exp: any = false;
callback = () => {}; callback = () => {};
@Input('exp2')
exp2: any = false;
} }
@Component({ @Component({
@ -2571,11 +2589,3 @@ class ExtendedWebAnimationsDriver extends WebAnimationsDriver {
return player; return player;
} }
} }
class ErroneousAnimationDriver extends MockAnimationDriver {
animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string): WebAnimationsPlayer {
throw new Error();
}
}

View File

@ -262,12 +262,11 @@ export class DomRenderer implements Renderer {
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string, duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer { previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
try { if (this._rootRenderer.document.body.contains(element)) {
return this._animationDriver.animate( return this._animationDriver.animate(
element, startingStyles, keyframes, duration, delay, easing, previousPlayers); element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} catch (e) {
return new NoOpAnimationPlayer();
} }
return new NoOpAnimationPlayer();
} }
} }

View File

@ -21,5 +21,3 @@ export type RenderDebugInfo = typeof r._RenderDebugInfo;
export const RenderDebugInfo: typeof r.RenderDebugInfo = r.RenderDebugInfo; export const RenderDebugInfo: typeof r.RenderDebugInfo = r.RenderDebugInfo;
export type DebugDomRootRenderer = typeof r._DebugDomRootRenderer; export type DebugDomRootRenderer = typeof r._DebugDomRootRenderer;
export const DebugDomRootRenderer: typeof r.DebugDomRootRenderer = r.DebugDomRootRenderer; export const DebugDomRootRenderer: typeof r.DebugDomRootRenderer = r.DebugDomRootRenderer;
export type NoOpAnimationPlayer = typeof r._NoOpAnimationPlayer;
export const NoOpAnimationPlayer: typeof r.NoOpAnimationPlayer = r.NoOpAnimationPlayer;

View File

@ -10,7 +10,7 @@ import {APP_ID, Inject, Injectable, NgZone, RenderComponentType, Renderer, RootR
import {AnimationDriver, DOCUMENT} from '@angular/platform-browser'; import {AnimationDriver, DOCUMENT} from '@angular/platform-browser';
import {isBlank, isPresent, stringify} from './facade/lang'; import {isBlank, isPresent, stringify} from './facade/lang';
import {AnimationKeyframe, AnimationPlayer, AnimationStyles, NoOpAnimationPlayer, RenderDebugInfo} from './private_import_core'; import {AnimationKeyframe, AnimationPlayer, AnimationStyles, RenderDebugInfo} from './private_import_core';
import {NAMESPACE_URIS, SharedStylesHost, flattenStyles, getDOM, isNamespaced, shimContentAttribute, shimHostAttribute, splitNamespace} from './private_import_platform-browser'; import {NAMESPACE_URIS, SharedStylesHost, flattenStyles, getDOM, isNamespaced, shimContentAttribute, shimHostAttribute, splitNamespace} from './private_import_platform-browser';
const TEMPLATE_COMMENT_TEXT = 'template bindings={}'; const TEMPLATE_COMMENT_TEXT = 'template bindings={}';
@ -208,12 +208,8 @@ export class ServerRenderer implements Renderer {
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
duration: number, delay: number, easing: string, duration: number, delay: number, easing: string,
previousPlayers: AnimationPlayer[] = []): AnimationPlayer { previousPlayers: AnimationPlayer[] = []): AnimationPlayer {
try { return this._animationDriver.animate(
return this._animationDriver.animate( element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
element, startingStyles, keyframes, duration, delay, easing, previousPlayers);
} catch (e) {
return new NoOpAnimationPlayer();
}
} }
} }

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {AUTO_STYLE, AnimationTransitionEvent, Component, Injector, animate, state, style, transition, trigger} from '@angular/core'; import {AUTO_STYLE, AnimationTransitionEvent, Component, Injector, ViewChild, animate, state, style, transition, trigger} from '@angular/core';
import {DebugDomRootRenderer} from '@angular/core/src/debug/debug_renderer'; import {DebugDomRootRenderer} from '@angular/core/src/debug/debug_renderer';
import {RootRenderer} from '@angular/core/src/render/api'; import {RootRenderer} from '@angular/core/src/render/api';
import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing'; import {TestBed, fakeAsync, flushMicrotasks} from '@angular/core/testing';
@ -84,7 +84,7 @@ export function main() {
workerRenderStore = new RenderStore(); workerRenderStore = new RenderStore();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AnimationCmp, MultiAnimationCmp], declarations: [AnimationCmp, MultiAnimationCmp, ContainerAnimationCmp],
providers: [ providers: [
Serializer, {provide: RenderStore, useValue: workerRenderStore}, { Serializer, {provide: RenderStore, useValue: workerRenderStore}, {
provide: RootRenderer, provide: RootRenderer,
@ -231,10 +231,9 @@ export function main() {
return (event: AnimationTransitionEvent) => { log[phaseName] = event; }; return (event: AnimationTransitionEvent) => { log[phaseName] = event; };
} }
const f1 = TestBed.createComponent(AnimationCmp); const fixture = TestBed.createComponent(ContainerAnimationCmp);
const f2 = TestBed.createComponent(AnimationCmp); const cmp1 = fixture.componentInstance.compOne;
const cmp1 = f1.componentInstance; const cmp2 = fixture.componentInstance.compTwo;
const cmp2 = f2.componentInstance;
const cmp1Log: {[phaseName: string]: AnimationTransitionEvent} = {}; const cmp1Log: {[phaseName: string]: AnimationTransitionEvent} = {};
const cmp2Log: {[phaseName: string]: AnimationTransitionEvent} = {}; const cmp2Log: {[phaseName: string]: AnimationTransitionEvent} = {};
@ -246,8 +245,7 @@ export function main() {
cmp1.state = 'off'; cmp1.state = 'off';
cmp2.state = 'on'; cmp2.state = 'on';
f1.detectChanges(); fixture.detectChanges();
f2.detectChanges();
flushMicrotasks(); flushMicrotasks();
uiDriver.log.shift()['player'].finish(); uiDriver.log.shift()['player'].finish();
@ -316,6 +314,18 @@ export function main() {
}); });
} }
@Component({
selector: 'container-comp',
template: `
<my-comp #one></my-comp>
<my-comp #two></my-comp>
`
})
class ContainerAnimationCmp {
@ViewChild('one') public compOne: AnimationCmp;
@ViewChild('two') public compTwo: AnimationCmp;
}
@Component({ @Component({
selector: 'my-comp', selector: 'my-comp',