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:
parent
393398e6f5
commit
c61e4d7841
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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]); }
|
||||
|
|
Loading…
Reference in New Issue