From 5e90878a9e608bc662ac825a205c4deed5f4737e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Fri, 8 Jul 2016 17:11:12 -0700 Subject: [PATCH] feat(animations): allow animation integration support into host params Closes #9044 Closes #9933 --- .../src/animation/animation_compiler.ts | 5 +- .../@angular/compiler/src/compile_metadata.ts | 8 ++- .../@angular/compiler/src/template_parser.ts | 21 ++++--- .../src/view_compiler/compile_view.ts | 4 +- .../src/view_compiler/property_binder.ts | 22 ++++--- .../src/view_compiler/view_builder.ts | 4 +- .../@angular/core/src/linker/view_utils.ts | 6 +- modules/@angular/core/src/render/api.ts | 5 +- .../animation/animation_integration_spec.ts | 60 +++++++++++++++---- .../src/web_workers/shared/serializer.ts | 2 +- .../playground/src/animate/app/animate-app.ts | 27 +++++---- modules/playground/src/animate/index.html | 11 ++++ tools/public_api_guard/core/index.d.ts | 7 ++- 13 files changed, 126 insertions(+), 56 deletions(-) diff --git a/modules/@angular/compiler/src/animation/animation_compiler.ts b/modules/@angular/compiler/src/animation/animation_compiler.ts index b657b82835b499..4025088489a682 100644 --- a/modules/@angular/compiler/src/animation/animation_compiler.ts +++ b/modules/@angular/compiler/src/animation/animation_compiler.ts @@ -31,7 +31,6 @@ export class AnimationCompiler { compileComponent(component: CompileDirectiveMetadata, template: TemplateAst[]): CompiledAnimation[] { var compiledAnimations: CompiledAnimation[] = []; - var index = 0; var groupedErrors: string[] = []; var triggerLookup: {[key: string]: CompiledAnimation} = {}; var componentName = component.type.name; @@ -52,9 +51,7 @@ export class AnimationCompiler { groupedErrors.push( `The animation trigger "${triggerName}" has already been registered on "${componentName}"`); } else { - var factoryName = `${component.type.name}_${entry.name}_${index}`; - index++; - + var factoryName = `${component.type.name}_${entry.name}`; var visitor = new _AnimationBuilder(triggerName, factoryName); var compileResult = visitor.build(result.ast) compiledAnimations.push(compileResult); diff --git a/modules/@angular/compiler/src/compile_metadata.ts b/modules/@angular/compiler/src/compile_metadata.ts index 2c124390843407..45a8ee829ff0eb 100644 --- a/modules/@angular/compiler/src/compile_metadata.ts +++ b/modules/@angular/compiler/src/compile_metadata.ts @@ -18,9 +18,11 @@ import {getUrlScheme} from './url_resolver'; import {sanitizeIdentifier, splitAtColon} from './util'; - +// group 0: "[prop] or (event) or @trigger" +// group 1: "prop" from "[prop]" // group 2: "event" from "(event)" -var HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))$/g; +// group 3: "@trigger" from "@trigger" +var HOST_REG_EXP = /^(?:(?:\[([^\]]+)\])|(?:\(([^\)]+)\)))|(\@[-\w]+)$/g; export abstract class CompileMetadataWithIdentifier { abstract toJson(): {[key: string]: any}; @@ -741,6 +743,8 @@ export class CompileDirectiveMetadata implements CompileMetadataWithType { hostProperties[matches[1]] = value; } else if (isPresent(matches[2])) { hostListeners[matches[2]] = value; + } else if (isPresent(matches[3])) { + hostProperties[matches[3]] = value; } }); } diff --git a/modules/@angular/compiler/src/template_parser.ts b/modules/@angular/compiler/src/template_parser.ts index fcc4c16a241a56..e506e0aafb141c 100644 --- a/modules/@angular/compiler/src/template_parser.ts +++ b/modules/@angular/compiler/src/template_parser.ts @@ -774,13 +774,20 @@ class TemplateParseVisitor implements HtmlAstVisitor { const parts = name.split(PROPERTY_PARTS_SEPARATOR); let securityContext: SecurityContext; if (parts.length === 1) { - boundPropertyName = this._schemaRegistry.getMappedPropName(parts[0]); - securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName); - bindingType = PropertyBindingType.Property; - if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName)) { - this._reportError( - `Can't bind to '${boundPropertyName}' since it isn't a known native property`, - sourceSpan); + var partValue = parts[0]; + if (partValue[0] == '@') { + boundPropertyName = partValue.substr(1); + bindingType = PropertyBindingType.Animation; + securityContext = SecurityContext.NONE; + } else { + boundPropertyName = this._schemaRegistry.getMappedPropName(partValue); + securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName); + bindingType = PropertyBindingType.Property; + if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName)) { + this._reportError( + `Can't bind to '${boundPropertyName}' since it isn't a known native property`, + sourceSpan); + } } } else { if (parts[0] == ATTRIBUTE_PREFIX) { diff --git a/modules/@angular/compiler/src/view_compiler/compile_view.ts b/modules/@angular/compiler/src/view_compiler/compile_view.ts index d5b68ec857224c..1220360fa37304 100644 --- a/modules/@angular/compiler/src/view_compiler/compile_view.ts +++ b/modules/@angular/compiler/src/view_compiler/compile_view.ts @@ -65,16 +65,14 @@ export class CompileView implements NameResolver { public literalArrayCount = 0; public literalMapCount = 0; public pipeCount = 0; - public animations = new Map(); public componentContext: o.Expression; constructor( public component: CompileDirectiveMetadata, public genConfig: CompilerConfig, public pipeMetas: CompilePipeMetadata[], public styles: o.Expression, - animations: CompiledAnimation[], public viewIndex: number, + public animations: CompiledAnimation[], public viewIndex: number, public declarationElement: CompileElement, public templateVariableBindings: string[][]) { - animations.forEach(entry => this.animations.set(entry.name, entry)); this.createMethod = new CompileMethod(this); this.injectorGetMethod = new CompileMethod(this); this.updateContentQueriesMethod = new CompileMethod(this); diff --git a/modules/@angular/compiler/src/view_compiler/property_binder.ts b/modules/@angular/compiler/src/view_compiler/property_binder.ts index 2d59a2e2d77bee..8fdc7f3f39e922 100644 --- a/modules/@angular/compiler/src/view_compiler/property_binder.ts +++ b/modules/@angular/compiler/src/view_compiler/property_binder.ts @@ -23,7 +23,6 @@ import {CompileView} from './compile_view'; import {DetectChangesVars, ViewProperties} from './constants'; import {convertCdExpressionToIr} from './expression_converter'; - function createBindFieldExpr(exprIndex: number): o.ReadPropExpr { return o.THIS_EXPR.prop(`_expr_${exprIndex}`); } @@ -85,7 +84,8 @@ export function bindRenderText( } function bindAndWriteToRenderer( - boundProps: BoundElementPropertyAst[], context: o.Expression, compileElement: CompileElement) { + boundProps: BoundElementPropertyAst[], context: o.Expression, compileElement: CompileElement, + isHostProp: boolean) { var view = compileElement.view; var renderNode = compileElement.renderNode; boundProps.forEach((boundProp) => { @@ -129,6 +129,7 @@ function bindAndWriteToRenderer( if (isPresent(boundProp.unit)) { strValue = strValue.plus(o.literal(boundProp.unit)); } + renderValue = renderValue.isBlank().conditional(o.NULL_EXPR, strValue); updateStmts.push( o.THIS_EXPR.prop('renderer') @@ -137,7 +138,13 @@ function bindAndWriteToRenderer( break; case PropertyBindingType.Animation: var animationName = boundProp.name; - var animation = view.componentView.animations.get(animationName); + var targetViewExpr: o.Expression = o.THIS_EXPR; + if (isHostProp) { + targetViewExpr = compileElement.appElement.prop('componentView'); + } + + var animationFnExpr = + targetViewExpr.prop('componentType').prop('animations').key(o.literal(animationName)); // it's important to normalize the void value as `void` explicitly // so that the styles data can be obtained from the stringmap @@ -158,11 +165,10 @@ function bindAndWriteToRenderer( [newRenderVar.set(emptyStateValue).toStmt()])); updateStmts.push( - animation.fnVariable.callFn([o.THIS_EXPR, renderNode, oldRenderVar, newRenderVar]) - .toStmt()); + animationFnExpr.callFn([o.THIS_EXPR, renderNode, oldRenderVar, newRenderVar]).toStmt()); view.detachMethod.addStmt( - animation.fnVariable.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue]) + animationFnExpr.callFn([o.THIS_EXPR, renderNode, oldRenderValue, emptyStateValue]) .toStmt()); if (!_animationViewCheckedFlagMap.get(view)) { @@ -212,13 +218,13 @@ function sanitizedValue( export function bindRenderInputs( boundProps: BoundElementPropertyAst[], compileElement: CompileElement): void { - bindAndWriteToRenderer(boundProps, compileElement.view.componentContext, compileElement); + bindAndWriteToRenderer(boundProps, compileElement.view.componentContext, compileElement, false); } export function bindDirectiveHostProps( directiveAst: DirectiveAst, directiveInstance: o.Expression, compileElement: CompileElement): void { - bindAndWriteToRenderer(directiveAst.hostProperties, directiveInstance, compileElement); + bindAndWriteToRenderer(directiveAst.hostProperties, directiveInstance, compileElement, true); } export function bindDirectiveInputs( diff --git a/modules/@angular/compiler/src/view_compiler/view_builder.ts b/modules/@angular/compiler/src/view_compiler/view_builder.ts index 0990c225a79de2..183fdd5f888b81 100644 --- a/modules/@angular/compiler/src/view_compiler/view_builder.ts +++ b/modules/@angular/compiler/src/view_compiler/view_builder.ts @@ -500,6 +500,7 @@ function createViewFactory( templateUrlInfo = view.component.template.templateUrl; } if (view.viewIndex === 0) { + var animationsExpr = o.literalMap(view.animations.map(entry => [entry.name, entry.fnVariable])); initRenderCompTypeStmts = [new o.IfStmt(renderCompTypeVar.identical(o.NULL_EXPR), [ renderCompTypeVar .set(ViewConstructorVars.viewUtils.callMethod( @@ -507,7 +508,8 @@ function createViewFactory( [ o.literal(templateUrlInfo), o.literal(view.component.template.ngContentSelectors.length), - ViewEncapsulationEnum.fromValue(view.component.template.encapsulation), view.styles + ViewEncapsulationEnum.fromValue(view.component.template.encapsulation), view.styles, + animationsExpr ])) .toStmt() ])]; diff --git a/modules/@angular/core/src/linker/view_utils.ts b/modules/@angular/core/src/linker/view_utils.ts index 299108f51a4477..c198695ac4d231 100644 --- a/modules/@angular/core/src/linker/view_utils.ts +++ b/modules/@angular/core/src/linker/view_utils.ts @@ -34,11 +34,13 @@ export class ViewUtils { /** * Used by the generated code */ + // TODO (matsko): add typing for the animation function createRenderComponentType( templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation, - styles: Array): RenderComponentType { + styles: Array, animations: {[key: string]: Function}): RenderComponentType { return new RenderComponentType( - `${this._appId}-${this._nextCompTypeId++}`, templateUrl, slotCount, encapsulation, styles); + `${this._appId}-${this._nextCompTypeId++}`, templateUrl, slotCount, encapsulation, styles, + animations); } /** @internal */ diff --git a/modules/@angular/core/src/render/api.ts b/modules/@angular/core/src/render/api.ts index b26cd8e0107a74..8c581055b9a690 100644 --- a/modules/@angular/core/src/render/api.ts +++ b/modules/@angular/core/src/render/api.ts @@ -13,14 +13,15 @@ import {Injector} from '../di/injector'; import {unimplemented} from '../facade/exceptions'; import {ViewEncapsulation} from '../metadata/view'; - /** * @experimental */ +// TODO (matsko): add typing for the animation function export class RenderComponentType { constructor( public id: string, public templateUrl: string, public slotCount: number, - public encapsulation: ViewEncapsulation, public styles: Array) {} + public encapsulation: ViewEncapsulation, public styles: Array, + public animations: {[key: string]: Function}) {} } export abstract class RenderDebugInfo { diff --git a/modules/@angular/core/test/animation/animation_integration_spec.ts b/modules/@angular/core/test/animation/animation_integration_spec.ts index eafb3ada3a0d73..3ab4ebcde16ba1 100644 --- a/modules/@angular/core/test/animation/animation_integration_spec.ts +++ b/modules/@angular/core/test/animation/animation_integration_spec.ts @@ -894,6 +894,27 @@ function declareTests({useJit}: {useJit: boolean}) { }); }))); + it('should be permitted to be registered on the host element', + inject( + [TestComponentBuilder, AnimationDriver], + fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => { + tcb = tcb.overrideAnimations(DummyLoadingCmp, [trigger('loading', [ + state('final', style({'background': 'grey'})), + transition('* => final', [animate(1000)]) + ])]); + tcb.createAsync(DummyLoadingCmp).then(fixture => { + var cmp = fixture.debugElement.componentInstance; + cmp.exp = 'final'; + fixture.detectChanges(); + flushMicrotasks(); + + var animation = driver.log.pop(); + var keyframes = animation['keyframeLookup']; + expect(keyframes[1]).toEqual([1, {'background': 'grey'}]); + }); + tick(); + }))); + it('should retain the destination animation state styles once the animation is complete', inject( [TestComponentBuilder, AnimationDriver], @@ -1183,18 +1204,6 @@ function declareTests({useJit}: {useJit: boolean}) { }); } -@Component({ - selector: 'if-cmp', - directives: [NgIf], - template: ` -
- ` -}) -class DummyIfCmp { - exp = false; - exp2 = false; -} - class InnerContentTrackingAnimationDriver extends MockAnimationDriver { animate( element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[], @@ -1208,12 +1217,39 @@ class InnerContentTrackingAnimationDriver extends MockAnimationDriver { class InnerContentTrackingAnimationPlayer extends MockAnimationPlayer { constructor(public element: any) { super(); } + public computedHeight: number; public capturedInnerText: string; public playAttempts = 0; + init() { this.computedHeight = getDOM().getComputedStyle(this.element)['height']; } + play() { this.playAttempts++; this.capturedInnerText = this.element.querySelector('.inner').innerText; } } + +@Component({ + selector: 'if-cmp', + directives: [NgIf], + template: ` +
+ ` +}) +class DummyIfCmp { + exp = false; + exp2 = false; +} + +@Component({ + selector: 'if-cmp', + host: {'@loading': 'exp'}, + directives: [NgIf], + template: ` +
loading...
+ ` +}) +class DummyLoadingCmp { + exp = false; +} diff --git a/modules/@angular/platform-browser/src/web_workers/shared/serializer.ts b/modules/@angular/platform-browser/src/web_workers/shared/serializer.ts index a1a76e0c35d868..fcb44e30d8e66f 100644 --- a/modules/@angular/platform-browser/src/web_workers/shared/serializer.ts +++ b/modules/@angular/platform-browser/src/web_workers/shared/serializer.ts @@ -111,7 +111,7 @@ export class Serializer { return new RenderComponentType( map['id'], map['templateUrl'], map['slotCount'], this.deserialize(map['encapsulation'], ViewEncapsulation), - this.deserialize(map['styles'], PRIMITIVE)); + this.deserialize(map['styles'], PRIMITIVE), {}); } } diff --git a/modules/playground/src/animate/app/animate-app.ts b/modules/playground/src/animate/app/animate-app.ts index 36391cf0defd1d..636f2abdda5e76 100644 --- a/modules/playground/src/animate/app/animate-app.ts +++ b/modules/playground/src/animate/app/animate-app.ts @@ -19,22 +19,23 @@ import { } from '@angular/core'; @Component({ + host: { + '[@backgroundAnimation]': "bgStatus" + }, selector: 'animate-app', styleUrls: ['css/animate-app.css'], template: ` -
- - - | - - - -
-
- {{ item }} -
- something inside -
+ + + | + + + +
+
+ {{ item }} +
+ something inside
`, diff --git a/modules/playground/src/animate/index.html b/modules/playground/src/animate/index.html index 7e0d0b1467c0ef..acaaacef316115 100644 --- a/modules/playground/src/animate/index.html +++ b/modules/playground/src/animate/index.html @@ -2,6 +2,17 @@ Animation Example + Loading... diff --git a/tools/public_api_guard/core/index.d.ts b/tools/public_api_guard/core/index.d.ts index 156dba1692699a..f268271c86b9a5 100644 --- a/tools/public_api_guard/core/index.d.ts +++ b/tools/public_api_guard/core/index.d.ts @@ -1200,12 +1200,17 @@ export declare class ReflectiveKey { /** @experimental */ export declare class RenderComponentType { + animations: { + [key: string]: Function; + }; encapsulation: ViewEncapsulation; id: string; slotCount: number; styles: Array; templateUrl: string; - constructor(id: string, templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation, styles: Array); + constructor(id: string, templateUrl: string, slotCount: number, encapsulation: ViewEncapsulation, styles: Array, animations: { + [key: string]: Function; + }); } /** @experimental */