feat(animate): adds basic support for CSS animations on enter and leave

Closes #3876
This commit is contained in:
Robert Messerle 2015-08-28 14:39:34 -07:00
parent effbb54f3d
commit 39ce9d3397
26 changed files with 688 additions and 8 deletions

View File

@ -1,4 +1,5 @@
export * from './core';
export * from './profile';
export * from './lifecycle_hooks';
export * from './bootstrap';
export * from './bootstrap';
export * from './animate';

View File

@ -0,0 +1,5 @@
export {Animation} from './src/animate/animation';
export {AnimationBuilder} from './src/animate/animation_builder';
export {BrowserDetails} from './src/animate/browser_details';
export {CssAnimationBuilder} from './src/animate/css_animation_builder';
export {CssAnimationOptions} from './src/animate/css_animation_options';

View File

@ -0,0 +1,188 @@
import {
DateWrapper,
StringWrapper,
RegExpWrapper,
NumberWrapper
} from 'angular2/src/core/facade/lang';
import {Math} from 'angular2/src/core/facade/math';
import {camelCaseToDashCase} from 'angular2/src/core/render/dom/util';
import {StringMapWrapper} from 'angular2/src/core/facade/collection';
import {DOM} from 'angular2/src/core/dom/dom_adapter';
import {BrowserDetails} from './browser_details';
import {CssAnimationOptions} from './css_animation_options';
export class Animation {
/** functions to be called upon completion */
callbacks: Function[] = [];
/** the duration (ms) of the animation (whether from CSS or manually set) */
computedDuration: number;
/** the animation delay (ms) (whether from CSS or manually set) */
computedDelay: number;
/** timestamp of when the animation started */
startTime: number;
/** functions for removing event listeners */
eventClearFunctions: Function[] = [];
/** flag used to track whether or not the animation has finished */
completed: boolean = false;
/** total amount of time that the animation should take including delay */
get totalTime(): number {
let delay = this.computedDelay != null ? this.computedDelay : 0;
let duration = this.computedDuration != null ? this.computedDuration : 0;
return delay + duration;
}
/**
* Stores the start time and starts the animation
* @param element
* @param data
* @param browserDetails
*/
constructor(public element: HTMLElement, public data: CssAnimationOptions,
public browserDetails: BrowserDetails) {
this.startTime = DateWrapper.toMillis(DateWrapper.now());
this.setup();
this.wait(timestamp => this.start());
}
wait(callback: Function) {
// Firefox requires 2 frames for some reason
this.browserDetails.raf(callback, 2);
}
/**
* Sets up the initial styles before the animation is started
*/
setup(): void {
if (this.data.fromStyles != null) this.applyStyles(this.data.fromStyles);
if (this.data.duration != null)
this.applyStyles({'transitionDuration': this.data.duration.toString() + 'ms'});
if (this.data.delay != null)
this.applyStyles({'transitionDelay': this.data.delay.toString() + 'ms'});
}
/**
* After the initial setup has occurred, this method adds the animation styles
*/
start(): void {
this.addClasses(this.data.classesToAdd);
this.addClasses(this.data.animationClasses);
this.removeClasses(this.data.classesToRemove);
if (this.data.toStyles != null) this.applyStyles(this.data.toStyles);
var computedStyles = DOM.getComputedStyle(this.element);
this.computedDelay =
Math.max(this.parseDurationString(computedStyles.getPropertyValue('transition-delay')),
this.parseDurationString(this.element.style.getPropertyValue('transition-delay')));
this.computedDuration = Math.max(
this.parseDurationString(computedStyles.getPropertyValue('transition-duration')),
this.parseDurationString(this.element.style.getPropertyValue('transition-duration')));
this.addEvents();
}
/**
* Applies the provided styles to the element
* @param styles
*/
applyStyles(styles: StringMap<string, any>): void {
StringMapWrapper.forEach(styles, (value, key) => {
DOM.setStyle(this.element, camelCaseToDashCase(key), value.toString());
});
}
/**
* Adds the provided classes to the element
* @param classes
*/
addClasses(classes: string[]): void {
for (let i = 0, len = classes.length; i < len; i++) DOM.addClass(this.element, classes[i]);
}
/**
* Removes the provided classes from the element
* @param classes
*/
removeClasses(classes: string[]): void {
for (let i = 0, len = classes.length; i < len; i++) DOM.removeClass(this.element, classes[i]);
}
/**
* Adds events to track when animations have finished
*/
addEvents(): void {
if (this.totalTime > 0) {
this.eventClearFunctions.push(DOM.onAndCancel(
this.element, 'transitionend', (event: any) => this.handleAnimationEvent(event)));
} else {
this.handleAnimationCompleted();
}
}
handleAnimationEvent(event: any): void {
let elapsedTime = Math.round(event.elapsedTime * 1000);
if (!this.browserDetails.elapsedTimeIncludesDelay) elapsedTime += this.computedDelay;
event.stopPropagation();
if (elapsedTime >= this.totalTime) this.handleAnimationCompleted();
}
/**
* Runs all animation callbacks and removes temporary classes
*/
handleAnimationCompleted(): void {
this.removeClasses(this.data.animationClasses);
this.callbacks.forEach(callback => callback());
this.callbacks = [];
this.eventClearFunctions.forEach(fn => fn());
this.eventClearFunctions = [];
this.completed = true;
}
/**
* Adds animation callbacks to be called upon completion
* @param callback
* @returns {Animation}
*/
onComplete(callback: Function): Animation {
if (this.completed) {
callback();
} else {
this.callbacks.push(callback);
}
return this;
}
/**
* Converts the duration string to the number of milliseconds
* @param duration
* @returns {number}
*/
parseDurationString(duration: string): number {
var maxValue = 0;
// duration must have at least 2 characters to be valid. (number + type)
if (duration == null || duration.length < 2) {
return maxValue;
} else if (duration.substring(duration.length - 2) == 'ms') {
let value = NumberWrapper.parseInt(this.stripLetters(duration), 10);
if (value > maxValue) maxValue = value;
} else if (duration.substring(duration.length - 1) == 's') {
let ms = NumberWrapper.parseFloat(this.stripLetters(duration)) * 1000;
let value = Math.floor(ms);
if (value > maxValue) maxValue = value;
}
return maxValue;
}
/**
* Strips the letters from the duration string
* @param str
* @returns {string}
*/
stripLetters(str: string): string {
return StringWrapper.replaceAll(str, RegExpWrapper.create('[^0-9]+$', ''), '');
}
}

