diff --git a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
index ba02f37428..fdb81fc978 100644
--- a/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
+++ b/packages/compiler-cli/test/compliance/r3_compiler_compliance_spec.ts
@@ -836,7 +836,7 @@ describe('compiler compliance', () => {
};
const MyAppDefinition = `
- const $e0_attr$ = [${AttributeMarker.SelectOnly}, "names"];
+ const $e0_attr$ = [${AttributeMarker.SelectOnly}, "names"];
const $e0_ff$ = function ($v0$, $v1$, $v2$, $v3$, $v4$, $v5$, $v6$, $v7$, $v8$) {
return ["start-", $v0$, $v1$, $v2$, $v3$, $v4$, "-middle-", $v5$, $v6$, $v7$, $v8$, "-end"];
}
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 3a5aeb201f..7b52c47e9b 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
@@ -180,6 +180,52 @@ describe('compiler compliance: styling', () => {
const result = compile(files, angularFiles);
expectEmit(result.source, template, 'Incorrect template');
});
+
+ it('should generate any animation triggers into the component template', () => {
+ const files = {
+ app: {
+ 'spec.ts': `
+ import {Component, NgModule} from '@angular/core';
+
+ @Component({
+ selector: "my-component",
+ template: \`
+
, LocalResolver
initialClassDeclarations.length || classInputs.length;
// add attributes for directive matching purposes
- attributes.push(...this.prepareSelectOnlyAttrs(allOtherInputs, element.outputs));
+ attributes.push(...this.prepareSyntheticAndSelectOnlyAttrs(allOtherInputs, element.outputs));
parameters.push(this.toAttrsParam(attributes));
// local refs (ex.: )
@@ -608,20 +609,27 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
// Generate element input bindings
allOtherInputs.forEach((input: t.BoundAttribute) => {
- if (input.type === BindingType.Animation) {
- console.error('warning: animation bindings not yet supported');
- return;
- }
-
const instruction = mapBindingToInstruction(input.type);
- if (instruction) {
+ if (input.type === BindingType.Animation) {
+ const value = input.value.visit(this._valueConverter);
+ // setAttribute without a value doesn't make any sense
+ if (value.name || value.value) {
+ const name = prepareSyntheticAttributeName(input.name);
+ this.updateInstruction(input.sourceSpan, R3.elementAttribute, () => {
+ return [
+ o.literal(elementIndex), o.literal(name), this.convertPropertyBinding(implicit, value)
+ ];
+ });
+ }
+ } else if (instruction) {
const params: any[] = [];
const sanitizationRef = resolveSanitizationFn(input, input.securityContext);
if (sanitizationRef) params.push(sanitizationRef);
- // TODO(chuckj): runtime: security context?
+ // TODO(chuckj): runtime: security context
const value = input.value.visit(this._valueConverter);
this.allocateBindingSlots(value);
+
this.updateInstruction(input.sourceSpan, instruction, () => {
return [
o.literal(elementIndex), o.literal(input.name),
@@ -680,7 +688,7 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
const attrsExprs: o.Expression[] = [];
template.attributes.forEach(
(a: t.TextAttribute) => { attrsExprs.push(asLiteral(a.name), asLiteral(a.value)); });
- attrsExprs.push(...this.prepareSelectOnlyAttrs(template.inputs, template.outputs));
+ attrsExprs.push(...this.prepareSyntheticAndSelectOnlyAttrs(template.inputs, template.outputs));
parameters.push(this.toAttrsParam(attrsExprs));
// local refs (ex.: )
@@ -856,14 +864,30 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver
return attributesMap;
}
- private prepareSelectOnlyAttrs(inputs: t.BoundAttribute[], outputs: t.BoundEvent[]):
+ private prepareSyntheticAndSelectOnlyAttrs(inputs: t.BoundAttribute[], outputs: t.BoundEvent[]):
o.Expression[] {
const attrExprs: o.Expression[] = [];
+ const nonSyntheticInputs: t.BoundAttribute[] = [];
- if (inputs.length || outputs.length) {
+ if (inputs.length) {
+ const EMPTY_STRING_EXPR = asLiteral('');
+ inputs.forEach(input => {
+ if (input.type === BindingType.Animation) {
+ // @attributes are for Renderer2 animation @triggers, but this feature
+ // may be supported differently in future versions of angular. However,
+ // @triggers should always just be treated as regular attributes (it's up
+ // to the renderer to detect and use them in a special way).
+ attrExprs.push(asLiteral(prepareSyntheticAttributeName(input.name)), EMPTY_STRING_EXPR);
+ } else {
+ nonSyntheticInputs.push(input);
+ }
+ });
+ }
+
+ if (nonSyntheticInputs.length || outputs.length) {
attrExprs.push(o.literal(core.AttributeMarker.SelectOnly));
- inputs.forEach((i: t.BoundAttribute) => { attrExprs.push(asLiteral(i.name)); });
- outputs.forEach((o: t.BoundEvent) => { attrExprs.push(asLiteral(o.name)); });
+ nonSyntheticInputs.forEach((i: t.BoundAttribute) => attrExprs.push(asLiteral(i.name)));
+ outputs.forEach((o: t.BoundEvent) => attrExprs.push(asLiteral(o.name)));
}
return attrExprs;
@@ -1429,3 +1453,7 @@ function isStyleSanitizable(prop: string): boolean {
}
return false;
}
+
+function prepareSyntheticAttributeName(name: string) {
+ return '@' + name;
+}
diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts
index 9fa3c376ad..b08a163fb2 100644
--- a/packages/core/test/render3/integration_spec.ts
+++ b/packages/core/test/render3/integration_spec.ts
@@ -9,12 +9,13 @@
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
import {RenderFlags} from '@angular/core/src/render3';
-import {RendererType2} from '../../src/render/api';
+import {RendererStyleFlags2, RendererType2} from '../../src/render/api';
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
import {AttributeMarker, defineComponent, defineDirective, injectElementRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
+
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, element, elementAttribute, elementClassProp, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, elementStyleProp, elementStyling, elementStylingApply, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, listener, load, loadDirective, projection, projectionDef, text, textBinding, template} from '../../src/render3/instructions';
import {InitialStylingFlags} from '../../src/render3/interfaces/definition';
-import {RElement, Renderer3, RendererFactory3, domRendererFactory3} from '../../src/render3/interfaces/renderer';
+import {RElement, Renderer3, RendererFactory3, domRendererFactory3, RText, RComment, RNode, RendererStyleFlags3, ProceduralRenderer3} from '../../src/render3/interfaces/renderer';
import {HEADER_OFFSET, CONTEXT, DIRECTIVES} from '../../src/render3/interfaces/view';
import {sanitizeUrl} from '../../src/sanitization/sanitization';
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
@@ -1387,7 +1388,7 @@ describe('render3 integration test', () => {
}
});
}
- const rendererFactory = new MockRendererFactory();
+ const rendererFactory = new ProxyRenderer3Factory();
new ComponentFixture(StyledComp, {rendererFactory});
expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']);
expect(rendererFactory.lastCapturedType !.encapsulation).toEqual(100);
@@ -1413,7 +1414,7 @@ describe('render3 integration test', () => {
template: (rf: RenderFlags, ctx: AnimComp) => {}
});
}
- const rendererFactory = new MockRendererFactory();
+ const rendererFactory = new ProxyRenderer3Factory();
new ComponentFixture(AnimComp, {rendererFactory});
const capturedAnimations = rendererFactory.lastCapturedType !.data !['animations'];
@@ -1435,11 +1436,74 @@ describe('render3 integration test', () => {
template: (rf: RenderFlags, ctx: AnimComp) => {}
});
}
- const rendererFactory = new MockRendererFactory();
+ const rendererFactory = new ProxyRenderer3Factory();
new ComponentFixture(AnimComp, {rendererFactory});
const data = rendererFactory.lastCapturedType !.data;
expect(data.animations).toEqual([]);
});
+
+ it('should allow [@trigger] bindings to be picked up by the underlying renderer', () => {
+ class AnimComp {
+ static ngComponentDef = defineComponent({
+ type: AnimComp,
+ consts: 1,
+ vars: 1,
+ selectors: [['foo']],
+ factory: () => new AnimComp(),
+ template: (rf: RenderFlags, ctx: AnimComp) => {
+ if (rf & RenderFlags.Create) {
+ element(0, 'div', [AttributeMarker.SelectOnly, '@fooAnimation']);
+ }
+ if (rf & RenderFlags.Update) {
+ elementAttribute(0, '@fooAnimation', bind(ctx.animationValue));
+ }
+ }
+ });
+
+ animationValue = '123';
+ }
+
+ const rendererFactory = new MockRendererFactory(['setAttribute']);
+ const fixture = new ComponentFixture(AnimComp, {rendererFactory});
+
+ const renderer = rendererFactory.lastRenderer !;
+ fixture.component.animationValue = '456';
+ fixture.update();
+
+ const spy = renderer.spies['setAttribute'];
+ const [elm, attr, value] = spy.calls.mostRecent().args;
+
+ expect(attr).toEqual('@fooAnimation');
+ expect(value).toEqual('456');
+ });
+
+ it('should allow creation-level [@trigger] properties to be picked up by the underlying renderer',
+ () => {
+ class AnimComp {
+ static ngComponentDef = defineComponent({
+ type: AnimComp,
+ consts: 1,
+ vars: 1,
+ selectors: [['foo']],
+ factory: () => new AnimComp(),
+ template: (rf: RenderFlags, ctx: AnimComp) => {
+ if (rf & RenderFlags.Create) {
+ element(0, 'div', ['@fooAnimation', '']);
+ }
+ }
+ });
+ }
+
+ const rendererFactory = new MockRendererFactory(['setAttribute']);
+ const fixture = new ComponentFixture(AnimComp, {rendererFactory});
+
+ const renderer = rendererFactory.lastRenderer !;
+ fixture.update();
+
+ const spy = renderer.spies['setAttribute'];
+ const [elm, attr, value] = spy.calls.mostRecent().args;
+ expect(attr).toEqual('@fooAnimation');
+ });
});
describe('element discovery', () => {
@@ -2201,7 +2265,7 @@ class LocalSanitizer implements Sanitizer {
bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); }
}
-class MockRendererFactory implements RendererFactory3 {
+class ProxyRenderer3Factory implements RendererFactory3 {
lastCapturedType: RendererType2|null = null;
createRenderer(hostElement: RElement|null, rendererType: RendererType2|null): Renderer3 {
@@ -2209,3 +2273,55 @@ class MockRendererFactory implements RendererFactory3 {
return domRendererFactory3.createRenderer(hostElement, rendererType);
}
}
+
+class MockRendererFactory implements RendererFactory3 {
+ lastRenderer: any;
+ private _spyOnMethods: string[];
+
+ constructor(spyOnMethods?: string[]) { this._spyOnMethods = spyOnMethods || []; }
+
+ createRenderer(hostElement: RElement|null, rendererType: RendererType2|null): Renderer3 {
+ const renderer = this.lastRenderer = new MockRenderer(this._spyOnMethods);
+ return renderer;
+ }
+}
+
+class MockRenderer implements ProceduralRenderer3 {
+ public spies: {[methodName: string]: any} = {};
+
+ constructor(spyOnMethods: string[]) {
+ spyOnMethods.forEach(methodName => {
+ this.spies[methodName] = spyOn(this as any, methodName).and.callThrough();
+ });
+ }
+
+ destroy(): void {}
+ createComment(value: string): RComment { return document.createComment(value); }
+ createElement(name: string, namespace?: string|null): RElement {
+ return document.createElement(name);
+ }
+ createText(value: string): RText { return document.createTextNode(value); }
+ appendChild(parent: RElement, newChild: RNode): void { parent.appendChild(newChild); }
+ insertBefore(parent: RNode, newChild: RNode, refChild: RNode|null): void {
+ parent.insertBefore(newChild, refChild, false);
+ }
+ removeChild(parent: RElement, oldChild: RNode): void { parent.removeChild(oldChild); }
+ selectRootElement(selectorOrNode: string|any): RElement {
+ return ({} as any);
+ }
+ setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void {}
+ removeAttribute(el: RElement, name: string, namespace?: string|null): void {}
+ addClass(el: RElement, name: string): void {}
+ removeClass(el: RElement, name: string): void {}
+ setStyle(
+ el: RElement, style: string, value: any,
+ flags?: RendererStyleFlags2|RendererStyleFlags3): void {}
+ removeStyle(el: RElement, style: string, flags?: RendererStyleFlags2|RendererStyleFlags3): void {}
+ setProperty(el: RElement, name: string, value: any): void {}
+ setValue(node: RText, value: string): void {}
+
+ // TODO(misko): Deprecate in favor of addEventListener/removeEventListener
+ listen(target: RNode, eventName: string, callback: (event: any) => boolean | void): () => void {
+ return () => {};
+ }
+}