diff --git a/demo/animate_demo/pubspec.lock b/demo/animate_demo/pubspec.lock new file mode 100644 index 000000000..51eda6677 --- /dev/null +++ b/demo/animate_demo/pubspec.lock @@ -0,0 +1,77 @@ +# Generated by pub +# See http://pub.dartlang.org/doc/glossary.html#lockfile +packages: + analyzer: + description: analyzer + source: hosted + version: "0.10.5" + angular: + description: + path: "../.." + relative: true + source: path + version: "0.9.7" + args: + description: args + source: hosted + version: "0.9.0" + browser: + description: browser + source: hosted + version: "0.9.1" + collection: + description: collection + source: hosted + version: "0.9.1" + di: + description: di + source: hosted + version: "0.0.32" + html5lib: + description: html5lib + source: hosted + version: "0.9.1" + intl: + description: intl + source: hosted + version: "0.9.1" + logging: + description: logging + source: hosted + version: "0.9.1+1" + path: + description: path + source: hosted + version: "1.0.0" + perf_api: + description: perf_api + source: hosted + version: "0.0.8" + route_hierarchical: + description: route_hierarchical + source: hosted + version: "0.4.14" + shadow_dom: + description: shadow_dom + source: hosted + version: "0.9.2" + source_maps: + description: source_maps + source: hosted + version: "0.9.0" + stack_trace: + description: stack_trace + source: hosted + version: "0.9.1" + unittest: + description: unittest + source: hosted + version: "0.10.0" + unmodifiable_collection: + description: unmodifiable_collection + source: hosted + version: "0.9.2+1" + utf: + description: utf + source: hosted + version: "0.9.0" diff --git a/demo/animate_demo/pubspec.yaml b/demo/animate_demo/pubspec.yaml new file mode 100644 index 000000000..7256f7122 --- /dev/null +++ b/demo/animate_demo/pubspec.yaml @@ -0,0 +1,7 @@ +name: angular_animate_demo +version: 0.0.1 +dependencies: + angular: + path: ../.. + browser: any + unittest: any diff --git a/demo/animate_demo/web/animate_demo.dart b/demo/animate_demo/web/animate_demo.dart new file mode 100644 index 000000000..b6dd18332 --- /dev/null +++ b/demo/animate_demo/web/animate_demo.dart @@ -0,0 +1,36 @@ +library animate_demo; + +import 'package:angular/angular.dart'; +import 'package:angular/animate/module.dart'; + +// This annotation allows Dart to shake away any classes +// not used from Dart code nor listed in another @MirrorsUsed. +// +// If you create classes that are referenced from the Angular +// expressions, you must include a library target in @MirrorsUsed. +@MirrorsUsed(override: '*') +import 'dart:mirrors'; + +part 'repeat_demo.dart'; +part 'visibility_demo.dart'; +part 'stress_demo.dart'; +part 'css_demo.dart'; + +@NgController( + selector: '[animation-demo]', + publishAs: 'demo' +) +class AnimationDemoController { + var pages = ["About", "ng-repeat", "Visibility", "Css", "Stress Test"]; + var currentPage = "About"; +} + +main() { + ngBootstrap(module: new Module() + ..install(new NgAnimateModule()) + ..type(RepeatDemoComponent) + ..type(VisibilityDemoComponent) + ..type(StressDemoComponent) + ..type(CssDemoComponent) + ..type(AnimationDemoController)); +} diff --git a/demo/animate_demo/web/css_demo.dart b/demo/animate_demo/web/css_demo.dart new file mode 100644 index 000000000..045de466f --- /dev/null +++ b/demo/animate_demo/web/css_demo.dart @@ -0,0 +1,32 @@ +part of animate_demo; + +@NgComponent( + selector: 'css-demo', + template: ''' +
+ + + +
+
BOX
+
+
+ + ''', + publishAs: 'ctrl', + applyAuthorStyles: true +) +class CssDemoComponent { + bool stateA = false; + bool stateB = false; + bool stateC = false; +} \ No newline at end of file diff --git a/demo/animate_demo/web/index.html b/demo/animate_demo/web/index.html new file mode 100644 index 000000000..a775700b6 --- /dev/null +++ b/demo/animate_demo/web/index.html @@ -0,0 +1,49 @@ + + + + NgAnimate | Demos, Stress Tests, Examples and More! + + + + +
+
+

About

+

The NgAnimate module is a port with modifications of the original + AngularJS animation module. The default implementation does nothing. + It simply provides hooks into the angular subsystem. Adding + NgAnimateModule however is a whole different story. Once + added it allows you define and run css animations on your elements with + pure CSS.

+

Check out the demos above.

+
+
+

ng-repeat Demo

+ +
+
+

Visibility Demo

+ +
+
+

Css Demo

+

TODO This should contain a demo of css animation by applying multiple + classes and running multiple simultanious animations on the same + object.

+ +
+
+

Stress Test

+ +
+
+ + + + diff --git a/demo/animate_demo/web/repeat_demo.dart b/demo/animate_demo/web/repeat_demo.dart new file mode 100644 index 000000000..59bf78c7b --- /dev/null +++ b/demo/animate_demo/web/repeat_demo.dart @@ -0,0 +1,32 @@ +part of animate_demo; + +@NgComponent( + selector: 'repeat-demo', + template: ''' +
+ + + +
+ ''', + publishAs: 'ctrl', + applyAuthorStyles: true +) +class RepeatDemoComponent { + var thing = 0; + var items = []; + + addItem() { + items.add("Thing ${thing++}"); + } + + removeItem() { + if (items.length > 0) { + items.removeLast(); + } + } +} diff --git a/demo/animate_demo/web/stress_demo.dart b/demo/animate_demo/web/stress_demo.dart new file mode 100644 index 000000000..7615be93f --- /dev/null +++ b/demo/animate_demo/web/stress_demo.dart @@ -0,0 +1,34 @@ +part of animate_demo; + +@NgComponent(selector: 'stress-demo', template: + ''' +
+ +
+
+
+
+
+ ''', + publishAs: 'ctrl', applyAuthorStyles: true) +class StressDemoComponent { + bool _visible = true; + + // When visibility changes add or remove a large + // chunk of elements. + set visible(bool value) { + if (value) { + for (int i = 0; i < 200; i++) { + numbers.add(i); + } + } else { + numbers.clear(); + } + _visible = value; + } + get visible => _visible; + + List numbers = [1, 2]; +} diff --git a/demo/animate_demo/web/style.css b/demo/animate_demo/web/style.css new file mode 100644 index 000000000..8f54df4fc --- /dev/null +++ b/demo/animate_demo/web/style.css @@ -0,0 +1,292 @@ +[ng-cloak], .ng-cloak, .ng-hide { + display: none !important; +} + +html, body { + font-family: "Open Sans" sans-serif; + font-size: 14px; + color: #fafafa; + background-color: #303030; + margin: 0px; + padding: 0px; + box-shadow: inset 0 10px 10px rgba(0, 0, 0, .1); + width: 100%; + height: 100%; +} + +p { + max-width: 540px; +} + +button { + color: #303030; + background-color: #fafafa; + border: none; + border-radius: 2px; + margin: 5px 5px 5px 0; + height: 30px; + line-height: 30px; + padding: 0 20px; +} + +nav { + padding-left: 20px; +} + +nav button { + margin: 0 5px 5px; +} + +nav .current { + border-top: 5px solid #fafafa; + border-radius: 0 0 2px 2px; + height: 35px; +} + +.content { + margin: 0 30px; +} + +.demo { + position: absolute; + width: 100%; +} + +.demo.ng-enter { + transform: scale(.8, .8); + -webkit-transform: scale(.8, .8); + /* Chrome bug prevents putting opacity at 0 right now */ + opacity: 0.000001; + transition: all 218ms; +} + +.demo.ng-enter.ng-enter-active { + transform: scale(1.0, 1.0); + -webkit-transform: scale(1.0, 1.0); + /* Chrome bug prevents putting opacity at 0 right now */ + opacity: 1.0; + transition: all 218ms; +} + +.demo.ng-leave { + transform: scale(1.0, 1.0); + -webkit-transform: scale(1.0, 1.0); + /* Chrome bug prevents putting opacity at 0 right now */ + opacity: 1; + transition: all 218ms; +} + +.demo.ng-leave.ng-leave-active { + transform: scale(1.3, 1.3); + -webkit-transform: scale(1.3, 1.3); + /* Chrome bug prevents putting opacity at 0 right now */ + opacity: 0.0; + transition: all 218ms; +} + +.repeat-demo ul, .repeat-demo li { + list-style-type: none; + margin: 0; + padding: 0; +} + +.repeat-demo li li { + display: inline-block; + min-width: 30px; + margin: 0 5px 0 0; + padding: 0 5px; + font-size: 10px; + line-height: 15px; + border-radius: 2px; + background: #fafafa; + color: #303030; +} + +.repeat-demo li.ng-enter { + transform: scale(1.1, 1.1); + -webkit-transform: scale(1.1,1.1); + + opacity: 0; + transition: opacity 100ms linear, -webkit-transform 432ms linear; +} + +.repeat-demo li.ng-enter.ng-enter-active { + transform: scale(1.0, 1.0); + -webkit-transform: scale(1.0,1.0); + + opacity: 1; +} + +/* This will show you if one of the sub elements is + animating */ +.repeat-demo li li.ng-enter.ng-enter-active, +.repeat-demo li li.ng-leave.ng-leave-active { + background-color: #1080F0; +} + + +.repeat-demo li.ng-leave { + transform: scale(1.0, 1.0); + -webkit-transform: scale(1.0,1.0); + + opacity: 1; + transition: all 900ms; +} + +.repeat-demo li.ng-leave.ng-leave-active { + transform: scale(1.1, 1.1); + -webkit-transform: scale(1.1,1.1); + + opacity: 0; +} + +.visibility-demo div, +.visibility-demo p { + height: 50px; + margin: 0; + padding: 0; +} + +.visibility-demo div { + border-left: 5px solid #fbfbfb; + padding-left: 10px; +} + +.visibility-demo .ng-enter, +.visibility-demo .ng-hide-remove { + opacity: 0.0001; + height: 0; + transition: all 900ms ease; +} + +.visibility-demo .ng-enter.ng-enter-active, +.visibility-demo .ng-hide-remove.ng-hide-remove-active { + opacity: 1; + height: 50px; +} + +.visibility-demo .ng-leave, +.visibility-demo .ng-hide-add { + opacity: 1; + height: 50px; + transition: all 900ms ease; +} + +.visibility-demo .ng-leave.ng-leave-active, +.visibility-demo .ng-hide-add.ng-hide-add-active { + opacity: 0; + height: 0; +} + +.css-demo .active { + background-color: #1080F0; +} + +.css-demo .css-box { + width: 100px; + height: 100px; + background-color: #fbfbfb; + color: #303030; + margin: 100px auto; + text-align: center; + line-height: 100px; + font-weight: bold; +} + + +.css-demo .css-box.a { + width: 200px; +} +.css-demo .css-box.a-add { + transition: width 1200ms; +} +.css-demo .css-box.a-add.a-add-active { + width: 200px; +} +.css-demo .css-box.a-remove { + transition: width 1200ms; +} +.css-demo .css-box.a-remove.a-remove-active { + width: 100px; +} + +.css-demo .css-box.b { + background-color: #1080F0; +} +.css-demo .css-box.b-add { + background-color: #fbfbfb; + transition: all 500ms; +} +.css-demo .css-box.b-add.b-add-active { + background-color: #1080F0; +} +.css-demo .css-box.b-remove { + background-color: #1080F0; + transition: all 500ms; +} +.css-demo .css-box.b-remove.b-remove-active { + background-color: #fbfbfb; +} + +.css-demo .css-box.c { + transform: rotate(45deg); + -webkit-transform: rotate(45deg); +} +.css-demo .css-box.c-add { + transform: rotate(0deg); + -webkit-transform: rotate(0deg); + + transition: all 200ms; +} +.css-demo .css-box.c-add.c-add-active { + transform: rotate(45deg); + -webkit-transform: rotate(45deg); +} +.css-demo .css-box.c-remove { + transform: rotate(45deg); + -webkit-transform: rotate(45deg); + + transition: all 200ms; +} +.css-demo .css-box.c-remove.c-remove-active { + transform: rotate(0deg); + -webkit-transform: rotate(0deg); +} + +.stress-box { + display: inline-block; + width: 20px; + height: 20px; + margin: 5px 5px 0 0; + padding: 0; + background-color: #fbfbfb; + border-radius: 2px; +} + +.stress-box.ng-enter { + transform: scale(0, 0); + -webkit-transform: scale(0,0); + opacity: 0.000001; + + transition: all 500ms; +} + +.stress-box.ng-enter.ng-enter-active { + transform: scale(1, 1); + -webkit-transform: scale(1,1); + opacity: 1.0; +} + +.stress-box.ng-leave { + transform: scale(1, 1); + -webkit-transform: scale(1,1); + opacity: 1.0; + + transition: all 500ms; +} + +.stress-box.ng-leave.ng-leave-active { + transform: scale(0, 0); + -webkit-transform: scale(0,0); + opacity: 0.000001; +} \ No newline at end of file diff --git a/demo/animate_demo/web/visibility_demo.dart b/demo/animate_demo/web/visibility_demo.dart new file mode 100644 index 000000000..caff7d507 --- /dev/null +++ b/demo/animate_demo/web/visibility_demo.dart @@ -0,0 +1,26 @@ +part of animate_demo; + +@NgComponent( + selector: 'visibility-demo', + template: ''' +
+ +
+

