feat(animate): adds basic support for CSS animations on enter and leave
Closes #3876
This commit is contained in:
parent
effbb54f3d
commit
39ce9d3397
|
@ -1,4 +1,5 @@
|
|||
export * from './core';
|
||||
export * from './profile';
|
||||
export * from './lifecycle_hooks';
|
||||
export * from './bootstrap';
|
||||
export * from './bootstrap';
|
||||
export * from './animate';
|
||||
|
|
|
@ -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';
|
|
@ -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]+$', ''), '');
|
||||
}
|
||||
}
|
|
@ -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); }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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); }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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(); }
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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})
|
||||
];
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}));
|
||||
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
import {AnimateApp} from './animate-app';
|
||||
import {bootstrap} from 'angular2/bootstrap';
|
||||
|
||||
export function main() {
|
||||
bootstrap(AnimateApp);
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue