feat(ivy): patch animations into metadata (#25828)

PR Close #25828
This commit is contained in:
Matias Niemelä 2018-09-05 15:23:59 -07:00 committed by Igor Minar
parent d6cd041cbd
commit d2dfd48be0
8 changed files with 176 additions and 6 deletions

View File

@ -164,6 +164,14 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
component.get('encapsulation') !, this.reflector, this.checker) as string); component.get('encapsulation') !, this.reflector, this.checker) as string);
} }
let animations: any[]|null = null;
if (component.has('animations')) {
animations =
(staticallyResolve(component.get('animations') !, this.reflector, this.checker) as any |
null[]);
animations = animations ? animations.map(entry => convertMapToStringMap(entry)) : null;
}
return { return {
analysis: { analysis: {
...metadata, ...metadata,
@ -176,7 +184,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// analyzed and the full compilation scope for the component can be realized. // analyzed and the full compilation scope for the component can be realized.
pipes: EMPTY_MAP, pipes: EMPTY_MAP,
directives: EMPTY_MAP, directives: EMPTY_MAP,
wrapDirectivesInClosure: false, wrapDirectivesInClosure: false, animations,
} }
}; };
} }
@ -224,3 +232,9 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
return meta; return meta;
} }
} }
function convertMapToStringMap<T>(map: Map<string, T>): {[key: string]: T} {
const stringMap: {[key: string]: T} = {};
map.forEach((value: T, key: string) => { stringMap[key] = value; });
return stringMap;
}

View File

@ -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]', () => { describe('[style] and [style.prop]', () => {
it('should create style instructions on the element', () => { it('should create style instructions on the element', () => {
const files = { const files = {

View File

@ -189,6 +189,7 @@ export interface CompileTemplateSummary {
ngContentSelectors: string[]; ngContentSelectors: string[];
encapsulation: ViewEncapsulation|null; encapsulation: ViewEncapsulation|null;
styles: string[]; styles: string[];
animations: any[]|null;
} }
/** /**
@ -244,7 +245,8 @@ export class CompileTemplateMetadata {
return { return {
ngContentSelectors: this.ngContentSelectors, ngContentSelectors: this.ngContentSelectors,
encapsulation: this.encapsulation, encapsulation: this.encapsulation,
styles: this.styles styles: this.styles,
animations: this.animations
}; };
} }
} }

View File

@ -175,6 +175,11 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
* into a shadow root. * into a shadow root.
*/ */
encapsulation: ViewEncapsulation; encapsulation: ViewEncapsulation;
/**
* A collection of animation triggers that will be used in the component template.
*/
animations: {[key: string]: any}[]|null;
} }
/** /**

View File

@ -27,7 +27,7 @@ import {typeWithParameters} from '../util';
import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api'; import {R3ComponentDef, R3ComponentMetadata, R3DirectiveDef, R3DirectiveMetadata, R3QueryMetadata} from './api';
import {BindingScope, TemplateDefinitionBuilder, ValueConverter, renderFlagCheckIfStmt} from './template'; 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[] = []; const EMPTY_ARRAY: any[] = [];
@ -246,6 +246,12 @@ export function compileComponentFromMetadata(
definitionMap.set('styles', o.literalArr(strings)); 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 // 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. // string literal, which must be on one line.
const selectorForType = (meta.selector || '').replace(/\n/g, ''); const selectorForType = (meta.selector || '').replace(/\n/g, '');
@ -301,6 +307,7 @@ export function compileComponentFromRender2(
const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Component); const definitionField = outputCtx.constantPool.propertyNameOf(DefinitionKind.Component);
const summary = component.toSummary(); const summary = component.toSummary();
const animations = summary.template && summary.template.animations || null;
// Compute the R3ComponentMetadata from the CompileDirectiveMetadata // Compute the R3ComponentMetadata from the CompileDirectiveMetadata
const meta: R3ComponentMetadata = { const meta: R3ComponentMetadata = {
@ -318,7 +325,8 @@ export function compileComponentFromRender2(
wrapDirectivesInClosure: false, wrapDirectivesInClosure: false,
styles: (summary.template && summary.template.styles) || EMPTY_ARRAY, styles: (summary.template && summary.template.styles) || EMPTY_ARRAY,
encapsulation: 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); const res = compileComponentFromMetadata(meta, outputCtx.constantPool, bindingParser);

View File

@ -262,6 +262,11 @@ export function defineComponent<T>(componentDefinition: {
* `PipeDefs`s. The function is necessary to be able to support forward declarations. * `PipeDefs`s. The function is necessary to be able to support forward declarations.
*/ */
pipes?: PipeTypesOrFactory | null; pipes?: PipeTypesOrFactory | null;
/**
* Registry of the animation triggers present on the component that will be used by the view.
*/
animations?: any[] | null;
}): never { }): never {
const type = componentDefinition.type; const type = componentDefinition.type;
const pipeTypes = componentDefinition.pipes !; const pipeTypes = componentDefinition.pipes !;
@ -269,6 +274,11 @@ export function defineComponent<T>(componentDefinition: {
const declaredInputs: {[key: string]: string} = {} as any; const declaredInputs: {[key: string]: string} = {} as any;
const encapsulation = componentDefinition.encapsulation || ViewEncapsulation.Emulated; const encapsulation = componentDefinition.encapsulation || ViewEncapsulation.Emulated;
const styles: string[] = componentDefinition.styles || EMPTY_ARRAY; 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<any> = { const def: ComponentDefInternal<any> = {
type: type, type: type,
diPublic: null, diPublic: null,
@ -303,7 +313,7 @@ export function defineComponent<T>(componentDefinition: {
selectors: componentDefinition.selectors, selectors: componentDefinition.selectors,
viewQuery: componentDefinition.viewQuery || null, viewQuery: componentDefinition.viewQuery || null,
features: componentDefinition.features || 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 // 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. // next line. Also `None` should be 0 not 2.
encapsulation, encapsulation,

View File

@ -76,7 +76,8 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
viewQueries: [], viewQueries: [],
wrapDirectivesInClosure: false, wrapDirectivesInClosure: false,
styles: metadata.styles || [], styles: metadata.styles || [],
encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated encapsulation: metadata.encapsulation || ViewEncapsulation.Emulated,
animations: metadata.animations || null
}, },
constantPool, makeBindingParser()); constantPool, makeBindingParser());
const preStatements = [...constantPool.statements, ...res.statements]; const preStatements = [...constantPool.statements, ...res.statements];

View File

@ -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', () => { describe('element discovery', () => {
it('should only monkey-patch immediate child nodes in a component', () => { it('should only monkey-patch immediate child nodes in a component', () => {
class StructuredComp { class StructuredComp {