Skip to content

Commit

Permalink
feat(animations): allow animation integration support into host params
Browse files Browse the repository at this point in the history
  • Loading branch information
matsko committed Jul 11, 2016
1 parent c482625 commit 453494f
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 57 deletions.
8 changes: 2 additions & 6 deletions modules/@angular/compiler/src/animation/animation_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -44,19 +43,16 @@ export class AnimationCompiler {
`Unable to parse the animation sequence for "${triggerName}" due to the following errors:`;
result.errors.forEach(
(error: AnimationParseError) => { errorMessage += '\n-- ' + error.msg; });
// todo (matsko): include the component name when throwing
groupedErrors.push(errorMessage);
}

if (triggerLookup[triggerName]) {
groupedErrors.push(
`The animation trigger "${triggerName}" has already been registered on "${componentName}"`);
} else {
var factoryName = `${component.type.name}_${entry.name}_${index}`;
index++;

var factoryName = `${componentName}_${entry.name}`;
var visitor = new _AnimationBuilder(triggerName, factoryName);
var compileResult = visitor.build(result.ast)
var compileResult = visitor.build(result.ast);
compiledAnimations.push(compileResult);
triggerLookup[entry.name] = compileResult;
}
Expand Down
8 changes: 6 additions & 2 deletions modules/@angular/compiler/src/compile_metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
}
});
}
Expand Down
22 changes: 16 additions & 6 deletions modules/@angular/compiler/src/template_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -774,13 +774,23 @@ 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)) {
var partValue = parts[0];
if (partValue[0] == '@') {
boundPropertyName = partValue.substr(1);
bindingType = PropertyBindingType.Animation;
securityContext = SecurityContext.NONE;
this._reportError(
`Can't bind to '${boundPropertyName}' since it isn't a known native property`,
sourceSpan);
`Assigning animation triggers within host data as attributes such as "@prop": "exp" is deprecated. Use "[@prop]": "exp" instead!`,
sourceSpan, ParseErrorLevel.WARNING);
} 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) {
Expand Down
4 changes: 1 addition & 3 deletions modules/@angular/compiler/src/view_compiler/compile_view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,16 +65,14 @@ export class CompileView implements NameResolver {
public literalArrayCount = 0;
public literalMapCount = 0;
public pipeCount = 0;
public animations = new Map<string, CompiledAnimation>();

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);
Expand Down
22 changes: 14 additions & 8 deletions modules/@angular/compiler/src/view_compiler/property_binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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')
Expand All @@ -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
Expand All @@ -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)) {
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 3 additions & 1 deletion modules/@angular/compiler/src/view_compiler/view_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -500,14 +500,16 @@ 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(
'createRenderComponentType',
[
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()
])];
Expand Down
16 changes: 16 additions & 0 deletions modules/@angular/compiler/test/template_parser_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,22 @@ export function main() {
].join('\n')]);
});

it('should issue a warning when host attributes contain a non property-bound animation trigger',
() => {
var dirA = CompileDirectiveMetadata.create({
selector: 'div',
type: new CompileTypeMetadata({moduleUrl: someModuleUrl, name: 'DirA'}),
host: {'@prop': 'expr'}
});

humanizeTplAst(parse('<div></div>', [dirA]));

expect(console.warnings).toEqual([[
'Template parse warnings:',
`Assigning animation triggers within host data as attributes such as "@prop": "exp" is deprecated. Use "[@prop]": "exp" instead! ("[ERROR ->]<div></div>"): TestComp@0:0`
].join('\n')]);
});

it('should not issue a warning when an animation property is bound without an expression',
() => {
humanizeTplAst(parse('<div @something>', []));
Expand Down
6 changes: 4 additions & 2 deletions modules/@angular/core/src/linker/view_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string|any[]>): RenderComponentType {
styles: Array<string|any[]>, 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 */
Expand Down
5 changes: 3 additions & 2 deletions modules/@angular/core/src/render/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string|any[]>) {}
public encapsulation: ViewEncapsulation, public styles: Array<string|any[]>,
public animations: {[key: string]: Function}) {}
}

export abstract class RenderDebugInfo {
Expand Down
60 changes: 48 additions & 12 deletions modules/@angular/core/test/animation/animation_integration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,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],
Expand Down Expand Up @@ -1189,18 +1210,6 @@ function declareTests({useJit}: {useJit: boolean}) {
});
}

@Component({
selector: 'if-cmp',
directives: [NgIf],
template: `
<div *ngIf="exp" [@myAnimation]="exp"></div>
`
})
class DummyIfCmp {
exp = false;
exp2 = false;
}

class InnerContentTrackingAnimationDriver extends MockAnimationDriver {
animate(
element: any, startingStyles: AnimationStyles, keyframes: AnimationKeyframe[],
Expand All @@ -1214,12 +1223,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: `
<div *ngIf="exp" [@myAnimation]="exp"></div>
`
})
class DummyIfCmp {
exp = false;
exp2 = false;
}

@Component({
selector: 'if-cmp',
host: {'[@loading]': 'exp'},
directives: [NgIf],
template: `
<div>loading...</div>
`
})
class DummyLoadingCmp {
exp = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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), {});
}
}

Expand Down
27 changes: 14 additions & 13 deletions modules/playground/src/animate/app/animate-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@ import {
} from '@angular/core';

@Component({
host: {
'[@backgroundAnimation]': "bgStatus"
},
selector: 'animate-app',
styleUrls: ['css/animate-app.css'],
template: `
<div [@backgroundAnimation]="bgStatus">
<button (click)="state='start'">Start State</button>
<button (click)="state='active'">Active State</button>
|
<button (click)="state='void'">Void State</button>
<button (click)="state='default'">Unhandled (default) State</button>
<button style="float:right" (click)="bgStatus='blur'">Blur Page</button>
<hr />
<div *ngFor="let item of items" class="box" [@boxAnimation]="state">
{{ item }}
<div *ngIf="true">
something inside
</div>
<button (click)="state='start'">Start State</button>
<button (click)="state='active'">Active State</button>
|
<button (click)="state='void'">Void State</button>
<button (click)="state='default'">Unhandled (default) State</button>
<button style="float:right" (click)="bgStatus='blur'">Blur Page (Host)</button>
<hr />
<div *ngFor="let item of items" class="box" [@boxAnimation]="state">
{{ item }}
<div *ngIf="true">
something inside
</div>
</div>
`,
Expand Down
11 changes: 11 additions & 0 deletions modules/playground/src/animate/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
<html>
<title>Animation Example</title>
<link rel="stylesheet" type="text/css" href="./css/app.css" />
<style>
animate-app {
display:block;
position:fixed;
top:0;
left:0;
right:0;
bottom:0;
padding:50px;
}
</style>
<body>
<animate-app>Loading...</animate-app>
<script src="../bootstrap.js"></script>
Expand Down
Loading

0 comments on commit 453494f

Please sign in to comment.