fix(core): animations no longer silently exits if the element is not apart of the DOM (#13763)
This commit is contained in:
parent
889b48d85f
commit
21030e9a1c
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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',
|
||||||
|
|
Loading…
Reference in New Issue