View File

@ -0,0 +1,19 @@
import {Injectable} from 'angular2/src/core/di';
import {CssAnimationBuilder} from './css_animation_builder';
import {BrowserDetails} from './browser_details';
@Injectable()
export class AnimationBuilder {
/**
* Used for DI
* @param browserDetails
*/
constructor(public browserDetails: BrowserDetails) {}
/**
* Creates a new CSS Animation
* @returns {CssAnimationBuilder}
*/
css(): CssAnimationBuilder { return new CssAnimationBuilder(this.browserDetails); }
}

View File

@ -0,0 +1,54 @@
import {Injectable} from 'angular2/src/core/di';
import {Math} from 'angular2/src/core/facade/math';
import {DOM} from 'angular2/src/core/dom/dom_adapter';
@Injectable()
export class BrowserDetails {
elapsedTimeIncludesDelay = false;
constructor() { this.doesElapsedTimeIncludesDelay(); }
/**
* Determines if `event.elapsedTime` includes transition delay in the current browser. At this
* time, Chrome and Opera seem to be the only browsers that include this.
*/
doesElapsedTimeIncludesDelay(): void {
var div = DOM.createElement('div');
DOM.setAttribute(div, 'style', `position: absolute; top: -9999px; left: -9999px; width: 1px;
height: 1px; transition: all 1ms linear 1ms;`);
// Firefox requires that we wait for 2 frames for some reason
this.raf(timestamp => {
DOM.on(div, 'transitionend', (event: any) => {
var elapsed = Math.round(event.elapsedTime * 1000);
this.elapsedTimeIncludesDelay = elapsed == 2;
DOM.remove(div);
});
DOM.setStyle(div, 'width', '2px');
}, 2);
}
raf(callback: Function, frames: number = 1): Function {
var queue: RafQueue = new RafQueue(callback, frames);
return () => queue.cancel();
}
}
class RafQueue {
currentFrameId: number;
constructor(public callback: Function, public frames: number) { this._raf(); }
private _raf() {
this.currentFrameId = DOM.requestAnimationFrame(timestamp => this._nextFrame(timestamp));
}
private _nextFrame(timestamp: number) {
this.frames--;
if (this.frames > 0) {
this._raf();
} else {
this.callback(timestamp);
}
}
cancel() {
DOM.cancelAnimationFrame(this.currentFrameId);
this.currentFrameId = null;
}
}

