feat(animations): support styling of the default animation state

It is now possible to set a fallback state that will apply its
styling when the destination state is not detected.

```ts
state("*", style({ ... }))
```

Closes #9013
This commit is contained in:
Matias Niemelä 2016-06-03 17:52:33 -07:00
parent c3d2459a4e
commit 36d25f2a07
9 changed files with 121 additions and 11 deletions

View File

@ -1,19 +1,21 @@
import {Component, trigger, state, animate, transition, style} from '@angular/core';
import {AUTO_STYLE, Component, trigger, state, animate, transition, style} from '@angular/core';
@Component({
selector: "animate-cmp",
animations: [
trigger('openClose', [
state("*", style({ height: AUTO_STYLE, color: 'black', borderColor: 'black' })),
state('closed, void',
style({ height:"0px", color: "maroon", borderColor: "maroon" })),
state('open',
style({ height:"*", borderColor:"green", color:"green" })),
style({ height: AUTO_STYLE, borderColor:"green", color:"green" })),
transition("* => *", animate(500))
])
],
template: `
<button (click)="setAsOpen()">Open</button>
<button (click)="setAsClosed()">Closed</button>
<button (click)="setAsSomethingElse()">Something Else</button>
<hr />
<div @openClose="stateExpression">
Look at this box
@ -25,6 +27,9 @@ export class AnimateCmp {
constructor() {
this.setAsClosed();
}
setAsSomethingElse() {
this.stateExpression = 'something';
}
setAsOpen() {
this.stateExpression = 'open';
}

View File

@ -8,15 +8,19 @@ import {AUTO_STYLE, ReflectiveInjector, DebugElement, getDebugNode} from '@angul
import {browserPlatform, BROWSER_APP_PROVIDERS} from '@angular/platform-browser';
describe("template codegen output", () => {
function findTargetElement(elm: DebugElement): DebugElement {
// the open-close-container is a child of the main container
// if the template changes then please update the location below
return elm.children[4];
}
it("should apply the animate states to the element", (done) => {
const appInjector = ReflectiveInjector.resolveAndCreate(BROWSER_APP_PROVIDERS,
browserPlatform().injector);
var comp = AnimateCmpNgFactory.create(appInjector);
var debugElement = <DebugElement>getDebugNode(comp.location.nativeElement);
// the open-close-container is a child of the main container
// if the template changes then please update the location below
var targetDebugElement = <DebugElement>debugElement.children[3];
var targetDebugElement = findTargetElement(<DebugElement>debugElement);
comp.instance.setAsOpen();
comp.changeDetectorRef.detectChanges();
@ -37,4 +41,32 @@ describe("template codegen output", () => {
}, 0);
}, 0);
});
it("should apply the default animate state to the element", (done) => {
const appInjector = ReflectiveInjector.resolveAndCreate(BROWSER_APP_PROVIDERS,
browserPlatform().injector);
var comp = AnimateCmpNgFactory.create(appInjector);
var debugElement = <DebugElement>getDebugNode(comp.location.nativeElement);
var targetDebugElement = findTargetElement(<DebugElement>debugElement);
comp.instance.setAsSomethingElse();
comp.changeDetectorRef.detectChanges();
setTimeout(() => {
expect(targetDebugElement.styles['height']).toEqual(AUTO_STYLE);
expect(targetDebugElement.styles['borderColor']).toEqual('black');
expect(targetDebugElement.styles['color']).toEqual('black');
comp.instance.setAsClosed();
comp.changeDetectorRef.detectChanges();
setTimeout(() => {
expect(targetDebugElement.styles['height']).not.toEqual(AUTO_STYLE);
expect(targetDebugElement.styles['borderColor']).not.toEqual('grey');
expect(targetDebugElement.styles['color']).not.toEqual('grey');
done();
}, 0);
}, 0);
});
});

View File

@ -77,6 +77,7 @@ export var AnimationKeyframe: typeof t.AnimationKeyframe = r.AnimationKeyframe;
export type AnimationStyles = t.AnimationStyles;
export var AnimationStyles: typeof t.AnimationStyles = r.AnimationStyles;
export var ANY_STATE = r.ANY_STATE;
export var DEFAULT_STATE = r.DEFAULT_STATE;
export var EMPTY_STATE = r.EMPTY_STATE;
export var FILL_STYLE_FLAG = r.FILL_STYLE_FLAG;
export var balanceAnimationStyles: typeof t.balanceAnimationStyles = r.balanceAnimationStyles;

View File

@ -6,7 +6,7 @@ import {Identifiers} from '../identifiers';
import * as o from '../output/output_ast';
import {AUTO_STYLE} from '@angular/core';
import {ANY_STATE, EMPTY_STATE} from '../../core_private';
import {DEFAULT_STATE, ANY_STATE, EMPTY_STATE} from '../../core_private';
import {
AnimationParseError,
@ -64,6 +64,7 @@ export class AnimationCompiler {
}
var _ANIMATION_FACTORY_ELEMENT_VAR = o.variable('element');
var _ANIMATION_DEFAULT_STATE_VAR = o.variable('defaultStateStyles');
var _ANIMATION_FACTORY_VIEW_VAR = o.variable('view');
var _ANIMATION_FACTORY_RENDERER_VAR = _ANIMATION_FACTORY_VIEW_VAR.prop('renderer');
var _ANIMATION_CURRENT_STATE_VAR = o.variable('currentState');
@ -212,6 +213,9 @@ class _AnimationBuilder implements AnimationAstVisitor {
//visit each of the declarations first to build the context state map
ast.stateDeclarations.forEach(def => def.visit(this, context));
//this should always be defined even if the user overrides it
context.stateMap.registerState(DEFAULT_STATE, {});
var statements = [];
statements.push(
_ANIMATION_FACTORY_VIEW_VAR.callMethod('cancelActiveAnimation', [
@ -223,6 +227,11 @@ class _AnimationBuilder implements AnimationAstVisitor {
statements.push(_ANIMATION_COLLECTED_STYLES.set(EMPTY_MAP).toDeclStmt());
statements.push(_ANIMATION_PLAYER_VAR.set(o.NULL_EXPR).toDeclStmt());
statements.push(
_ANIMATION_DEFAULT_STATE_VAR.set(
this._statesMapVar.key(o.literal(DEFAULT_STATE))
).toDeclStmt());
statements.push(
_ANIMATION_START_STATE_STYLES_VAR.set(
this._statesMapVar.key(_ANIMATION_CURRENT_STATE_VAR)
@ -230,7 +239,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
statements.push(
new o.IfStmt(_ANIMATION_START_STATE_STYLES_VAR.equals(o.NULL_EXPR), [
_ANIMATION_START_STATE_STYLES_VAR.set(EMPTY_MAP).toStmt()
_ANIMATION_START_STATE_STYLES_VAR.set(_ANIMATION_DEFAULT_STATE_VAR).toStmt()
]));
statements.push(
@ -240,7 +249,7 @@ class _AnimationBuilder implements AnimationAstVisitor {
statements.push(
new o.IfStmt(_ANIMATION_END_STATE_STYLES_VAR.equals(o.NULL_EXPR), [
_ANIMATION_END_STATE_STYLES_VAR.set(EMPTY_MAP).toStmt()
_ANIMATION_END_STATE_STYLES_VAR.set(_ANIMATION_DEFAULT_STATE_VAR).toStmt()
]));

View File

@ -39,6 +39,7 @@ import {AnimationStyles as AnimationStyles_} from './src/animation/animation_sty
import * as animationUtils from './src/animation/animation_style_util';
import {
ANY_STATE as ANY_STATE_,
DEFAULT_STATE as DEFAULT_STATE_,
EMPTY_STATE as EMPTY_STATE_,
FILL_STYLE_FLAG as FILL_STYLE_FLAG_
} from './src/animation/animation_constants';
@ -130,6 +131,7 @@ export declare namespace __core_private_types__ {
export type AnimationStyles = AnimationStyles_;
export var AnimationStyles: typeof AnimationStyles_;
export var ANY_STATE: typeof ANY_STATE_;
export var DEFAULT_STATE: typeof DEFAULT_STATE_;
export var EMPTY_STATE: typeof EMPTY_STATE_;
export var FILL_STYLE_FLAG: typeof FILL_STYLE_FLAG_;
}
@ -199,6 +201,7 @@ export var __core_private__ = {
collectAndResolveStyles: animationUtils.collectAndResolveStyles,
AnimationStyles: AnimationStyles_,
ANY_STATE: ANY_STATE_,
DEFAULT_STATE: DEFAULT_STATE_,
EMPTY_STATE: EMPTY_STATE_,
FILL_STYLE_FLAG: FILL_STYLE_FLAG_
};

View File

@ -1,3 +1,4 @@
export const FILL_STYLE_FLAG = 'true'; // TODO (matsko): change to boolean
export const ANY_STATE = '*';
export const DEFAULT_STATE = '*';
export const EMPTY_STATE = 'void';

View File

@ -292,6 +292,10 @@ export function style(tokens: string|{[key: string]: string | number}|Array<stri
* of the application anymore (e.g. when an `ngIf` evaluates to false then the state of the associated element
* is void).
*
* #### The `*` (default) state
*
* The `*` state (when styled) is a fallback state that will be used if
* the state that is being animated is not declared within the trigger.
*
* ### Usage
*

View File

@ -25,6 +25,7 @@ import {
import {isPresent, isArray, IS_DART} from '../../src/facade/lang';
import {Component} from '../../index';
import {DEFAULT_STATE} from '../../src/animation/animation_constants';
import {NgIf} from '@angular/common';
@ -702,6 +703,57 @@ function declareTests() {
});
})));
it('should animate to and retain the default animation state styles once the animation is complete if defined',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="target" @status="exp"></div>', [
trigger('status', [
state(DEFAULT_STATE, style({ "background": 'grey' })),
state('green', style({ "background": 'green' })),
state('red', style({ "background": 'red' })),
transition('* => *', [ animate(1000) ])
])
], (fixture) => {
tick();
var cmp = fixture.debugElement.componentInstance;
var node = getDOM().querySelector(fixture.debugElement.nativeElement, '.target');
cmp.exp = 'green';
fixture.detectChanges();
flushMicrotasks();
var animation = driver.log.pop();
var keyframes = animation['keyframeLookup'];
expect(keyframes[1]).toEqual([1, {'background': 'green' }]);
cmp.exp = 'blue';
fixture.detectChanges();
flushMicrotasks();
animation = driver.log.pop();
keyframes = animation['keyframeLookup'];
expect(keyframes[0]).toEqual([0, {'background': 'green' }]);
expect(keyframes[1]).toEqual([1, {'background': 'grey' }]);
cmp.exp = 'red';
fixture.detectChanges();
flushMicrotasks();
animation = driver.log.pop();
keyframes = animation['keyframeLookup'];
expect(keyframes[0]).toEqual([0, {'background': 'grey' }]);
expect(keyframes[1]).toEqual([1, {'background': 'red' }]);
cmp.exp = 'orange';
fixture.detectChanges();
flushMicrotasks();
animation = driver.log.pop();
keyframes = animation['keyframeLookup'];
expect(keyframes[0]).toEqual([0, {'background': 'red' }]);
expect(keyframes[1]).toEqual([1, {'background': 'grey' }]);
});
})));
it('should seed in the origin animation state styles into the first animation step',
inject([TestComponentBuilder, AnimationDriver], fakeAsync((tcb: TestComponentBuilder, driver: MockAnimationDriver) => {
makeAnimationCmp(tcb, '<div class="target" @status="exp"></div>', [

View File

@ -17,7 +17,9 @@ import {
<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">
@ -34,6 +36,7 @@ import {
])
]),
trigger("boxAnimation", [
state("*", style({ "height": "*", "background-color": "#dddddd", "color":"black" })),
state("void, hidden", style({ "height": 0, "opacity": 0 })),
state("start", style({ "background-color": "red", "height": "*" })),
state("active", style({ "background-color": "orange", "color": "white", "font-size":"100px" })),
@ -65,15 +68,15 @@ export class AnimateApp {
get state() { return this._state; }
set state(s) {
this._state = s;
if (s == 'start' || s == 'active') {
if (s == 'void') {
this.items = [];
} else {
this.items = [
1,2,3,4,5,
6,7,8,9,10,
11,12,13,14,15,
16,17,18,19,20
];
} else {
this.items = [];
}
}
}