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 $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"]; | ||||
|           } | ||||
|  | ||||
| @ -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: \` | ||||
|                     <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]', () => { | ||||
|  | ||||
| @ -36,10 +36,11 @@ function mapBindingToInstruction(type: BindingType): o.ExternalReference|undefin | ||||
|   switch (type) { | ||||
|     case BindingType.Property: | ||||
|       return R3.elementProperty; | ||||
|     case BindingType.Attribute: | ||||
|       return R3.elementAttribute; | ||||
|     case BindingType.Class: | ||||
|       return R3.elementClassProp; | ||||
|     case BindingType.Attribute: | ||||
|     case BindingType.Animation: | ||||
|       return R3.elementAttribute; | ||||
|     default: | ||||
|       return undefined; | ||||
|   } | ||||
| @ -459,7 +460,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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.: <div #foo #bar="baz">)
 | ||||
| @ -608,20 +609,27 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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<void>, 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.: <ng-template #foo>)
 | ||||
| @ -856,14 +864,30 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, 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; | ||||
| } | ||||
|  | ||||
| @ -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 () => {}; | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user