View File

@ -0,0 +1,93 @@
import {CssAnimationOptions} from './css_animation_options';
import {Animation} from './animation';
import {BrowserDetails} from './browser_details';
export class CssAnimationBuilder {
/** @type {CssAnimationOptions} */
data: CssAnimationOptions = new CssAnimationOptions();
/**
* Accepts public properties for CssAnimationBuilder
*/
constructor(public browserDetails: BrowserDetails) {}
/**
* Adds a temporary class that will be removed at the end of the animation
* @param className
*/
addAnimationClass(className: string): CssAnimationBuilder {
this.data.animationClasses.push(className);
return this;
}
/**
* Adds a class that will remain on the element after the animation has finished
* @param className
*/
addClass(className: string): CssAnimationBuilder {
this.data.classesToAdd.push(className);
return this;
}
/**
* Removes a class from the element
* @param className
*/
removeClass(className: string): CssAnimationBuilder {
this.data.classesToRemove.push(className);
return this;
}
/**
* Sets the animation duration (and overrides any defined through CSS)
* @param duration
*/
setDuration(duration: number): CssAnimationBuilder {
this.data.duration = duration;
return this;
}
/**
* Sets the animation delay (and overrides any defined through CSS)
* @param delay
*/
setDelay(delay: number): CssAnimationBuilder {
this.data.delay = delay;
return this;
}
/**
* Sets styles for both the initial state and the destination state
* @param from
* @param to
*/
setStyles(from: StringMap<string, any>, to: StringMap<string, any>): CssAnimationBuilder {
return this.setFromStyles(from).setToStyles(to);
}
/**
* Sets the initial styles for the animation
* @param from
*/
setFromStyles(from: StringMap<string, any>): CssAnimationBuilder {
this.data.fromStyles = from;
return this;
}
/**
* Sets the destination styles for the animation
* @param to
*/
setToStyles(to: StringMap<string, any>): CssAnimationBuilder {
this.data.toStyles = to;
return this;
}
/**
* Starts the animation and returns a promise
* @param element
*/
start(element: HTMLElement): Animation {
return new Animation(element, this.data, this.browserDetails);
}
}

View File

@ -0,0 +1,22 @@
export class CssAnimationOptions {
/** initial styles for the element */
fromStyles: StringMap<string, any>;
/** destination styles for the element */
toStyles: StringMap<string, any>;
/** classes to be added to the element */
classesToAdd: string[] = [];
/** classes to be removed from the element */
classesToRemove: string[] = [];
/** classes to be added for the duration of the animation */
animationClasses: string[] = [];
/** override the duration of the animation (in milliseconds) */
duration: number;
/** override the transition delay (in milliseconds) */
delay: number;
}

View File

@ -86,6 +86,8 @@ import {APP_COMPONENT_REF_PROMISE, APP_COMPONENT} from './application_tokens';
import {wtfInit} from './profile/wtf_init';
import {EXCEPTION_BINDING} from './platform_bindings';
import {ApplicationRef} from './application_ref';
import {AnimationBuilder} from 'angular2/src/animate/animation_builder';
import {BrowserDetails} from 'angular2/src/animate/browser_details';
var _rootInjector: Injector;
@ -161,6 +163,8 @@ function _injectorBindings(appComponentType): Array<Type | Binding | any[]> {
Testability,
AnchorBasedAppRootUrl,
bind(AppRootUrl).toAlias(AnchorBasedAppRootUrl),
BrowserDetails,
AnimationBuilder,
FORM_BINDINGS
];
}

View File

