fix(ivy): process nested animation metadata (#32818)

In View Engine, animation metadata could occur in nested arrays which
would be flattened in the compiler. When compiling a component for Ivy
however, the compiler no longer statically evaluates a component's
animation metadata and is therefore unable to flatten it statically.
This resulted in an issue to find animations at runtime, as the metadata
was incorrectly registered with the animation engine.

Although it would be possible to statically evaluate the animation
metadata in ngtsc, doing so would prevent reusable animations exported
from libraries from being usable as ngtsc's partial evaluator is unable
to read values inside libraries. This is unlike ngc's usage of static
symbols represented in a library's `.metadata.json`, which explains how
the View Engine compiler is able to flatten the animation metadata
statically.

As an alternative solution, the metadata flattening is now done in the
runtime during the registration of the animation metadata with the
animation engine.

Fixes #32794

PR Close #32818
This commit is contained in:
JoostK 2019-09-23 20:31:09 +02:00 committed by Alex Rickabaugh
parent 393398e6f5
commit c61e4d7841
3 changed files with 61 additions and 6 deletions

View File

@ -328,6 +328,36 @@ const DEFAULT_COMPONENT_ID = '1';
]);
});
// https://github.com/angular/angular/issues/32794
it('should support nested animation triggers', () => {
const REUSABLE_ANIMATION = [trigger(
'myAnimation',
[transition(
'void => *', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])];
@Component({
selector: 'if-cmp',
template: `
<div @myAnimation></div>
`,
animations: [REUSABLE_ANIMATION],
})
class Cmp {
}
TestBed.configureTestingModule({declarations: [Cmp]});
const engine = TestBed.inject(ɵAnimationEngine);
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
engine.flush();
expect(getLog().length).toEqual(1);
expect(getLog().pop() !.keyframes).toEqual([
{offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}
]);
});
it('should allow a transition to use a function to determine what method to run', () => {
let valueToMatch = '';
let capturedElement: any;

View File

@ -12,6 +12,12 @@ import {Injectable, NgZone, Renderer2, RendererFactory2, RendererStyleFlags2, Re
const ANIMATION_PREFIX = '@';
const DISABLE_ANIMATIONS_FLAG = '@.disabled';
// Define a recursive type to allow for nested arrays of `AnimationTriggerMetadata`. Note that an
// interface declaration is used as TypeScript prior to 3.7 does not support recursive type
// references, see https://github.com/microsoft/TypeScript/pull/33050 for details.
type NestedAnimationTriggerMetadata = AnimationTriggerMetadata | RecursiveAnimationTriggerMetadata;
interface RecursiveAnimationTriggerMetadata extends Array<NestedAnimationTriggerMetadata> {}
@Injectable()
export class AnimationRendererFactory implements RendererFactory2 {
private _currentId: number = 0;
@ -55,10 +61,17 @@ export class AnimationRendererFactory implements RendererFactory2 {
this._currentId++;
this.engine.register(namespaceId, hostElement);
const animationTriggers = type.data['animation'] as AnimationTriggerMetadata[];
animationTriggers.forEach(
trigger => this.engine.registerTrigger(
componentId, namespaceId, hostElement, trigger.name, trigger));
const registerTrigger = (trigger: NestedAnimationTriggerMetadata) => {
if (Array.isArray(trigger)) {
trigger.forEach(registerTrigger);
} else {
this.engine.registerTrigger(componentId, namespaceId, hostElement, trigger.name, trigger);
}
};
const animationTriggers = type.data['animation'] as NestedAnimationTriggerMetadata[];
animationTriggers.forEach(registerTrigger);
return new AnimationRenderer(this, namespaceId, delegate, this.engine);
}

View File

@ -79,6 +79,16 @@ import {el} from '../../testing/src/browser_util';
expect(engine.captures['setProperty'].pop()).toEqual([element, 'prop', 'value']);
});
// https://github.com/angular/angular/issues/32794
it('should support nested animation triggers', () => {
makeRenderer([[trigger('myAnimation', [])]]);
const {triggers} = TestBed.inject(AnimationEngine) as MockAnimationEngine;
expect(triggers.length).toEqual(1);
expect(triggers[0].name).toEqual('myAnimation');
});
describe('listen', () => {
it('should hook into the engine\'s listen call if the property begins with `@`', () => {
const renderer = makeRenderer();
@ -320,8 +330,10 @@ class MockAnimationEngine extends InjectableAnimationEngine {
data.push(args);
}
registerTrigger(componentId: string, namespaceId: string, trigger: AnimationTriggerMetadata) {
this.triggers.push(trigger);
registerTrigger(
componentId: string, namespaceId: string, hostElement: any, name: string,
metadata: AnimationTriggerMetadata): void {
this.triggers.push(metadata);
}
onInsert(namespaceId: string, element: any): void { this._capture('onInsert', [element]); }