diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 06c5daaad3..e394e8c35c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -164,6 +164,14 @@ export class ComponentDecoratorHandler implements DecoratorHandler convertMapToStringMap(entry)) : null; + } + return { analysis: { ...metadata, @@ -176,7 +184,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler(map: Map): {[key: string]: T} { + const stringMap: {[key: string]: T} = {}; + map.forEach((value: T, key: string) => { stringMap[key] = value; }); + return stringMap; +} diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 816c796dd2..3a5aeb201f 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -100,6 +100,88 @@ describe('compiler compliance: styling', () => { }); }); + describe('@Component.animations', () => { + it('should pass in the component metadata animations into the component definition', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: "my-component", + animations: [{name: 'foo123'}, {name: 'trigger123'}], + template: "" + }) + export class MyComponent { + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + MyComponent.ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + selectors:[["my-component"]], + factory:function MyComponent_Factory(t){ + return new (t || MyComponent)(); + }, + features: [$r3$.ɵPublicFeature], + consts: 0, + vars: 0, + template: function MyComponent_Template(rf, $ctx$) { + }, + animations: [{name: "foo123"}, {name: "trigger123"}] + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + + it('should include animations even if the provided array is empty', () => { + const files = { + app: { + 'spec.ts': ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: "my-component", + animations: [], + template: "" + }) + export class MyComponent { + } + + @NgModule({declarations: [MyComponent]}) + export class MyModule {} + ` + } + }; + + const template = ` + MyComponent.ngComponentDef = $r3$.ɵdefineComponent({ + type: MyComponent, + selectors:[["my-component"]], + factory:function MyComponent_Factory(t){ + return new (t || MyComponent)(); + }, + features: [$r3$.ɵPublicFeature], + consts: 0, + vars: 0, + template: function MyComponent_Template(rf, $ctx$) { + }, + animations: [] + }); + `; + + const result = compile(files, angularFiles); + expectEmit(result.source, template, 'Incorrect template'); + }); + }); + describe('[style] and [style.prop]', () => { it('should create style instructions on the element', () => { const files = { diff --git a/packages/compiler/src/compile_metadata.ts b/packages/compiler/src/compile_metadata.ts index fe00577af0..44f8194448 100644 --- a/packages/compiler/src/compile_metadata.ts +++ b/packages/compiler/src/compile_metadata.ts @@ -189,6 +189,7 @@ export interface CompileTemplateSummary { ngContentSelectors: string[]; encapsulation: ViewEncapsulation|null; styles: string[]; + animations: any[]|null; } /** @@ -244,7 +245,8 @@ export class CompileTemplateMetadata { return { ngContentSelectors: this.ngContentSelectors, encapsulation: this.encapsulation, - styles: this.styles + styles: this.styles, + animations: this.animations }; } } diff --git a/packages/compiler/src/render3/view/api.ts b/packages/compiler/src/render3/view/api.ts index d1177d65a8..c0f554efc6 100644 --- a/packages/compiler/src/render3/view/api.ts +++ b/packages/compiler/src/render3/view/api.ts @@ -175,6 +175,11 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata { * into a shadow root. */ encapsulation: ViewEncapsulation; + + /** + * A collection of animation triggers that will be used in the component template. + */ + animations: {[key: string]: any}[]|null; } /** diff --git a/packages/compiler/src/render3/view/compiler.ts b/packages/compiler/src/render3/view/compiler.ts index 250d46497b..7da4c7c278 100644 --- a/packages/compiler/src/render3/view/compiler.ts +++ b/packages/compiler/src/render3/view/compiler.ts @@ -27,7 +27,7 @@ import {typeWithParameters} from '../util'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template'; -import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, temporaryAllocator} from './util'; +import {CONTEXT_NAME, DefinitionMap, RENDER_FLAGS, TEMPORARY_NAME, asLiteral, conditionallyCreateMapObjectLiteral, getQueryPredicate, mapToExpression, temporaryAllocator} from './util'; const EMPTY_ARRAY: any[] = []; @@ -246,6 +246,12 @@ export function compileComponentFromMetadata( definitionMap.set('styles', o.literalArr(strings)); } + // e.g. `animations: [trigger('123', [])]` + if (meta.animations) { + const animationValues = meta.animations.map(entry => mapToExpression(entry)); + definitionMap.set('animations', o.literalArr(animationValues)); + } + // On the type side, remove newlines from the selector as it will need to fit into a TypeScript // string literal, which must be on one line. const selectorForType = (meta.selector || '').replace(/\n/g, ''); @@ -301,6 +307,7 @@ export function compileComponentFromRender2( const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Component); const summary = component.toSummary(); + const animations = summary.template && summary.template.animations || null; // Compute the R3ComponentMetadata from the CompileDirectiveMetadata const meta: R3ComponentMetadata = { @@ -318,7 +325,8 @@ export function compileComponentFromRender2( wrapDirectivesInClosure: false, styles: (summary.template && summary.template.styles) || EMPTY_ARRAY, encapsulation: - (summary.template && summary.template.encapsulation) || core.ViewEncapsulation.Emulated + (summary.template && summary.template.encapsulation) || core.ViewEncapsulation.Emulated, + animations }; const res = compileComponentFromMetadata(meta, outputCtx.constantPool, bindingParser); diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 6286ddc9f6..2ac600cc28 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -262,6 +262,11 @@ export function defineComponent(componentDefinition: { * `PipeDefs`s. The function is necessary to be able to support forward declarations. */ pipes?: PipeTypesOrFactory | null; + + /** + * Registry of the animation triggers present on the component that will be used by the view. + */ + animations?: any[] | null; }): never { const type = componentDefinition.type; const pipeTypes = componentDefinition.pipes !; @@ -269,6 +274,11 @@ export function defineComponent(componentDefinition: { const declaredInputs: {[key: string]: string} = {} as any; const encapsulation = componentDefinition.encapsulation || ViewEncapsulation.Emulated; const styles: string[] = componentDefinition.styles || EMPTY_ARRAY; + const animations: any[]|null = componentDefinition.animations || null; + let data = componentDefinition.data || {}; + if (animations) { + data.animations = animations; + } const def: ComponentDefInternal = { type: type, diPublic: null, @@ -303,7 +313,7 @@ export function defineComponent(componentDefinition: { selectors: componentDefinition.selectors, viewQuery: componentDefinition.viewQuery || null, features: componentDefinition.features || null, - data: componentDefinition.data || EMPTY, + data, // TODO(misko): convert ViewEncapsulation into const enum so that it can be used directly in the // next line. Also `None` should be 0 not 2. encapsulation, diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 712a8aadcb..13ea793982 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -76,7 +76,8 @@ export function compileComponent(type: Type, metadata: Component): void { viewQueries: [], wrapDirectivesInClosure: false, styles: metadata.styles || [], - encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated + encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated, + animations: metadata.animations || null }, constantPool, makeBindingParser()); const preStatements = [...constantPool.statements, ...res.statements]; diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 3ee9e1c101..b099640117 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -1394,6 +1394,54 @@ describe('render3 integration test', () => { }); }); + describe('component animations', () => { + it('should pass in the component styles directly into the underlying renderer', () => { + const animA = {name: 'a'}; + const animB = {name: 'b'}; + + class AnimComp { + static ngComponentDef = defineComponent({ + type: AnimComp, + consts: 0, + vars: 0, + animations: [ + animA, + animB, + ], + selectors: [['foo']], + factory: () => new AnimComp(), + template: (rf: RenderFlags, ctx: AnimComp) => {} + }); + } + const rendererFactory = new MockRendererFactory(); + new ComponentFixture(AnimComp, {rendererFactory}); + + const capturedAnimations = rendererFactory.lastCapturedType !.data !['animations']; + expect(Array.isArray(capturedAnimations)).toBeTruthy(); + expect(capturedAnimations.length).toEqual(2); + expect(capturedAnimations).toContain(animA); + expect(capturedAnimations).toContain(animB); + }); + + it('should include animations in the renderType data array even if the array is empty', () => { + class AnimComp { + static ngComponentDef = defineComponent({ + type: AnimComp, + consts: 0, + vars: 0, + animations: [], + selectors: [['foo']], + factory: () => new AnimComp(), + template: (rf: RenderFlags, ctx: AnimComp) => {} + }); + } + const rendererFactory = new MockRendererFactory(); + new ComponentFixture(AnimComp, {rendererFactory}); + const data = rendererFactory.lastCapturedType !.data; + expect(data.animations).toEqual([]); + }); + }); + describe('element discovery', () => { it('should only monkey-patch immediate child nodes in a component', () => { class StructuredComp {