@ -451,6 +451,8 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
return element.dataset[name];
}
getComputedStyle(elem) => elem.getComputedStyle();
// TODO(tbosch): move this into a separate environment class once we have it
setGlobalVar(String path, value) {
var parts = path.split('.');
@ -465,6 +467,14 @@ class BrowserDomAdapter extends GenericBrowserDomAdapter {
}
obj[parts.removeAt(0)] = value;
}
requestAnimationFrame(callback) {
return window.requestAnimationFrame(callback);
}
cancelAnimationFrame(id) {
window.cancelAnimationFrame(id);
}
}
var baseElement = null;

View File

@ -317,8 +317,11 @@ export class BrowserDomAdapter extends GenericBrowserDomAdapter {
this.setAttribute(element, 'data-' + name, value);
}
getData(element, name: string): string { return this.getAttribute(element, 'data-' + name); }
getComputedStyle(element): any { return getComputedStyle(element); }
// TODO(tbosch): move this into a separate environment class once we have it
setGlobalVar(path: string, value: any) { setValueOnPath(global, path, value); }
requestAnimationFrame(callback): number { return window.requestAnimationFrame(callback); }
cancelAnimationFrame(id: number) { window.cancelAnimationFrame(id); }
}

View File

@ -133,6 +133,9 @@ export class DomAdapter {
resetBaseElement(): void { throw _abstract(); }
getUserAgent(): string { throw _abstract(); }
setData(element, name: string, value: string) { throw _abstract(); }
getComputedStyle(element): any { throw _abstract(); }
getData(element, name: string): string { throw _abstract(); }
setGlobalVar(name: string, value: any) { throw _abstract(); }
requestAnimationFrame(callback): number { throw _abstract(); }
cancelAnimationFrame(id) { throw _abstract(); }
}

View File

@ -412,6 +412,10 @@ class Html5LibDomAdapter implements DomAdapter {
this.setAttribute(element, 'data-${name}', value);
}
getComputedStyle(element) {
throw 'not implemented';
}
String getData(Element element, String name) {
return this.getAttribute(element, 'data-${name}');
}
@ -420,4 +424,11 @@ class Html5LibDomAdapter implements DomAdapter {
setGlobalVar(String name, value) {
// noop on the server
}
requestAnimationFrame(callback) {
throw 'not implemented';
}
cancelAnimationFrame(id) {
throw 'not implemented';
}
}

View File

@ -539,9 +539,12 @@ export class Parse5DomAdapter extends DomAdapter {
getLocation(): Location { throw 'not implemented'; }
getUserAgent(): string { return "Fake user agent"; }
getData(el, name: string): string { return this.getAttribute(el, 'data-' + name); }
getComputedStyle(el): any { throw 'not implemented'; }
setData(el, name: string, value: string) { this.setAttribute(el, 'data-' + name, value); }
// TODO(tbosch): move this into a separate environment class once we have it
setGlobalVar(path: string, value: any) { setValueOnPath(global, path, value); }
requestAnimationFrame(callback): number { return setTimeout(callback, 0); }
cancelAnimationFrame(id: number) { clearTimeout(id); }
}
// TODO: build a proper list, this one is all the keys of a HTMLInputElement

View File

@ -19,4 +19,6 @@ class Math {
static num ceil(num a) => a.ceil();
static num sqrt(num x) => math.sqrt(x);
static num round(num x) => x.round();
}

View File