Hello World. ng-if will create and destroy + dom elements each time you toggle me.

+
+
+

Hello World. ng-hide will add and remove + the .ng-hide class from me to show and + hide this block of text.

+
+
+ ''', + publishAs: 'ctrl', + applyAuthorStyles: true +) +class VisibilityDemoComponent { + // TODO(codelogic): split and add ng-switch. + bool visible = false; +} \ No newline at end of file diff --git a/lib/angular.dart b/lib/angular.dart index a663a77d5..3d7c69312 100644 --- a/lib/angular.dart +++ b/lib/angular.dart @@ -29,6 +29,7 @@ import 'package:di/dynamic_injector.dart'; */ @MirrorsUsed(targets: const [ 'angular', + 'angular.animate', 'angular.core', 'angular.core.dom', 'angular.filter', @@ -51,6 +52,7 @@ metaTargets: const [ ]) import 'dart:mirrors' show MirrorsUsed; +import 'package:angular/animate/module.dart'; import 'package:angular/core/module.dart'; import 'package:angular/core_dom/module.dart'; import 'package:angular/directive/module.dart'; diff --git a/lib/animate/animation_loop.dart b/lib/animate/animation_loop.dart new file mode 100644 index 000000000..0d8a2fc41 --- /dev/null +++ b/lib/animate/animation_loop.dart @@ -0,0 +1,108 @@ +part of angular.animate; + +/** + * Window.animationFrame update loop that tracks and drives + * [LoopedAnimations]'s. + */ +class AnimationLoop { + final AnimationFrame _frames; + final Profiler _profiler; + final List _animations = []; + final NgZone _zone; + + bool _animationFrameQueued = false; + + /** + * The animation runner requires an [AnimationFrame] to drive the animation + * frames, and profiler will report timing information for each of the + * animation frames. + */ + AnimationLoop(this._frames, this._profiler, this._zone); + + /** + * Start and play an animation through the state transitions defined in + * [Animation]. + */ + void play(LoopedAnimation animation) { + _animations.add(animation); + _queueAnimationFrame(); + } + + void _queueAnimationFrame() { + if (!_animationFrameQueued) { + _animationFrameQueued = true; + + // TODO(codleogic): This should run outside of an angular scope digest. + _zone.runOutsideAngular(() { + _frames.animationFrame.then((timeInMs) => _animationFrame(timeInMs)) + .catchError((error) => print(error)); + }); + } + } + + /* On the browsers animation frame event, update each of the tracked + * animations. Group dom reads first, and and writes second. + * + * At any point any animation may be updated by calling interrupt and cancel + * with a reference to the [Animation] to cancel. The [AnimationRunner] will + * then forget about the [Animation] and will not call any further methods on + * the [Animation]. + */ + void _animationFrame(num timeInMs) { + _profiler.startTimer("AnimationRunner.AnimationFrame"); + _animationFrameQueued = false; + + _profiler.startTimer("AnimationRunner.AnimationFrame.DomReads"); + // Dom reads + _read(timeInMs); + _profiler.stopTimer("AnimationRunner.AnimationFrame.DomReads"); + + _profiler.startTimer("AnimationRunner.AnimationFrame.DomMutates"); + // Dom mutates + _update(timeInMs); + _profiler.stopTimer("AnimationRunner.AnimationFrame.DomMutates"); + + // We don't need to continue queuing animation frames + // if there are no more animations to process. + if (_animations.length > 0) { + _queueAnimationFrame(); + } + + _profiler.stopTimer("AnimationRunner.AnimationFrame"); + } + + void _update(num timeInMs) { + for (int i=0; i< _animations.length; i++) { + var controller = _animations[i]; + if (!controller.update(timeInMs)) { + _animations.removeAt(i--); + } + } + } + + void _read(num timeInMs) { + for (int i=0; i< _animations.length; i++) { + var animation = _animations[i]; + animation.read(timeInMs); + } + } + + /** + * Stop tracking and updating the [animation]. + */ + void forget(LoopedAnimation animation) { + assert(animation != null); + _animations.remove(animation); + } +} + +/** + * Wrapper around window.requestAnimationFrame so it can be intercepted and + * tested. + */ +class AnimationFrame { + final dom.Window _wnd; + Future get animationFrame => _wnd.animationFrame; + + AnimationFrame(this._wnd); +} diff --git a/lib/animate/animation_optimizer.dart b/lib/animate/animation_optimizer.dart new file mode 100644 index 000000000..7bf6faeaa --- /dev/null +++ b/lib/animate/animation_optimizer.dart @@ -0,0 +1,104 @@ +part of angular.animate; + +/** + * The optimizer tracks elements and running animations. It's used to control + * and optionally skip certain animations that are deemed "expensive" such as + * running animations on child elements while the dom parent is also running an + * animation. + */ +class AnimationOptimizer { + final Map> _elements = new Map>(); + final Map _animations = new Map(); + + Expando _expando; + + AnimationOptimizer(this._expando); + + /** + * Track an animation that is running against a dom element. Usually, this + * should occur when an animation starts. + */ + void track(Animation animation, dom.Element forElement) { + if (forElement != null) { + var animations = _elements.putIfAbsent(forElement, () => + new Set()); + animations.add(animation); + _animations[animation] = forElement; + } + } + + /** + * Stop tracking an animation. If it's the last tracked animation on an + * element forget about that element as well. + */ + void forget(Animation animation) { + var element = _animations.remove(animation); + if (element != null) { + var animationsOnElement = _elements[element]; + animationsOnElement.remove(animation); + // It may be more efficient just to keep sets around even after + // animations complete. + if (animationsOnElement.length == 0) { + _elements.remove(element); + } + } + } + + // TODO(codelogic): Allow animations to be forcibly prevented from executing + // on certain elements, elements and children, and forcibly allowed (ignoring + // parent animation state); + + /** + * Returns true if there is tracked animation on the given element. + */ + bool isAnimating(dom.Element element) { + return _elements.containsKey(element); + } + + /** + * Given all the information this optimizer knows about currently executing + * animations, return [true] if this element can be animated in an ideal case + * and [false] if the optimizer thinks that it should not execute. + */ + bool shouldAnimate(dom.Node node) { + //var probe = _findElementProbe(node.parentNode); + var source = node; + node = node.parentNode; + while (node != null) { + if (node.nodeType == dom.Node.ELEMENT_NODE + && isAnimating(node)) { + // If there is an already running animation, don't animate. + return false; + } + + // If we hit a null parent, try to break out of shadow dom. + if(node.parentNode == null) { + var probe = _findElementProbe(node); + if (probe != null && probe.parent != null) { + // Escape shadow dom. + node = probe.parent.element; + } else { + // If we are at the root of the document, we can animate. + return true; + } + } else { + node = node.parentNode; + } + } + + return true; + } + + // Search and find the element probe for a given node. + ElementProbe _findElementProbe(dom.Node node) { + while (node != null) { + if (_expando[node] != null) { + return _expando[node]; + } + node = node.parentNode; + } + return null; + } +} diff --git a/lib/animate/animations.dart b/lib/animate/animations.dart new file mode 100644 index 000000000..9d5e27404 --- /dev/null +++ b/lib/animate/animations.dart @@ -0,0 +1,100 @@ +part of angular.animate; + +/** + * A [LoopedAnimation] is used with the [AnimationLoop] to drive window + * animation frame animations. This provides hooks for dom reads and updates so + * that they can be batched together to prevent excessive dom recalculations + * when running multiple animations. + */ +abstract class LoopedAnimation implements Animation { + + /** + * This is used to batch dom read operations to prevent excessive + * recalculations when dom is modified. + * + * [timeInMs] is the time since the last animation frame. + */ + void read(num timeInMs) { } + + /** + * Occurs every animation frame. Return false to stop receiving animation + * frame updates. Detach will be called after [update] returns false. + * + * [timeInMs] is the time since the last animation frame. + */ + bool update(num timeInMs) { return false; } +} + +/** + * This is a proxy class for dealing with a set of elements where the 'same' + * or similar animations are being run on them and it's more convenient to have + * a merged animation to control and listen to a set of animations. + */ +class AnimationList extends Animation { + final List _animations; + Future _onCompleted; + + /** + * [OnCompleted] executes once all the OnCompleted futures for each of the + * animations completes. + * + * if every animation returns [AnimationResult.COMPLETED], + * [AnimationResult.COMPLETED] will be returned. + * if any animation was [AnimationResult.COMPLETED_IGNORED] instead, even if + * some animations were completed, [AnimationResult.COMPLETED_IGNORED] will + * be returned. + * if any animation was [AnimationResult.CANCELED], the result will be + * [AnimationResult.CANCELED]. + */ + Future get onCompleted { + if (_onCompleted == null) { + _onCompleted = Future.wait(_animations.map((x) => x.onCompleted)) + .then((results) { + var rtrn = AnimationResult.COMPLETED; + for (var result in results) { + if (result == AnimationResult.CANCELED) + return AnimationResult.CANCELED; + if (result == AnimationResult.COMPLETED_IGNORED) + rtrn = result; + } + return rtrn; + }); + } + + return _onCompleted; + } + + /// track and create a new [Animation] that acts as a proxy to a list of + /// existing [Animation]s. + AnimationList(this._animations); + + /// For each of the tracked [Animation]s, call complete(). + void complete() { + for (var animation in _animations) { + animation.complete(); + } + } + + /// For each of the tracked [Animation]s, call cancel(). + void cancel() { + for (var animation in _animations) { + animation.cancel(); + } + } +} + +Animation _animationFromList(Iterable animations) { + if (animations == null) { + return new NoOpAnimation(); + } + + List list = animations.toList(); + + if (list.length == 0) { + return new NoOpAnimation(); + } + if (list.length == 1) { + return list.first; + } + return new AnimationList(list); +} diff --git a/lib/animate/css_animate.dart b/lib/animate/css_animate.dart new file mode 100644 index 000000000..524c8de57 --- /dev/null +++ b/lib/animate/css_animate.dart @@ -0,0 +1,166 @@ +part of angular.animate; + +/** + * This defines the standard set of CSS animation classes, transitions, and + * nomenclature that will eventually be the foundation of the AngularDart + * animation framework. This implementation uses the [AnimationLoop] class to + * queue and run CSS based transition and keyframe animations. + */ +class CssAnimate implements NgAnimate { + static const String NG_ANIMATE = "ng-animate"; + static const String NG_MOVE = "ng-move"; + static const String NG_INSERT = "ng-enter"; + static const String NG_REMOVE = "ng-leave"; + + static const String NG_ADD_POSTFIX = "-add"; + static const String NG_REMOVE_POSTFIX = "-remove"; + static const String NG_ACTIVE_POSTFIX = "-active"; + + final NoOpAnimation _noOp = new NoOpAnimation(); + + final AnimationLoop _runner; + final AnimationOptimizer _optimizer; + final CssAnimationMap _animationMap; + + CssAnimate(this._runner, this._animationMap, this._optimizer); + + Animation addClass(dom.Element element, String cssClass) { + if (!_optimizer.shouldAnimate(element)) { + element.classes.add(cssClass); + return _noOp; + } + + cancelAnimation(element, "$cssClass$NG_REMOVE_POSTFIX"); + var event = "$cssClass$NG_ADD_POSTFIX"; + return animate(element, event, addAtEnd: cssClass); + } + + Animation removeClass(dom.Element element, String cssClass) { + if (!_optimizer.shouldAnimate(element)) { + element.classes.remove(cssClass); + return _noOp; + } + + cancelAnimation(element, "$cssClass$NG_ADD_POSTFIX"); + + var event = "$cssClass$NG_REMOVE_POSTFIX"; + return animate(element, event, removeAtEnd: cssClass); + } + + Animation insert(Iterable nodes, dom.Node parent, + { dom.Node insertBefore }) { + util.domInsert(nodes, parent, insertBefore: insertBefore); + + var animations = util.getElements(nodes).where((el) { + return _optimizer.shouldAnimate(el); + }).map((el) { + return animate(el, NG_INSERT); + }); + + return _animationFromList(animations); + } + + Animation remove(Iterable nodes) { + var animations = nodes.map((node) { + if (node.nodeType == dom.Node.ELEMENT_NODE + && _optimizer.shouldAnimate(node)) { + return animate(node, NG_REMOVE); + } + return _noOp; + }); + + var result = _animationFromList(animations)..onCompleted.then((result) { + if (result.isCompleted) { + nodes.toList().forEach((n) => n.remove()); + } + }); + + return result; + } + + Animation move(Iterable nodes, dom.Node parent, + { dom.Node insertBefore }) { + util.domMove(nodes, parent, insertBefore: insertBefore); + + var animations = util.getElements(nodes).where((el) { + return _optimizer.shouldAnimate(el); + }).map((el) { + return animate(el, NG_MOVE); + }); + + return _animationFromList(animations); + } + + /** + * Run a css animation on a element for a given css class. If the css + * animation already exists, the method will attempt to return the existing + * instance. + */ + CssAnimation animate( + dom.Element element, + String event, + { String addAtStart, + String addAtEnd, + String removeAtStart, + String removeAtEnd }) { + + var _existing = _animationMap.findExisting(element, event); + if (_existing != null) { + return _existing; + } + + var animation = new CssAnimation( + element, + event, + "$event$NG_ACTIVE_POSTFIX", + addAtStart: addAtStart, + addAtEnd: addAtEnd, + removeAtStart: removeAtStart, + removeAtEnd: removeAtEnd, + animationMap: _animationMap, + optimizer: _optimizer); + + _runner.play(animation); + return animation; + } + + /** + * For a given element and css event, attempt to find an existing instance + * of the given animation and cancel it. + */ + void cancelAnimation(dom.Element element, String event) { + var existing = _animationMap.findExisting(element, event); + + if (existing != null) { + existing.cancel(); + } + } +} + +/** + * Tracked set of currently running css animations grouped by element. + */ +class CssAnimationMap { + final Map> cssAnimations + = new Map>(); + + void track(CssAnimation animation) { + var animations = cssAnimations.putIfAbsent(animation.element, () + => new Map()); + animations[animation.eventClass] = animation; + } + + void forget(CssAnimation animation) { + var animations = cssAnimations[animation.element]; + animations.remove(animation.eventClass); + if (animations.length == 0) { + cssAnimations.remove(animation.element); + } + } + + CssAnimation findExisting(dom.Element element, String event) { + var animations = cssAnimations[element]; + if (animations == null) return null; + return animations[event]; + } +} diff --git a/lib/animate/css_animation.dart b/lib/animate/css_animation.dart new file mode 100644 index 000000000..fc96d91d9 --- /dev/null +++ b/lib/animate/css_animation.dart @@ -0,0 +1,159 @@ +part of angular.animate; + +/** + * [Animation] implementation for handling the standard angular 'event' and + * 'event-active' class pattern with css. This will compute transition and + * animation duration from the css classes and use it to complete futures when + * the css animations complete. + */ +class CssAnimation extends LoopedAnimation { + final CssAnimationMap _animationMap; + final AnimationOptimizer _optimizer; + + final dom.Element element; + final String addAtStart; + final String addAtEnd; + final String removeAtStart; + final String removeAtEnd; + + final String eventClass; + final String activeClass; + + final Completer _completer + = new Completer.sync(); + + static const num extraDuration = 16.0; // Just a little extra time + + bool _active = true; + bool _started = false; + bool _isDisplayNone = false; + + Future get onCompleted => _completer.future; + + num _startTime; + num _duration; + + CssAnimation( + this.element, + this.eventClass, + this.activeClass, + { this.addAtStart, + this.removeAtStart, + this.addAtEnd, + this.removeAtEnd, + CssAnimationMap animationMap, + AnimationOptimizer optimizer }) + : _animationMap = animationMap, + _optimizer = optimizer + { + if (_optimizer != null) { + _optimizer.track(this, element); + } + if (_animationMap != null) { + _animationMap.track(this); + } + element.classes.add(eventClass); + if (addAtStart != null) { + element.classes.add(addAtStart); + } + if (removeAtStart != null) { + element.classes.remove(removeAtStart); + } + } + + void read(num timeInMs) { + if (_active && _startTime == null) { + _startTime = timeInMs; + var style = element.getComputedStyle(); + _isDisplayNone = style.display == "none"; + _duration = util.computeLongestTransition(style); + if (_duration > 0.0) { + // Add a little extra time just to make sure transitions + // fully complete and that we don't remove the animation classes + // before it's completed. + _duration = _duration + extraDuration; + } + } + } + + bool update(num timeInMs) { + if (!_active) { + return false; + } + + if (timeInMs >= _startTime + _duration) { + _complete(AnimationResult.COMPLETED); + // TODO(codelogic): If the initial frame takes a significant amount of + // time, the computed duration + startTime might not actually represent + // the end of the animation + // Done with the animation + return false; + } else if (!_started) { + // This will always run after the first animationFrame is queued so that + // inserted elements have the base event class applied before adding the + // active class to the element. If this is not done, inserted dom nodes + // will not run their enter animation. + + if (_isDisplayNone && removeAtEnd != null) { + element.classes.remove(removeAtEnd); + } + + element.classes.add(activeClass); + _started = true; + } + + // Continue updating + return true; + } + + void cancel() { + if (_active) { + _detach(); + if (addAtStart != null) { + + element.classes.remove(addAtStart); + } + if (removeAtStart != null) { + element.classes.add(removeAtStart); + } + + if (_completer != null) { + _completer.complete(AnimationResult.CANCELED); + } + } + } + + void complete() { + _complete(AnimationResult.COMPLETED_IGNORED); + } + + // Since there are two different ways to 'complete' an animation, this lets us + // configure the final result. + void _complete(AnimationResult result) { + if (_active) { + _detach(); + if (addAtEnd != null) { + element.classes.add(addAtEnd); + } + if (removeAtEnd != null) { + element.classes.remove(removeAtEnd); + } + _completer.complete(result); + } + } + + // Cleanup css event classes. + void _detach() { + _active = false; + + if (_animationMap != null) { + _animationMap.forget(this); + } + if (_optimizer != null) { + _optimizer.forget(this); + } + + element.classes.remove(eventClass); + element.classes.remove(activeClass); + } +} diff --git a/lib/animate/module.dart b/lib/animate/module.dart new file mode 100644 index 000000000..325c78045 --- /dev/null +++ b/lib/animate/module.dart @@ -0,0 +1,59 @@ +library angular.animate; + +import 'dart:async'; +import 'dart:html' as dom; + +import 'package:angular/core/module.dart'; +import 'package:angular/core_dom/module.dart'; +import 'package:angular/core_dom/dom_util.dart' as util; +import 'package:logging/logging.dart'; +import 'package:perf_api/perf_api.dart'; +import 'package:di/di.dart'; + +part 'animations.dart'; +part 'animation_loop.dart'; +part 'animation_optimizer.dart'; +part 'css_animate.dart'; +part 'css_animation.dart'; + +final Logger _logger = new Logger('ng.animate'); + +/** + * Installing the NgAnimateModule will install a [CssAnimate] implementation of + * the [NgAnimate] interface in your application. This will change the behavior + * of block construction, and some of the native directives to allow you to add + * and define css transition and keyframe animations for the styles of your + * elements. + * + * Example html: + * + *
...
+ * + * Example css defining an opacity transition over .5 seconds using the + * `.ng-enter` and `.ng-leave` css classes: + * + * .my-div.ng-enter { + * transition: all 500ms; + * opacity: 0; + * } + * .my-div.ng-enter-active { + * opacity: 1; + * } + * + * .my-div.ng-leave { + * transition: all 500ms; + * opacity: 1; + * } + * .my-div.ng-leave-active { + * opacity: 0; + * } + */ +class NgAnimateModule extends Module { + NgAnimateModule() { + type(AnimationFrame); + type(AnimationLoop); + type(CssAnimationMap); + type(AnimationOptimizer); + type(NgAnimate, implementedBy: CssAnimate); + } +} \ No newline at end of file diff --git a/lib/core/zone.dart b/lib/core/zone.dart index 600b775aa..6d5eb3e39 100644 --- a/lib/core/zone.dart +++ b/lib/core/zone.dart @@ -144,7 +144,7 @@ class NgZone { * * Returns the return value of body. */ - run(body()) => _zone.run(body); + dynamic run(body()) => _zone.run(body); /** * Allows one to escape the auto-digest mechanism of Angular. @@ -160,13 +160,13 @@ class NgZone { * }); * } */ - runOutsideAngular(body()) => _outerZone.run(body); + dynamic runOutsideAngular(body()) => _outerZone.run(body); - assertInTurn() { + void assertInTurn() { assert(_runningInTurn > 0 || _inFinishTurn); } - assertInZone() { + void assertInZone() { assertInTurn(); } } diff --git a/lib/core_dom/animation.dart b/lib/core_dom/animation.dart new file mode 100644 index 000000000..45c19936a --- /dev/null +++ b/lib/core_dom/animation.dart @@ -0,0 +1,138 @@ +part of angular.core.dom; + +/** + * The [NgAnimate] service provides dom lifecycle mangement, detection and + * analysis of css animations, and hooks for custom animations. When any of + * these animations are run, [Animation]s are returned so the animation can be + * controled and so that custom dom manipulations can occur when animations + * complete. + */ +class NgAnimate { + /** + * Add the [cssClass] to the classes on [element] after running any + * defined animations. + */ + Animation addClass(dom.Element element, String cssClass) { + element.classes.add(cssClass); + return new NoOpAnimation(); + } + + /** + * Remove the [cssClass] from the classes on [element] after running any + * defined animations. + */ + Animation removeClass(dom.Element element, String cssClass) { + element.classes.remove(cssClass); + return new NoOpAnimation(); + } + + /** + * Perform an 'enter' animation for each element in [nodes]. The elements + * must exist in the dom. This is equivalent to running enter on each element + * in [nodes] and returning Future.wait(handles); for the onCompleted + * property on [Animation]. + */ + Animation insert(Iterable nodes, dom.Node parent, + { dom.Node insertBefore }) { + util.domInsert(nodes, parent, insertBefore: insertBefore); + return new NoOpAnimation(); + } + + /** + * Perform a 'remove' animation for each element in [nodes]. The elements + * must exist in the dom and should not be detached until the [onCompleted] + * future on the [Animation] is executed AND the [AnimationResult] is + * [AnimationResult.COMPLETED] or [AnimationResult.COMPLETED_IGNORED]. + * + * This is equivalent to running remove on each element in [nodes] and + * returning Future.wait(handles); for the onCompleted property on + * [Animation]. + */ + Animation remove(Iterable nodes) { + util.domRemove(nodes.toList(growable: false)); + return new NoOpAnimation(); + } + + /** + * Perform a 'move' animation for each element in [nodes]. The elements + * must exist in the dom. This is equivalent to running move on each element + * in [nodes] and returning Future.wait(handles); for the onCompleted + * property on [Animation]. + */ + Animation move(Iterable nodes, dom.Node parent, + { dom.Node insertBefore }) { + util.domMove(nodes, parent, insertBefore: insertBefore); + return new NoOpAnimation(); + } +} + + +/** + * Animation handle for controlling and listening to animation completion. + */ +abstract class Animation { + /** + * Executed once when the animation is completed with the type of completion + * result. + */ + async.Future get onCompleted; + + /** + * Stop and complete the animation immediatly. This has no effect if the + * animation has already completed. + * + * The onCompleted future will be executed if the animation has not been + * completed. + */ + void complete(); + + /** + * Stop and cancel the animation immediatly. This has no effect if the + * animation has already completed. + * + * The onCompleted future will be executed if the animation has not been + * completed. + */ + void cancel(); +} + +/** + * Completed animation handle that is used when an animation is ignored and the + * final effect of the animation is immediatly completed. + * + * TODO(codelogic): consider making a singleton instance. Depends on how future + * behaves. + */ +class NoOpAnimation extends Animation { + async.Future _future; + get onCompleted { + if (_future == null) { + _future = new async.Future.value(AnimationResult.COMPLETED_IGNORED); + } + return _future; + } + + complete() { } + cancel() { } +} + +/** + * Final result of an animation after it is no longer attached to the element. + */ +class AnimationResult { + /// Animation was run (if it exists) and completed successfully. + static const COMPLETED = const AnimationResult._('COMPLETED'); + + /// Animation was skipped, but should be continued. + static const COMPLETED_IGNORED = const AnimationResult._('COMPLETED_IGNORED'); + + /// A [CANCELED] animation should not procced with it's final effects. + static const CANCELED = const AnimationResult._('CANCELED'); + + /// Convienence method if you don't care exactly how an animation completed + /// only that it did. + bool get isCompleted => this == COMPLETED || this == COMPLETED_IGNORED; + + final String value; + const AnimationResult._(this.value); +} \ No newline at end of file diff --git a/lib/core_dom/block.dart b/lib/core_dom/block.dart index 45018f0ba..3f2e15dab 100644 --- a/lib/core_dom/block.dart +++ b/lib/core_dom/block.dart @@ -36,8 +36,9 @@ class Block implements ElementWrapper { Function onMove; List _directives = []; + final NgAnimate _animate; - Block(this.elements); + Block(this.elements, this._animate); Block insertAfter(ElementWrapper previousBlock) { // Update Link List. @@ -55,8 +56,9 @@ class Block implements ElementWrapper { dom.Node parentElement = previousElement.parentNode; bool preventDefault = false; - Function insertDomElements = () => - elements.forEach((el) => parentElement.insertBefore(el, insertBeforeElement)); + Function insertDomElements = () { + _animate.insert(elements, parentElement, insertBefore: insertBeforeElement); + }; if (onInsert != null) { onInsert({ @@ -78,22 +80,15 @@ class Block implements ElementWrapper { bool preventDefault = false; Function removeDomElements = () { - for (var j = 0; j < elements.length; j++) { - dom.Node current = elements[j]; - dom.Node next = j+1 < elements.length ? elements[j+1] : null; - - while(next != null && current.nextNode != next) { - current.nextNode.remove(); - } - elements[j].remove(); - } + _animate.remove(elements); }; if (onRemove != null) { onRemove({ "preventDefault": () { preventDefault = true; - return removeDomElements(); + removeDomElements(); + return this; }, "element": elements[0] }); @@ -116,7 +111,7 @@ class Block implements ElementWrapper { previousElement = previousElements[previousElements.length - 1], insertBeforeElement = previousElement.nextNode, parentElement = previousElement.parentNode; - + elements.forEach((el) => parentElement.insertBefore(el, insertBeforeElement)); // Remove block from list diff --git a/lib/core_dom/block_factory.dart b/lib/core_dom/block_factory.dart index a58102be3..260d1e685 100644 --- a/lib/core_dom/block_factory.dart +++ b/lib/core_dom/block_factory.dart @@ -43,7 +43,7 @@ class BlockFactory { var timerId; try { assert((timerId = _perf.startTimer('ng.block')) != false); - var block = new Block(elements); + var block = new Block(elements, injector.get(NgAnimate)); _link(block, elements, directivePositions, injector); return block; } finally { diff --git a/lib/core_dom/dom_util.dart b/lib/core_dom/dom_util.dart new file mode 100644 index 000000000..8e744c10e --- /dev/null +++ b/lib/core_dom/dom_util.dart @@ -0,0 +1,131 @@ +library angular.dom.util; + +import 'dart:html' as dom; + +Iterable getElements(Iterable nodes) { + return nodes.where((el) => el.nodeType == dom.Node.ELEMENT_NODE); +} + +void domRemove(List nodes) { + // Not every element is sequential if the list of nodes only + // includes the elements. Removing a block also includes + // removing non-element nodes inbetween. + for (var j = 0; j < nodes.length; j++) { + dom.Node current = nodes[j]; + dom.Node next = j+1 < nodes.length ? nodes[j+1] : null; + + while(next != null && current.nextNode != next) { + current.nextNode.remove(); + } + nodes[j].remove(); + } +} + +void domInsert(Iterable nodes, dom.Node parent, + { dom.Node insertBefore }) { + parent.insertAllBefore(nodes, insertBefore); +} + +void domMove(Iterable nodes, dom.Node parent, + { dom.Node insertBefore }) { + nodes.forEach((n) { + if (n.parentNode == null) n.remove(); + parent.insertBefore(n, insertBefore); + }); +} + +List allNodesBetween(List nodes) { + var result = []; + // Not every element is sequential if the list of nodes only + // includes the elements. Removing a block also includes + // removing non-element nodes inbetween. + for (var j = 0, jj = nodes.length; j < jj; j++) { + dom.Node current = nodes[j]; + dom.Node next = j+1 < jj ? nodes[j+1] : null; + + while (next != null && current.nextNode != next) { + result.add(current.nextNode); + current = current.nextNode; + } + result.add(nodes[j]); + } + return result; +} + +/** + * Computes and returns the longest css transition or keyframe animation from + * a computed style in milliseconds. + */ +num computeLongestTransition(dynamic style) { + double longestTransitionSeconds = 0.0; + + if (style.transitionDuration.length > 0) { + // Parse transitions + List durations = _parseDurationList(style.transitionDuration) + .toList(); + List delays = _parseDurationList(style.transitionDelay) + .toList(); + + assert(durations.length == delays.length); + + for (int i = 0; i < durations.length; i++) { + var total = _computeTotalDurationSeconds(delays[i], durations[i]); + if (total > longestTransitionSeconds) + longestTransitionSeconds = total; + } + } + + if (style.animationDuration.length > 0) { + // Parse and add animation duration properties. + List animationDurations = + _parseDurationList(style.animationDuration).toList(growable: false); + // Note that animation iteration count only affects duration NOT delay. + List animationDelays = + _parseDurationList(style.animationDelay).toList(growable: false); + + List iterationCounts = _parseIterationCounts( + style.animationIterationCount).toList(growable: false); + + assert(animationDurations.length == animationDelays.length); + + for (int i = 0; i < animationDurations.length; i++) { + var total = _computeTotalDurationSeconds( + animationDelays[i], animationDurations[i], + iterations: iterationCounts[i]); + if (total > longestTransitionSeconds) + longestTransitionSeconds = total; + } + } + + // transition time in milliseconds. + return longestTransitionSeconds * 1000; +} + +Iterable _parseIterationCounts(String iterationCounts) { + return iterationCounts.split(", ") + .map((x) => x == "infinite" ? -1 : num.parse(x)); +} + +/// This expects a string in the form "0s, 3.234s, 10s" and will return a list +/// of doubles of (0, 3.234, 10). +Iterable _parseDurationList(String durations) { + // Substring removes the 's' from the end. + return durations.split(", ") + .map((x) => _parseCssDuration(x)); +} + +/// This expects a string in the form of '0.234s' or '4s' and will return +/// a parsed double. +num _parseCssDuration(String cssDuration) { + return double.parse(cssDuration.substring(0, cssDuration.length - 1)); +} + +num _computeTotalDurationSeconds(num delay, num duration, + { int iterations: 1}) { + if (iterations == 0) + return 0.0; + if (iterations < 0) // infinite + iterations = 1; + + return (duration * iterations) + delay; +} \ No newline at end of file diff --git a/lib/core_dom/module.dart b/lib/core_dom/module.dart index 34641b3cd..35161a29d 100644 --- a/lib/core_dom/module.dart +++ b/lib/core_dom/module.dart @@ -10,7 +10,9 @@ import 'package:perf_api/perf_api.dart'; import 'package:angular/core/module.dart'; import 'package:angular/core/parser/parser.dart'; +import 'package:angular/core_dom/dom_util.dart' as util; +part 'animation.dart'; part 'block.dart'; part 'block_factory.dart'; part 'cookies.dart'; @@ -43,6 +45,7 @@ class NgCoreDomModule extends Module { type(HttpDefaultHeaders); type(HttpDefaults); type(HttpInterceptors); + type(NgAnimate); type(BlockCache); type(BrowserCookies); type(Cookies); diff --git a/lib/directive/ng_class.dart b/lib/directive/ng_class.dart index 6c77f7302..3491bae01 100644 --- a/lib/directive/ng_class.dart +++ b/lib/directive/ng_class.dart @@ -62,13 +62,13 @@ part of angular.directive; * } * */ -@NgDirective( - selector: '[ng-class]', - map: const {'ng-class': '@valueExpression'}, - exportExpressionAttrs: const ['ng-class']) +@NgDirective(selector: '[ng-class]', map: const { + 'ng-class': '@valueExpression' +}, exportExpressionAttrs: const ['ng-class']) class NgClassDirective extends _NgClassBase { - NgClassDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) - : super(element, scope, null, attrs, parser); + NgClassDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser + parser, NgAnimate animate) + : super(element, scope, null, attrs, parser, animate); } /** @@ -97,13 +97,13 @@ class NgClassDirective extends _NgClassBase { * color: blue; * } */ -@NgDirective( - selector: '[ng-class-odd]', - map: const {'ng-class-odd': '@valueExpression'}, - exportExpressionAttrs: const ['ng-class-odd']) +@NgDirective(selector: '[ng-class-odd]', map: const { + 'ng-class-odd': '@valueExpression' +}, exportExpressionAttrs: const ['ng-class-odd']) class NgClassOddDirective extends _NgClassBase { - NgClassOddDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) - : super(element, scope, 0, attrs, parser); + NgClassOddDirective(dom.Element element, Scope scope, NodeAttrs + attrs, AstParser parser, NgAnimate animate) + : super(element, scope, 0, attrs, parser, animate); } /** @@ -132,13 +132,13 @@ class NgClassOddDirective extends _NgClassBase { * color: blue; * } */ -@NgDirective( - selector: '[ng-class-even]', - map: const {'ng-class-even': '@valueExpression'}, - exportExpressionAttrs: const ['ng-class-even']) +@NgDirective(selector: '[ng-class-even]', map: const { + 'ng-class-even': '@valueExpression' +}, exportExpressionAttrs: const ['ng-class-even']) class NgClassEvenDirective extends _NgClassBase { - NgClassEvenDirective(dom.Element element, Scope scope, NodeAttrs attrs, AstParser parser) - : super(element, scope, 1, attrs, parser); + NgClassEvenDirective(dom.Element element, Scope scope, NodeAttrs + attrs, AstParser parser, NgAnimate animate) + : super(element, scope, 1, attrs, parser, animate); } abstract class _NgClassBase { @@ -147,10 +147,13 @@ abstract class _NgClassBase { final int mode; final NodeAttrs nodeAttrs; final AstParser _parser; + final NgAnimate _animate; var previousSet = []; var currentSet = []; - _NgClassBase(this.element, this.scope, this.mode, this.nodeAttrs, this._parser) { + + _NgClassBase(this.element, this.scope, this.mode, this.nodeAttrs, this._parser, this._animate) + { var prevClass; nodeAttrs.observe('class', (String newValue) { @@ -164,22 +167,20 @@ abstract class _NgClassBase { set valueExpression(currentExpression) { // this should be called only once, so we don't worry about cleaning up // watcher registrations. - scope.watch( - _parser(currentExpression, collection: true), - (current, _) { - currentSet = _flatten(current); - _handleChange(scope.context[r'$index']); - }, - readOnly: true - ); + scope.watch(_parser(currentExpression, collection: true), (current, _) { + currentSet = _flatten(current); + _handleChange(scope.context[r'$index']); + }, readOnly: true); if (mode != null) { scope.watch(_parser(r'$index'), (index, oldIndex) { var mod = index % 2; if (oldIndex == null || mod != oldIndex % 2) { if (mod == mode) { - element.classes.addAll(currentSet); + currentSet.forEach((css) => _animate.addClass(element, css)); + //element.classes.addAll(currentSet); } else { - element.classes.removeAll(previousSet); + previousSet.forEach((css) => _animate.removeClass(element, css)); + //element.classes.removeAll(previousSet); } } }, readOnly: true); @@ -188,7 +189,21 @@ abstract class _NgClassBase { _handleChange(index) { if (mode == null || (index != null && index % 2 == mode)) { - element.classes..removeAll(previousSet)..addAll(currentSet); + previousSet.forEach((css) { + if (!currentSet.contains(css)) { + _animate.removeClass(element, css); + } else { + element.classes.remove(css); + } + }); + + currentSet.forEach((css) { + if (!previousSet.contains(css)) { + _animate.addClass(element, css); + } else { + element.classes.add(css); + } + }); } previousSet = currentSet; @@ -200,8 +215,8 @@ abstract class _NgClassBase { classes = (classes as CollectionChangeRecord).iterable.toList(); } if (classes is List) { - return classes.where((String e) => e != null && e.isNotEmpty) - .toList(growable: false); + return classes.where((String e) => e != null && e.isNotEmpty).toList( + growable: false); } if (classes is MapChangeRecord) { classes = (classes as MapChangeRecord).map; @@ -210,6 +225,7 @@ abstract class _NgClassBase { return classes.keys.where((key) => toBool(classes[key])).toList(); } if (classes is String) return classes.split(' '); - throw 'ng-class expects expression value to be List, Map or String, got $classes'; + throw + 'ng-class expects expression value to be List, Map or String, got $classes'; } } diff --git a/lib/directive/ng_cloak.dart b/lib/directive/ng_cloak.dart index a661af8e5..274667d8d 100644 --- a/lib/directive/ng_cloak.dart +++ b/lib/directive/ng_cloak.dart @@ -25,8 +25,8 @@ part of angular.directive; @NgDirective(selector: '[ng-cloak]') @NgDirective(selector: '.ng-cloak') class NgCloakDirective { - NgCloakDirective(dom.Element element) { + NgCloakDirective(dom.Element element, NgAnimate animate) { element.attributes.remove('ng-cloak'); - element.classes.remove('ng-cloak'); + animate.removeClass(element, 'ng-cloak'); } } diff --git a/lib/directive/ng_control.dart b/lib/directive/ng_control.dart index 3c6f9690c..cccc4ecd8 100644 --- a/lib/directive/ng_control.dart +++ b/lib/directive/ng_control.dart @@ -21,13 +21,15 @@ abstract class NgControl implements NgDetachAware { final Scope _scope; final NgControl _parentControl; + final NgAnimate _animate; dom.Element _element; final Map> errors = new Map>(); final List _controls = new List(); final Map _controlByName = new Map(); - NgControl(Scope this._scope, dom.Element this._element, Injector injector) + NgControl(Scope this._scope, dom.Element this._element, Injector injector, + NgAnimate this._animate) : _parentControl = injector.parent.get(NgControl) { pristine = true; @@ -50,10 +52,12 @@ abstract class NgControl implements NgDetachAware { _onSubmit(bool valid) { if (valid) { _submit_valid = true; - element.classes..add(NG_SUBMIT_VALID_CLASS)..remove(NG_SUBMIT_INVALID_CLASS); + _animate.addClass(element, NG_SUBMIT_VALID_CLASS); + _animate.removeClass(element, NG_SUBMIT_INVALID_CLASS); } else { _submit_valid = false; - element.classes..add(NG_SUBMIT_INVALID_CLASS)..remove(NG_SUBMIT_VALID_CLASS); + _animate.addClass(element, NG_SUBMIT_INVALID_CLASS); + _animate.removeClass(element, NG_SUBMIT_VALID_CLASS); } } @@ -74,7 +78,8 @@ abstract class NgControl implements NgDetachAware { _pristine = true; _dirty = false; - element.classes..remove(NG_DIRTY_CLASS)..add(NG_PRISTINE_CLASS); + _animate.addClass(element, NG_PRISTINE_CLASS); + _animate.removeClass(element, NG_DIRTY_CLASS); } get dirty => _dirty; @@ -82,7 +87,8 @@ abstract class NgControl implements NgDetachAware { _dirty = true; _pristine = false; - element.classes..remove(NG_PRISTINE_CLASS)..add(NG_DIRTY_CLASS); + _animate.addClass(element, NG_DIRTY_CLASS); + _animate.removeClass(element, NG_PRISTINE_CLASS); //as soon as one of the controls/models is modified //then all of the parent controls are dirty as well @@ -94,7 +100,8 @@ abstract class NgControl implements NgDetachAware { _invalid = false; _valid = true; - element.classes..remove(NG_INVALID_CLASS)..add(NG_VALID_CLASS); + _animate.addClass(element, NG_VALID_CLASS); + _animate.removeClass(element, NG_INVALID_CLASS); } get invalid => _invalid; @@ -102,7 +109,8 @@ abstract class NgControl implements NgDetachAware { _valid = false; _invalid = true; - element.classes..remove(NG_VALID_CLASS)..add(NG_INVALID_CLASS); + _animate.addClass(element, NG_INVALID_CLASS); + _animate.removeClass(element, NG_VALID_CLASS); } get touched => _touched; @@ -110,8 +118,8 @@ abstract class NgControl implements NgDetachAware { _touched = true; _untouched = false; - element.classes..remove(NG_UNTOUCHED_CLASS)..add(NG_TOUCHED_CLASS); - + _animate.addClass(element, NG_TOUCHED_CLASS); + _animate.removeClass(element, NG_UNTOUCHED_CLASS); //as soon as one of the controls/models is touched //then all of the parent controls are touched as well _parentControl.touched = true; @@ -121,7 +129,9 @@ abstract class NgControl implements NgDetachAware { set untouched(value) { _touched = false; _untouched = true; - element.classes..remove(NG_TOUCHED_CLASS)..add(NG_UNTOUCHED_CLASS); + + _animate.addClass(element, NG_UNTOUCHED_CLASS); + _animate.removeClass(element, NG_TOUCHED_CLASS); } /** @@ -193,7 +203,7 @@ abstract class NgControl implements NgDetachAware { class NgNullControl implements NgControl { var _name, _dirty, _valid, _invalid, _submit_valid, _pristine, _element; var _touched, _untouched; - var _controls, _scope, _parentControl, _controlName; + var _controls, _scope, _parentControl, _controlName, _animate; var errors, _controlByName; dom.Element element; diff --git a/lib/directive/ng_form.dart b/lib/directive/ng_form.dart index c2c4427f7..cce0b4400 100644 --- a/lib/directive/ng_form.dart +++ b/lib/directive/ng_form.dart @@ -32,8 +32,9 @@ class NgForm extends NgControl implements Map { * * [element] - The form DOM element. * * [injector] - An instance of Injector. */ - NgForm(Scope scope, dom.Element element, Injector injector) : - super(scope, element, injector) { + NgForm(Scope scope, dom.Element element, Injector injector, + NgAnimate animate) : + super(scope, element, injector, animate) { if (!element.attributes.containsKey('action')) { element.onSubmit.listen((event) { diff --git a/lib/directive/ng_if.dart b/lib/directive/ng_if.dart index 22120439e..548c536e4 100644 --- a/lib/directive/ng_if.dart +++ b/lib/directive/ng_if.dart @@ -31,7 +31,7 @@ abstract class _NgUnlessIfAttrDirectiveBase { var insertBlock = _block; _scope.rootScope.domWrite(() { insertBlock.insertAfter(_blockHole); - }); + }); } } diff --git a/lib/directive/ng_model.dart b/lib/directive/ng_model.dart index 712bc7783..84ab30305 100644 --- a/lib/directive/ng_model.dart +++ b/lib/directive/ng_model.dart @@ -27,8 +27,8 @@ class NgModel extends NgControl implements NgAttachAware { Function render = (value) => null; NgModel(Scope _scope, dom.Element _element, Injector injector, - NgForm this._form, this._parser, NodeAttrs attrs) - : super(_scope, _element, injector) + NgForm this._form, this._parser, NodeAttrs attrs, NgAnimate animate) + : super(_scope, _element, injector, animate) { _exp = attrs["ng-model"]; watchCollection = false; diff --git a/lib/directive/ng_repeat.dart b/lib/directive/ng_repeat.dart index 91bb5c4b0..873c3f63c 100644 --- a/lib/directive/ng_repeat.dart +++ b/lib/directive/ng_repeat.dart @@ -256,8 +256,10 @@ abstract class AbstractNgRepeatDirective { nextNode = nextNode.nextNode; } while(nextNode != null); - // existing item which got moved - if (row.startNode != nextNode) row.block.moveAfter(cursor); + if (row.startNode != nextNode) { + // existing item which got moved + row.block.moveAfter(cursor); + } previousNode = row.endNode; } else { // new item which we don't know about diff --git a/lib/directive/ng_show_hide.dart b/lib/directive/ng_show_hide.dart index 560cfce76..6b4260fa2 100644 --- a/lib/directive/ng_show_hide.dart +++ b/lib/directive/ng_show_hide.dart @@ -12,14 +12,15 @@ class NgHideDirective { static String NG_HIDE_CLASS = 'ng-hide'; final dom.Element element; + final NgAnimate animate; - NgHideDirective(this.element); + NgHideDirective(this.element, this.animate); set hide(value) { if (toBool(value)) { - element.classes.add(NG_HIDE_CLASS); + animate.addClass(element, NG_HIDE_CLASS); } else { - element.classes.remove(NG_HIDE_CLASS); + animate.removeClass(element, NG_HIDE_CLASS); } } } @@ -34,14 +35,15 @@ class NgHideDirective { map: const {'ng-show': '=>show'}) class NgShowDirective { final dom.Element element; + final NgAnimate animate; - NgShowDirective(this.element); + NgShowDirective(this.element, this.animate); set show(value) { if (toBool(value)) { - element.classes.remove(NgHideDirective.NG_HIDE_CLASS); + animate.removeClass(element, NgHideDirective.NG_HIDE_CLASS); } else { - element.classes.add(NgHideDirective.NG_HIDE_CLASS); + animate.addClass(element, NgHideDirective.NG_HIDE_CLASS); } } } diff --git a/lib/directive/ng_switch.dart b/lib/directive/ng_switch.dart index a66d5c404..fb9309bc7 100644 --- a/lib/directive/ng_switch.dart +++ b/lib/directive/ng_switch.dart @@ -131,4 +131,4 @@ class NgSwitchDefaultDirective { BoundBlockFactory blockFactory, Scope scope) { ngSwitch.addCase('?', hole, blockFactory); } -} +} \ No newline at end of file diff --git a/lib/mock/mock_window.dart b/lib/mock/mock_window.dart index 7e7c4e928..1be4f4a80 100644 --- a/lib/mock/mock_window.dart +++ b/lib/mock/mock_window.dart @@ -10,11 +10,17 @@ class MockWindow extends Mock implements Window { final onHashChangeController = new dart_async.StreamController(); final onClickController = new dart_async.StreamController(); - dart_async.Stream get onPopState => onPopStateController.stream; dart_async.Stream get onHashChange => onHashChangeController.stream; dart_async.Stream get onClick => onClickController.stream; - + dart_async.Future get animationFrame => animationFrameCompleter.future; + + executeAnimationFrame([num time=0.0]) { + var last = animationFrameCompleter; + animationFrameCompleter = new dart_async.Completer(); + last.complete(time); + } + noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } diff --git a/test/_specs.dart b/test/_specs.dart index 88b46e6e9..f6f63d8d0 100644 --- a/test/_specs.dart +++ b/test/_specs.dart @@ -14,6 +14,7 @@ export 'package:unittest/unittest.dart'; export 'package:unittest/mock.dart'; export 'package:di/dynamic_injector.dart'; export 'package:angular/angular.dart'; +export 'package:angular/animate/module.dart'; export 'package:angular/mock/module.dart'; export 'package:perf_api/perf_api.dart'; diff --git a/test/animate/animation_loop_spec.dart b/test/animate/animation_loop_spec.dart new file mode 100644 index 000000000..9a582fd86 --- /dev/null +++ b/test/animate/animation_loop_spec.dart @@ -0,0 +1,99 @@ +library animation_runner_spec; + +import 'dart:async'; +import '../_specs.dart'; + +main() { + describe('AnimationLoop', () { + TestBed _; + AnimationLoop runner; + MockAnimationFrame frame; + beforeEach(async(inject((TestBed tb, NgZone zone) { + _ = tb; + frame = new MockAnimationFrame(); + runner = new AnimationLoop(frame, new Profiler(), zone); + }))); + + it('should play animations with window animation frames', async(() { + var animation = new MockAnimation(); + animation.when(callsTo('read', anything)).alwaysReturn(null); + animation.when(callsTo('update', anything)) + .thenReturn(true, 2) + .thenReturn(false); + + runner.play(animation); + + animation.getLogs(callsTo('read', anything)).verify(happenedExactly(0)); + animation.getLogs(callsTo('update', anything)).verify(happenedExactly(0)); + animation.clearLogs(); + + frame.frame(0.0); + microLeap(); + + animation.getLogs(callsTo('read', anything)).verify(happenedExactly(1)); + animation.getLogs(callsTo('update', anything)).verify(happenedExactly(1)); + animation.clearLogs(); + + frame.frame(0.0); + microLeap(); + + animation.getLogs(callsTo('read', anything)).verify(happenedExactly(1)); + animation.getLogs(callsTo('update', anything)).verify(happenedExactly(1)); + animation.clearLogs(); + + frame.frame(0.0); + microLeap(); + + animation.getLogs(callsTo('read', anything)).verify(happenedExactly(1)); + animation.getLogs(callsTo('update', anything)).verify(happenedExactly(1)); + animation.clearLogs(); + + frame.frame(0.0); + microLeap(); + + animation.getLogs(callsTo('read', anything)).verify(happenedExactly(0)); + animation.getLogs(callsTo('update', anything)).verify(happenedExactly(0)); + })); + + it('should forget about animations when forget(animation) is called', async(() { + var animation = new MockAnimation(); + animation.when(callsTo('read', anything)).alwaysReturn(null); + animation.when(callsTo('update', anything)) + .thenReturn(true, 2) + .thenReturn(false); + + runner.play(animation); + runner.forget(animation); + + frame.frame(0.0); + microLeap(); + + animation.getLogs(callsTo('read', anything)).verify(happenedExactly(0)); + animation.getLogs(callsTo('update', anything)).verify(happenedExactly(0)); + })); + }); +} + +class MockAnimation extends Mock implements LoopedAnimation { + final Completer onCompletedCompleter = new Completer(); + Future get onCompleted => onCompletedCompleter.future; + + noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockAnimationFrame implements AnimationFrame { + Completer frameCompleter; + Future get animationFrame { + if (frameCompleter == null) + frameCompleter = new Completer(); + return frameCompleter.future; + } + + frame(num time) { + var completer = frameCompleter; + frameCompleter = null; + if (completer != null) { + completer.complete(time); + } + } +} \ No newline at end of file diff --git a/test/animate/animation_optimizer_spec.dart b/test/animate/animation_optimizer_spec.dart new file mode 100644 index 000000000..e7cf8d50a --- /dev/null +++ b/test/animate/animation_optimizer_spec.dart @@ -0,0 +1,54 @@ +library animation_optimizer_spec; + +import '../_specs.dart'; + +main() { + describe('AnimationLoop', () { + TestBed _; + AnimationOptimizer optimizer; + beforeEach(inject((TestBed tb, Expando expand) { + _ = tb; + optimizer = new AnimationOptimizer(expand); + })); + + it('should track and forget animations on elements', () { + var animation = new NoOpAnimation(); + _.compile('
'); + + expect(optimizer.isAnimating(_.rootElement)).toBeFalsy(); + optimizer.track(animation, _.rootElement); + expect(optimizer.isAnimating(_.rootElement)).toBeTruthy(); + optimizer.forget(animation); + expect(optimizer.isAnimating(_.rootElement)).toBeFalsy(); + }); + + it('should prevent animations on child elements', () { + var animation = new NoOpAnimation(); + _.compile('
'); + + + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + optimizer.track(animation, _.rootElement); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); + optimizer.forget(animation); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + }); + + it('should allow multiple animations on the same element', () { + var animation1 = new NoOpAnimation(); + var animation2 = new NoOpAnimation(); + _.compile('
'); + + expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); + optimizer.track(animation1, _.rootElement); + expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); + optimizer.track(animation2, _.rootElement); + expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy(); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); + optimizer.forget(animation1); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy(); + optimizer.forget(animation2); + expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy(); + }); + }); +} diff --git a/test/animate/css_animate_spec.dart b/test/animate/css_animate_spec.dart new file mode 100644 index 000000000..c5771b6cc --- /dev/null +++ b/test/animate/css_animate_spec.dart @@ -0,0 +1,131 @@ +library css_animate_spec; + +import 'dart:async'; + +import '../_specs.dart'; + +main() { + describe('CssAnimate', () { + TestBed _; + NgAnimate animate; + MockAnimationLoop runner; + + beforeEach(inject((TestBed tb, Expando expand) { + _ = tb; + runner = new MockAnimationLoop(); + animate = new CssAnimate(runner, + new CssAnimationMap(), new AnimationOptimizer(expand)); + })); + + it('should add a css class to an element node', async(() { + _.compile('
'); + expect(_.rootElement).not.toHaveClass('foo'); + + animate.addClass(_.rootElement, 'foo'); + runner.frame(); + + expect(_.rootElement).toHaveClass('foo'); + })); + + it('should remove a css class from an element node', async(() { + _.compile('
'); + expect(_.rootElement).toHaveClass('foo'); + + animate.removeClass(_.rootElement, 'foo'); + runner.frame(); + expect(_.rootElement).not.toHaveClass('foo'); + })); + + it('should insert nodes', async(() { + _.compile('
'); + expect(_.rootElement.children.length).toBe(0); + + animate.insert([new Element.div()], _.rootElement); + expect(_.rootElement.children.length).toBe(1); + })); + + it('should remove nodes', async(() { + _.compile('

Hello World

'); + expect(_.rootElement.childNodes.length).toBe(2); + + animate.remove(_.rootElement.childNodes); + runner.frame(); + // This might lead to a flash of unstyled content before + // removal. It would be nice if this was un-needed. + microLeap(); + expect(_.rootElement.childNodes.length).toBe(0); + })); + + it('should move nodes', async(() { + _.compile('
'); + List a = $('Aa').toList(); + List b = $('Bb').toList(); + a.forEach((n) => _.rootElement.append(n)); + b.forEach((n) => _.rootElement.append(n)); + expect(_.rootElement.text).toEqual("AaBb"); + + animate.move(b, _.rootElement, insertBefore: a.first); + runner.frame(); + expect(_.rootElement.text).toEqual("BbAa"); + + animate.move(a, _.rootElement, insertBefore: b.first); + runner.frame(); + expect(_.rootElement.text).toEqual("AaBb"); + + animate.move(a, _.rootElement); + runner.frame(); + expect(_.rootElement.text).toEqual("BbAa"); + })); + + + it('should animate multiple elements', async(() { + _.compile('
'); + List nodes = $('AaBb').toList(); + + animate.insert(nodes, _.rootElement); + runner.frame(); + expect(_.rootElement.text).toEqual("AaBb"); + })); + + it('should prevent child animations', async(() { + _.compile('
'); + animate.addClass(_.rootElement, 'test'); + runner.start(); + expect(_.rootElement).toHaveClass('test-add'); + var spans = $('AB'); + animate.insert(spans, _.rootElement); + runner.start(); + expect(spans.first).not.toHaveClass('ng-add'); + })); + }); +} + +class MockAnimationLoop extends Mock implements AnimationLoop { + num time = 0.0; + + Future get onCompleted { + var cmp = new Completer(); + cmp.complete(AnimationResult.COMPLETED); + return cmp.future; + } + + List animations = []; + + play(Animation animation) { + animations.add(animation); + } + + frame() { + for(var animation in animations) { + animation.read(time); + } + + for(var animation in animations) { + animation.update(time); + } + + time += 16.0; + } + + noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} \ No newline at end of file diff --git a/test/animate/css_animation_spec.dart b/test/animate/css_animation_spec.dart new file mode 100644 index 000000000..23c73f0e4 --- /dev/null +++ b/test/animate/css_animation_spec.dart @@ -0,0 +1,156 @@ +library css_animation_spec; + +import '../_specs.dart'; + +main() { + describe('CssAnimation', () { + TestBed _; + + beforeEach(inject((TestBed tb) => _ = tb)); + afterEach(() => _.rootElements.forEach((e) => e.remove())); + + it('should correctly respond to an animation lifecycle', async(() { + _.compile("" + +"
"); + + _.rootElements.forEach((e) => document.body.append(e)); + var element = _.rootElements[1]; + + expect(element).toHaveClass('always'); + expect(element).toHaveClass('remove-start'); + expect(element).toHaveClass('remove-end'); + expect(element).not.toHaveClass('event'); + expect(element).not.toHaveClass('event-active'); + expect(element).not.toHaveClass('add-start'); + expect(element).not.toHaveClass('add-end'); + + var animation = new CssAnimation(element, + "event", + "event-active", + addAtStart: "add-start", + removeAtStart: "remove-start", + addAtEnd: "add-end", + removeAtEnd: "remove-end"); + + expect(element).toHaveClass('always'); + expect(element).not.toHaveClass('remove-start'); + expect(element).toHaveClass('remove-end'); + expect(element).toHaveClass('event'); + expect(element).not.toHaveClass('event-active'); + expect(element).toHaveClass('add-start'); + expect(element).not.toHaveClass('add-end'); + + animation.read(0.0); + animation.update(0.0); + + expect(element).toHaveClass('always'); + expect(element).not.toHaveClass('remove-start'); + expect(element).toHaveClass('remove-end'); + expect(element).toHaveClass('event'); + expect(element).toHaveClass('event-active'); + expect(element).toHaveClass('add-start'); + expect(element).not.toHaveClass('add-end'); + + animation.read(1000.0); + animation.update(1000.0); + + expect(element).toHaveClass('always'); + expect(element).not.toHaveClass('remove-start'); + expect(element).not.toHaveClass('remove-end'); + expect(element).not.toHaveClass('event'); + expect(element).not.toHaveClass('event-active'); + expect(element).toHaveClass('add-start'); + expect(element).toHaveClass('add-end'); + + _.rootElements.forEach((e) => e.remove()); + })); + + it('should swap removeAtEnd class if initial style is display none', async(() { + _.compile("" + "
"); + _.rootElements.forEach((e) => document.body.append(e)); + var element = _.rootElements[1]; + + var animation = new CssAnimation(element, "event", "event-active", + removeAtEnd: 'remove-at-end', addAtEnd: 'add-at-end'); + + expect(element).toHaveClass('remove-at-end'); + + animation.read(0.0); + animation.update(0.0); + expect(element).not.toHaveClass('remove-at-end'); + expect(element).not.toHaveClass('add-at-end'); + + animation.read(1000.0); + animation.update(1000.0); + expect(element).toHaveClass('add-at-end'); + expect(element).not.toHaveClass('remove-at-end'); + })); + + it('should add classes at end', async(() { + _.compile("
"); + _.rootElements.forEach((e) => document.body.append(e)); + var element = _.rootElements[1]; + + var animation = new CssAnimation(element, "event", "event-active", + addAtEnd: 'add-at-end'); + + animation.read(0.0); + animation.update(0.0); + expect(element).not.toHaveClass('add-at-end'); + + animation.read(1000.0); + animation.update(1000.0); + expect(element).toHaveClass('add-at-end'); + expect(element).not.toHaveClass('event'); + expect(element).not.toHaveClass('event-active'); + })); + + it('should remove the cssClassToRemove', async(() { + _.compile("" + +"
"); + _.rootElements.forEach((e) => document.body.append(e)); + var element = _.rootElements[1]; + + var animation = new CssAnimation(element, "event", "event-active", + removeAtEnd: 'magic'); + + animation.complete(); + expect(element).not.toHaveClass('magic'); + + expect(element).not.toHaveClass('event'); + expect(element).not.toHaveClass('event-active'); + })); + + it('should clean up event classes when canceled after read', async(() { + _.compile("
"); + _.rootElements.forEach((e) => document.body.append(e)); + var element = _.rootElements[1]; + var animation = new CssAnimation(element, "event", "event-active", + addAtEnd: 'magic'); + + animation.read(0.0); + animation.cancel(); + expect(element).not.toHaveClass('magic'); + expect(element).not.toHaveClass('event'); + expect(element).not.toHaveClass('event-active'); + })); + + it('should clean up event classes when canceled after update', async(() { + _.compile("
"); + _.rootElements.forEach((e) => document.body.append(e)); + var element = _.rootElements[1]; + + var animation = new CssAnimation(element, "event", "event-active", + addAtEnd: 'add-end'); + + animation.read(0.0); + animation.update(0.0); + animation.cancel(); + expect(element).not.toHaveClass('add-end'); + expect(element).not.toHaveClass('event'); + expect(element).not.toHaveClass('event-active'); + })); + }); +} + diff --git a/test/core_dom/ng_animate_spec.dart b/test/core_dom/ng_animate_spec.dart new file mode 100644 index 000000000..974d0cbe2 --- /dev/null +++ b/test/core_dom/ng_animate_spec.dart @@ -0,0 +1,89 @@ +library no_animate_spec; + +import '../_specs.dart'; + +main() { + describe('NgAniamte', () { + TestBed _; + beforeEach(inject((TestBed tb) => _ = tb)); + + it('should exist', + inject((NgAnimate aniamte) { + expect(aniamte).toBeDefined(); + })); + + it('should add a css classes to nodes.', () { + var animate = new NgAnimate(); + _.compile('
'); + expect(_.rootElement).not.toHaveClass('foo'); + animate.addClass(_.rootElement, 'foo'); + expect(_.rootElement).toHaveClass('foo'); + }); + + it('should remove css classes from nodes.', () { + var animate = new NgAnimate(); + _.compile('
'); + expect(_.rootElement).toHaveClass('foo'); + animate.removeClass(_.rootElement, 'foo'); + expect(_.rootElement).not.toHaveClass('foo'); + }); + + it('should insert elements', () { + var animate = new NgAnimate(); + _.compile('
'); + expect(_.rootElement.children.length).toBe(0); + animate.insert([new Element.div()], _.rootElement); + expect(_.rootElement.children.length).toBe(1); + }); + + it('should remove nodes and elements', () { + var animate = new NgAnimate(); + _.compile('

Hello World

'); + expect(_.rootElement.childNodes.length).toBe(2); + animate.remove(_.rootElement.childNodes); + expect(_.rootElement.childNodes.length).toBe(0); + }); + + it('should move nodes and elements', () { + var animate = new NgAnimate(); + _.compile('
'); + List a = $('Aa').toList(); + List b = $('Bb').toList(); + a.forEach((n) => _.rootElement.append(n)); + b.forEach((n) => _.rootElement.append(n)); + + expect(_.rootElement.text).toEqual("AaBb"); + + animate.move(b, _.rootElement, insertBefore: a.first); + expect(_.rootElement.text).toEqual("BbAa"); + + animate.move(a, _.rootElement, insertBefore: b.first); + expect(_.rootElement.text).toEqual("AaBb"); + + animate.move(a, _.rootElement); + expect(_.rootElement.text).toEqual("BbAa"); + }); + }); + + describe('NoOpAnimation', () { + it('should not do anything async unless the future is asked for', () { + var completer = new NoOpAnimation(); + expect(completer).toBeDefined(); + }); + + it('should create a future once onCompleted is accessed', () { + expect(() => new NoOpAnimation().onCompleted).toThrow(); + }); + + it('should return a [COMPLETED_IGNORED] result when completed.', async(() { + bool success = false; + new NoOpAnimation().onCompleted.then((result) { + if (result == AnimationResult.COMPLETED_IGNORED) { + success = true; + } + }); + microLeap(); + expect(success).toBe(true); + })); + }); +} diff --git a/test/directive/ng_model_spec.dart b/test/directive/ng_model_spec.dart index 645e9c1a4..0a4fb6a4f 100644 --- a/test/directive/ng_model_spec.dart +++ b/test/directive/ng_model_spec.dart @@ -86,7 +86,7 @@ void main() { var element = new dom.InputElement(); NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -284,7 +284,7 @@ void main() { var element = new dom.InputElement(); NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -349,7 +349,7 @@ void main() { var element = new dom.InputElement(); NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -422,7 +422,7 @@ void main() { var element = new dom.InputElement(); NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope); @@ -576,7 +576,7 @@ void main() { var element = new dom.TextAreaElement(); NodeAttrs nodeAttrs = new NodeAttrs(new DivElement()); nodeAttrs['ng-model'] = 'model'; - var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs); + var model = new NgModel(scope, element, i.createChild([new Module()]), new NgNullForm(), parser, nodeAttrs, new NgAnimate()); dom.querySelector('body').append(element); var input = new InputTextLikeDirective(element, model, scope);