feat(ivy): support animation @triggers in templates (#25849)
PR Close #25849
This commit is contained in:
parent
ed266daf2c
commit
e3633888ed
@ -836,7 +836,7 @@ describe('compiler compliance', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MyAppDefinition = `
|
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$) {
|
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"];
|
return ["start-", $v0$, $v1$, $v2$, $v3$, $v4$, "-middle-", $v5$, $v6$, $v7$, $v8$, "-end"];
|
||||||
}
|
}
|
||||||
|
@ -180,6 +180,52 @@ describe('compiler compliance: styling', () => {
|
|||||||
const result = compile(files, angularFiles);
|
const result = compile(files, angularFiles);
|
||||||
expectEmit(result.source, template, 'Incorrect template');
|
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: \`
|
||||||
|
<div [@foo]='exp'></div>
|
||||||
|
<div @bar></div>
|
||||||
|
<div [@baz]></div>\`,
|
||||||
|
})
|
||||||
|
export class MyComponent {
|
||||||
|
}
|
||||||
|
|
||||||
|
@NgModule({declarations: [MyComponent]})
|
||||||
|
export class MyModule {}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
const $e0_attrs$ = ["@foo", ""];
|
||||||
|
const $e1_attrs$ = ["@bar", ""];
|
||||||
|
const $e2_attrs$ = ["@baz", ""];
|
||||||
|
…
|
||||||
|
MyComponent.ngComponentDef = $r3$.ɵdefineComponent({
|
||||||
|
…
|
||||||
|
template: function MyComponent_Template(rf, $ctx$) {
|
||||||
|
if (rf & 1) {
|
||||||
|
$r3$.ɵelement(0, "div", $e0_attrs$);
|
||||||
|
$r3$.ɵelement(1, "div", $e1_attrs$);
|
||||||
|
$r3$.ɵelement(2, "div", $e2_attrs$);
|
||||||
|
}
|
||||||
|
if (rf & 2) {
|
||||||
|
$r3$.ɵelementAttribute(0, "@foo", $r3$.ɵbind(ctx.exp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = compile(files, angularFiles);
|
||||||
|
expectEmit(result.source, template, 'Incorrect template');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('[style] and [style.prop]', () => {
|
describe('[style] and [style.prop]', () => {
|
||||||
|
@ -36,10 +36,11 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case BindingType.Property:
|
case BindingType.Property:
|
||||||
return R3.elementProperty;
|
return R3.elementProperty;
|
||||||
case BindingType.Attribute:
|
|
||||||
return R3.elementAttribute;
|
|
||||||
case BindingType.Class:
|
case BindingType.Class:
|
||||||
return R3.elementClassProp;
|
return R3.elementClassProp;
|
||||||
|
case BindingType.Attribute:
|
||||||
|
case BindingType.Animation:
|
||||||
|
return R3.elementAttribute;
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -459,7 +460,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
initialClassDeclarations.length || classInputs.length;
|
initialClassDeclarations.length || classInputs.length;
|
||||||
|
|
||||||
// add attributes for directive matching purposes
|
// 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));
|
parameters.push(this.toAttrsParam(attributes));
|
||||||
|
|
||||||
// local refs (ex.: <div #foo #bar="baz">)
|
// local refs (ex.: <div #foo #bar="baz">)
|
||||||
@ -608,20 +609,27 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
|
|
||||||
// Generate element input bindings
|
// Generate element input bindings
|
||||||
allOtherInputs.forEach((input: t.BoundAttribute) => {
|
allOtherInputs.forEach((input: t.BoundAttribute) => {
|
||||||
if (input.type === BindingType.Animation) {
|
|
||||||
console.error('warning: animation bindings not yet supported');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instruction = mapBindingToInstruction(input.type);
|
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 params: any[] = [];
|
||||||
const sanitizationRef = resolveSanitizationFn(input, input.securityContext);
|
const sanitizationRef = resolveSanitizationFn(input, input.securityContext);
|
||||||
if (sanitizationRef) params.push(sanitizationRef);
|
if (sanitizationRef) params.push(sanitizationRef);
|
||||||
|
|
||||||
// TODO(chuckj): runtime: security context?
|
// TODO(chuckj): runtime: security context
|
||||||
const value = input.value.visit(this._valueConverter);
|
const value = input.value.visit(this._valueConverter);
|
||||||
this.allocateBindingSlots(value);
|
this.allocateBindingSlots(value);
|
||||||
|
|
||||||
this.updateInstruction(input.sourceSpan, instruction, () => {
|
this.updateInstruction(input.sourceSpan, instruction, () => {
|
||||||
return [
|
return [
|
||||||
o.literal(elementIndex), o.literal(input.name),
|
o.literal(elementIndex), o.literal(input.name),
|
||||||
@ -680,7 +688,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
const attrsExprs: o.Expression[] = [];
|
const attrsExprs: o.Expression[] = [];
|
||||||
template.attributes.forEach(
|
template.attributes.forEach(
|
||||||
(a: t.TextAttribute) => { attrsExprs.push(asLiteral(a.name), asLiteral(a.value)); });
|
(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));
|
parameters.push(this.toAttrsParam(attrsExprs));
|
||||||
|
|
||||||
// local refs (ex.: <ng-template #foo>)
|
// local refs (ex.: <ng-template #foo>)
|
||||||
@ -856,14 +864,30 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
|
|||||||
return attributesMap;
|
return attributesMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private prepareSelectOnlyAttrs(inputs: t.BoundAttribute[], outputs: t.BoundEvent[]):
|
private prepareSyntheticAndSelectOnlyAttrs(inputs: t.BoundAttribute[], outputs: t.BoundEvent[]):
|
||||||
o.Expression[] {
|
o.Expression[] {
|
||||||
const attrExprs: 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));
|
attrExprs.push(o.literal(core.AttributeMarker.SelectOnly));
|
||||||
inputs.forEach((i: t.BoundAttribute) => { attrExprs.push(asLiteral(i.name)); });
|
nonSyntheticInputs.forEach((i: t.BoundAttribute) => attrExprs.push(asLiteral(i.name)));
|
||||||
outputs.forEach((o: t.BoundEvent) => { attrExprs.push(asLiteral(o.name)); });
|
outputs.forEach((o: t.BoundEvent) => attrExprs.push(asLiteral(o.name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
return attrExprs;
|
return attrExprs;
|
||||||
@ -1429,3 +1453,7 @@ function isStyleSanitizable(prop: string): boolean {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareSyntheticAttributeName(name: string) {
|
||||||
|
return '@' + name;
|
||||||
|
}
|
||||||
|
@ -9,12 +9,13 @@
|
|||||||
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
|
import {ElementRef, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||||
import {RenderFlags} from '@angular/core/src/render3';
|
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 {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
|
||||||
import {AttributeMarker, defineComponent, defineDirective, injectElementRef, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
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 {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 {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 {HEADER_OFFSET, CONTEXT, DIRECTIVES} from '../../src/render3/interfaces/view';
|
||||||
import {sanitizeUrl} from '../../src/sanitization/sanitization';
|
import {sanitizeUrl} from '../../src/sanitization/sanitization';
|
||||||
import {Sanitizer, SecurityContext} from '../../src/sanitization/security';
|
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});
|
new ComponentFixture(StyledComp, {rendererFactory});
|
||||||
expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']);
|
expect(rendererFactory.lastCapturedType !.styles).toEqual(['div { color: red; }']);
|
||||||
expect(rendererFactory.lastCapturedType !.encapsulation).toEqual(100);
|
expect(rendererFactory.lastCapturedType !.encapsulation).toEqual(100);
|
||||||
@ -1413,7 +1414,7 @@ describe('render3 integration test', () => {
|
|||||||
template: (rf: RenderFlags, ctx: AnimComp) => {}
|
template: (rf: RenderFlags, ctx: AnimComp) => {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const rendererFactory = new MockRendererFactory();
|
const rendererFactory = new ProxyRenderer3Factory();
|
||||||
new ComponentFixture(AnimComp, {rendererFactory});
|
new ComponentFixture(AnimComp, {rendererFactory});
|
||||||
|
|
||||||
const capturedAnimations = rendererFactory.lastCapturedType !.data !['animations'];
|
const capturedAnimations = rendererFactory.lastCapturedType !.data !['animations'];
|
||||||
@ -1435,11 +1436,74 @@ describe('render3 integration test', () => {
|
|||||||
template: (rf: RenderFlags, ctx: AnimComp) => {}
|
template: (rf: RenderFlags, ctx: AnimComp) => {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const rendererFactory = new MockRendererFactory();
|
const rendererFactory = new ProxyRenderer3Factory();
|
||||||
new ComponentFixture(AnimComp, {rendererFactory});
|
new ComponentFixture(AnimComp, {rendererFactory});
|
||||||
const data = rendererFactory.lastCapturedType !.data;
|
const data = rendererFactory.lastCapturedType !.data;
|
||||||
expect(data.animations).toEqual([]);
|
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', () => {
|
describe('element discovery', () => {
|
||||||
@ -2201,7 +2265,7 @@ class LocalSanitizer implements Sanitizer {
|
|||||||
bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); }
|
bypassSecurityTrustUrl(value: string) { return new LocalSanitizedValue(value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockRendererFactory implements RendererFactory3 {
|
class ProxyRenderer3Factory implements RendererFactory3 {
|
||||||
lastCapturedType: RendererType2|null = null;
|
lastCapturedType: RendererType2|null = null;
|
||||||
|
|
||||||
createRenderer(hostElement: RElement|null, rendererType: RendererType2|null): Renderer3 {
|
createRenderer(hostElement: RElement|null, rendererType: RendererType2|null): Renderer3 {
|
||||||
@ -2209,3 +2273,55 @@ class MockRendererFactory implements RendererFactory3 {
|
|||||||
return domRendererFactory3.createRenderer(hostElement, rendererType);
|
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 () => {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user