@ -1,4 +1,5 @@
import {Inject, Injectable, OpaqueToken} from 'angular2/src/core/di';
import {AnimationBuilder} from 'angular2/src/animate/animation_builder';
import {isPresent, isBlank, RegExpWrapper, CONST_EXPR} from 'angular2/src/core/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptions';
@ -42,7 +43,7 @@ export class DomRenderer extends Renderer {
* @private
*/
constructor(private _eventManager: EventManager,
private _domSharedStylesHost: DomSharedStylesHost,
private _domSharedStylesHost: DomSharedStylesHost, private _animate: AnimationBuilder,
private _templateCloner: TemplateCloner, @Inject(DOCUMENT) document) {
super();
this._document = document;
@ -94,7 +95,51 @@ export class DomRenderer extends Renderer {
var previousFragmentNodes = resolveInternalDomFragment(previousFragmentRef);
if (previousFragmentNodes.length > 0) {
var sibling = previousFragmentNodes[previousFragmentNodes.length - 1];
moveNodesAfterSibling(sibling, resolveInternalDomFragment(fragmentRef));
let nodes = resolveInternalDomFragment(fragmentRef);
moveNodesAfterSibling(sibling, nodes);
this.animateNodesEnter(nodes);
}
}
/**
* Iterates through all nodes being added to the DOM and animates them if necessary
* @param nodes
*/
animateNodesEnter(nodes: Node[]) {
for (let i = 0; i < nodes.length; i++) this.animateNodeEnter(nodes[i]);
}
/**
* Performs animations if necessary
* @param node
*/
animateNodeEnter(node: Node) {
if (DOM.isElementNode(node) && DOM.hasClass(node, 'ng-animate')) {
DOM.addClass(node, 'ng-enter');
this._animate.css()
.addAnimationClass('ng-enter-active')
.start(<HTMLElement>node)
.onComplete(() => { DOM.removeClass(node, 'ng-enter'); });
}
}
/**
* If animations are necessary, performs animations then removes the element; otherwise, it just
* removes the element.
* @param node
*/
animateNodeLeave(node: Node) {
if (DOM.isElementNode(node) && DOM.hasClass(node, 'ng-animate')) {
DOM.addClass(node, 'ng-leave');
this._animate.css()
.addAnimationClass('ng-leave-active')
.start(<HTMLElement>node)
.onComplete(() => {
DOM.removeClass(node, 'ng-leave');
DOM.remove(node);
});
} else {
DOM.remove(node);
}
}
@ -104,7 +149,9 @@ export class DomRenderer extends Renderer {
}
var parentView = resolveInternalDomView(elementRef.renderView);
var element = parentView.boundElements[elementRef.renderBoundElementIndex];
moveNodesAfterSibling(element, resolveInternalDomFragment(fragmentRef));
var nodes = resolveInternalDomFragment(fragmentRef);
moveNodesAfterSibling(element, nodes);
this.animateNodesEnter(nodes);
}
_detachFragmentScope = wtfCreateScope('DomRenderer#detachFragment()');
@ -112,7 +159,7 @@ export class DomRenderer extends Renderer {
var s = this._detachFragmentScope();
var fragmentNodes = resolveInternalDomFragment(fragmentRef);
for (var i = 0; i < fragmentNodes.length; i++) {
DOM.remove(fragmentNodes[i]);
this.animateNodeLeave(fragmentNodes[i]);
}
wtfLeave(s);
}

View File

@ -0,0 +1,33 @@
import {Injectable} from 'angular2/src/core/di';
import {AnimationBuilder} from 'angular2/src/animate/animation_builder';
import {CssAnimationBuilder} from 'angular2/src/animate/css_animation_builder';
import {CssAnimationOptions} from 'angular2/src/animate/css_animation_options';
import {Animation} from 'angular2/src/animate/animation';
import {BrowserDetails} from 'angular2/src/animate/browser_details';
@Injectable()
export class MockAnimationBuilder extends AnimationBuilder {
constructor() { super(null); }
css(): MockCssAnimationBuilder { return new MockCssAnimationBuilder(); }
}
class MockCssAnimationBuilder extends CssAnimationBuilder {
constructor() { super(null); }
start(element: HTMLElement): Animation { return new MockAnimation(element, this.data); }
}
class MockBrowserAbstraction extends BrowserDetails {
doesElapsedTimeIncludesDelay(): void { this.elapsedTimeIncludesDelay = false; }
}
class MockAnimation extends Animation {
private _callback: Function;
constructor(element: HTMLElement, data: CssAnimationOptions) {
super(element, data, new MockBrowserAbstraction());
}
wait(callback: Function) { this._callback = callback; }
flush() {
this._callback(0);
this._callback = null;
}
}

View File

@ -1,5 +1,7 @@
import {bind, Binding} from 'angular2/src/core/di';
import {DEFAULT_PIPES} from 'angular2/src/core/pipes';
import {AnimationBuilder} from 'angular2/src/animate/animation_builder';
import {MockAnimationBuilder} from 'angular2/src/mock/animation_builder_mock';
import {Compiler, CompilerCache} from 'angular2/src/core/compiler/compiler';
import {Reflector, reflector} from 'angular2/src/core/reflection/reflection';
@ -149,6 +151,7 @@ function _getAppBindings() {
StyleInliner,
TestComponentBuilder,
bind(NgZone).toClass(MockNgZone),
bind(AnimationBuilder).toClass(MockAnimationBuilder),
EventManager,
new Binding(EVENT_MANAGER_PLUGINS, {toClass: DomEventsPlugin, multi: true})
];

View File

@ -2,6 +2,8 @@
// There should be a way to refactor application so that this file is unnecessary. See #3277
import {Injector, bind, Binding} from "angular2/src/core/di";
import {DEFAULT_PIPES} from 'angular2/src/core/pipes';
import {AnimationBuilder} from 'angular2/src/animate/animation_builder';
import {BrowserDetails} from 'angular2/src/animate/browser_details';
import {Reflector, reflector} from 'angular2/src/core/reflection/reflection';
import {
Parser,
@ -140,7 +142,9 @@ function _injectorBindings(): any[] {
MessageBasedXHRImpl,
MessageBasedRenderer,
ServiceMessageBrokerFactory,
ClientMessageBrokerFactory
ClientMessageBrokerFactory,
BrowserDetails,
AnimationBuilder,
];
}

View File

@ -0,0 +1,119 @@
import {el, describe, it, expect, inject, SpyObject} from 'angular2/test_lib';
import {AnimationBuilder} from 'angular2/src/animate/animation_builder';
export function main() {
describe("AnimationBuilder", () => {
it('should have data object', inject([AnimationBuilder], animate => {
var animateCss = animate.css();
expect(animateCss.data).toBeDefined();
}));
it('should allow you to add classes', inject([AnimationBuilder], animate => {
var animateCss = animate.css();
animateCss.addClass('some-class');
expect(animateCss.data.classesToAdd).toEqual(['some-class']);
animateCss.addClass('another-class');
expect(animateCss.data.classesToAdd).toEqual(['some-class', 'another-class']);
}));
it('should allow you to add temporary classes', inject([AnimationBuilder], animate => {
var animateCss = animate.css();
animateCss.addAnimationClass('some-class');
expect(animateCss.data.animationClasses).toEqual(['some-class']);
animateCss.addAnimationClass('another-class');
expect(animateCss.data.animationClasses).toEqual(['some-class', 'another-class']);
}));
it('should allow you to remove classes', inject([AnimationBuilder], animate => {
var animateCss = animate.css();
animateCss.removeClass('some-class');
expect(animateCss.data.classesToRemove).toEqual(['some-class']);
animateCss.removeClass('another-class');
expect(animateCss.data.classesToRemove).toEqual(['some-class', 'another-class']);
}));
it('should support chaining', inject([AnimationBuilder], animate => {
var animateCss = animate.css()
.addClass('added-class')
.removeClass('removed-class')
.addAnimationClass('temp-class')
.addClass('another-added-class');
expect(animateCss.data.classesToAdd).toEqual(['added-class', 'another-added-class']);
expect(animateCss.data.classesToRemove).toEqual(['removed-class']);
expect(animateCss.data.animationClasses).toEqual(['temp-class']);
}));
it('should support duration and delay', inject([AnimationBuilder], (animate) => {
var animateCss = animate.css();
animateCss.setDelay(100).setDuration(200);
expect(animateCss.data.duration).toBe(200);
expect(animateCss.data.delay).toBe(100);
var element = el('<div></div>');
var runner = animateCss.start(element);
runner.flush();
expect(runner.computedDelay).toBe(100);
expect(runner.computedDuration).toBe(200);
}));
it('should support from styles', inject([AnimationBuilder], animate => {
var animateCss = animate.css();
animateCss.setFromStyles({'backgroundColor': 'blue'});
expect(animateCss.data.fromStyles).toBeDefined();
var element = el('<div></div>');
animateCss.start(element);
expect(element.style.getPropertyValue('background-color')).toEqual('blue');
}));
it('should support duration and delay defined in CSS', inject([AnimationBuilder], (animate) => {
var animateCss = animate.css();
var element = el('<div style="transition: 0.5s ease 250ms;"></div>');
var runner = animateCss.start(element);
runner.flush();
expect(runner.computedDuration).toEqual(500);
expect(runner.computedDelay).toEqual(250);
}));
it('should add classes', inject([AnimationBuilder], (animate) => {
var animateCss = animate.css().addClass('one').addClass('two');
var element = el('<div></div>');
var runner = animateCss.start(element);
expect(element).not.toHaveCssClass('one');
expect(element).not.toHaveCssClass('two');
runner.flush();
expect(element).toHaveCssClass('one');
expect(element).toHaveCssClass('two');
}));
it('should call `onComplete` method after animations have finished',
inject([AnimationBuilder], (animate) => {
var spyObject = new SpyObject();
var callback = spyObject.spy('animationFinished');
var runner = animate.css()
.addClass('one')
.addClass('two')
.setDuration(100)
.start(el('<div></div>'))
.onComplete(callback);
expect(callback).not.toHaveBeenCalled();
runner.flush();
expect(callback).not.toHaveBeenCalled();
runner.handleAnimationCompleted();
expect(callback).toHaveBeenCalled();
}));
});
}

View File

@ -21,7 +21,6 @@ transformers:
# The build currently fails on material files because there is not yet
# support for transforming cross-package urls. (see issue #2982)
- 'web/src/material/**'
- 'web/src/zippy_component/**'
# No need to transform the dart:mirrors specific entrypoints
- '**/index_dynamic.dart'
entry_points:
@ -39,6 +38,7 @@ transformers:
- web/src/web_workers/todo/background_index.dart
- web/src/web_workers/message_broker/background_index.dart
- web/src/web_workers/kitchen_sink/background_index.dart
- web/src/zippy_component/index.dart
# These entrypoints are disabled untl the transformer supports UI bootstrap (issue #3971)
# - web/src/web_workers/message_broker/index.dart
@ -53,7 +53,6 @@ transformers:
# - web/src/material/progress-linear/index.dart
# - web/src/material/radio/index.dart
# - web/src/material/switcher/index.dart
# - web/src/zippy_component/index.dart
#
# This entrypoint is not needed:
# - web/src/material/demo_common.dart

View File

@ -0,0 +1,14 @@
import {Component, View, NgIf} from 'angular2/angular2';
@Component({selector: 'animate-app'})
@View({
directives: [NgIf],
template: `
<h1>The box is {{visible ? 'visible' : 'hidden'}}</h1>
<div class="ng-animate box" *ng-if="visible"></div>
<button (click)="visible = !visible">Animate</button>
`
})
export class AnimateApp {
visible: boolean = false;
}

View File

@ -0,0 +1,25 @@
body {
padding: 20px;
}
.box {
width: 100px;
height: 100px;
background-color: red;
transition: all 0.35s ease-in-out;
}
.box.blue {
background-color: blue;
}
.box.ng-enter {
opacity: 0;
height: 0;
}
.box.ng-enter-active {
opacity: 1;
height: 100px;
}
.box.ng-leave-active {
opacity: 0;
height: 0;
}

View File

@ -0,0 +1,10 @@
<!doctype html>
<html>
<title>Animation Example</title>
<link rel="stylesheet" type="text/css" href="./css/app.css" />
<base href="/examples/src/animate/">
<body>
<animate-app>Loading...</animate-app>
$SCRIPTS$
</body>
</html>

View File

@ -0,0 +1,6 @@
import {AnimateApp} from './animate-app';
import {bootstrap} from 'angular2/bootstrap';
export function main() {
bootstrap(AnimateApp);
}

View File

@ -40,6 +40,7 @@ const kServedPaths = [
'benchmarks_external/src/static_tree',
// Relative (to /modules) paths to example directories
'examples/src/animate',
'examples/src/benchpress',
'examples/src/model_driven_forms',
'examples/src/template_driven_forms',

View File

@ -20,6 +20,7 @@ module.exports = function makeNodeTree(destinationPath) {
include: ['angular2/**', 'benchpress/**', '**/e2e_test/**'],
exclude: [
// the following code and tests are not compatible with CJS/node environment
'angular2/test/animate/**',
'angular2/test/core/zone/**',
'angular2/test/test_lib/fake_async_spec.ts',
'angular2/test/core/render/xhr_impl_spec.ts',