Skip to content
This repository has been archived by the owner on Feb 22, 2018. It is now read-only.

Commit

Permalink
feat(animate): ng-animate and ng-animate-children.
Browse files Browse the repository at this point in the history
These two directives allow you to control animations and turn them on or off on individual elements or whole sections of dom.

Closes #661
  • Loading branch information
codelogic authored and mhevery committed Mar 6, 2014
1 parent 312c0e6 commit 88d2af6
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 24 deletions.
3 changes: 3 additions & 0 deletions demo/animate_demo/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ <h2>Visibility Demo</h2>
</div>
<div class="demo" ng-switch-when="Css">
<h2>Css Demo</h2>
<p><strong>TODO</strong> This should contain a demo of css animation by applying multiple
classes and running multiple simultaneous animations on the same
object.</p>
<css-demo></css-demo>
</div>
<div class="demo" ng-switch-when="Stress Test">
Expand Down
85 changes: 72 additions & 13 deletions lib/animate/animation_optimizer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class AnimationOptimizer {
final Map<Animation, dom.Element> _animations = new Map<Animation,
dom.Element>();

final Map<dom.Node, bool> _alwaysAnimate = new Map<dom.Node, bool>();
final Map<dom.Node, bool> _alwaysAnimateChildren = new Map<dom.Node, bool>();

Expando _expando;

AnimationOptimizer(this._expando);
Expand Down Expand Up @@ -46,14 +49,50 @@ class AnimationOptimizer {
}
}

// TODO(codelogic): Allow animations to be forcibly prevented from executing
// on certain elements, elements and children, and forcibly allowed (ignoring
// parent animation state);
/**
* Since we can't overload forget...
*/
void detachAlwaysAnimateOptions(dom.Element element) {
_alwaysAnimate.remove(element);
_alwaysAnimateChildren.remove(element);
}

/**
* Control animation for a specific element, ignoring every other option.
* [mode] "always" will always animate this element.
* [mode] "never" will never animate this element.
* [mode] "auto" will detect if a parent animation is running or has child animations set.
*/
void alwaysAnimate(dom.Element element, String mode) {
if(mode == "always") {
_alwaysAnimate[element] = true;
} else if (mode == "never") {
_alwaysAnimate[element] = false;
} else if (mode == "auto") {
_alwaysAnimate.remove(element);
}
}

/**
* Control animation for child elements, ignoring running animations unless 'auto' is provided as an option.
* [mode] "always" will always animate children, unless it is specifically marked not to by [alwaysAnimate].
* [mode] "never" will never animate children.
* [mode] "auto" will detect if a parent animation is running or has child animations set.
*/
void alwaysAnimateChildren(dom.Element element, String mode) {
if(mode == "always") {
_alwaysAnimateChildren[element] = true;
} else if (mode == "never") {
_alwaysAnimateChildren[element] = false;
} else if (mode == "auto") {
_alwaysAnimateChildren.remove(element);
}
}

/**
* Returns true if there is tracked animation on the given element.
*/
bool isAnimating(dom.Element element) {
bool _isAnimating(dom.Element element) {
return _elements.containsKey(element);
}

Expand All @@ -63,32 +102,52 @@ class AnimationOptimizer {
* and [false] if the optimizer thinks that it should not execute.
*/
bool shouldAnimate(dom.Node node) {
//var probe = _findElementProbe(node.parentNode);
var source = node;
bool alwaysAnimate = _alwaysAnimate[node];
if (alwaysAnimate != null) {
print("alwaysAnimate: $alwaysAnimate");
return alwaysAnimate;
}

// If there are 'always allow' or 'always prevent' animations declared,
// fallback to the automatic detection of running parent animations. By
// default, we assume that we can run.
bool autoDecision = true;

node = node.parentNode;
while (node != null) {
if (node.nodeType == dom.Node.ELEMENT_NODE
&& isAnimating(node)) {
// Does this node give us animation information about our children?
alwaysAnimate = _alwaysAnimateChildren[node];
if(alwaysAnimate != null) {
return alwaysAnimate;
}

// If we hit a running parent animation, we still need to continue up
// the dom tree to see if there is or is not an 'alwaysAnimateChildren'
// decision somewhere.
if (autoDecision
&& node.nodeType == dom.Node.ELEMENT_NODE
&& _isAnimating(node)) {
// If there is an already running animation, don't animate.
return false;
autoDecision = 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.
// Escape shadow dom!
node = probe.parent.element;
} else {
// If we are at the root of the document, we can animate.
return true;
// If we can't go any further, return the auto decision because we
// havent hit any other more important optimizations.
return autoDecision;
}
} else {
node = node.parentNode;
}
}

return true;
return autoDecision;
}

// Search and find the element probe for a given node.
Expand Down
3 changes: 3 additions & 0 deletions lib/animate/module.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ part 'animation_loop.dart';
part 'animation_optimizer.dart';
part 'css_animate.dart';
part 'css_animation.dart';
part 'ng_animate.dart';

final Logger _logger = new Logger('ng.animate');

Expand Down Expand Up @@ -54,6 +55,8 @@ class NgAnimateModule extends Module {
type(AnimationLoop);
type(CssAnimationMap);
type(AnimationOptimizer);
type(NgAnimateDirective);
type(NgAnimateChildrenDirective);
type(NgAnimate, implementedBy: CssAnimate);
}
}
57 changes: 57 additions & 0 deletions lib/animate/ng_animate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
part of angular.animate;

/**
* This provides DOM controls for turning animations on and off for individual
* dom elements. Valid options are [always] [never] and [auto]. If this
* directive is not applied the default value is [auto] for animation.
*/
@NgDirective(selector: '[ng-animate]',
map: const {'ng-animate': '@option'})
class NgAnimateDirective extends NgAnimateDirectiveBase {
set option(value) {
_option = value;
_optimizer.alwaysAnimate(_element, _option);
}

NgAnimateDirective(dom.Element element, AnimationOptimizer optimizer)
: super(element, optimizer);
}

/**
* This provides DOM controls for turning animations on and off for child
* dom elements. Valid options are [always] [never] and [auto]. If this
* directive is not applied the default value is [auto] for animation.
*
* Values provided in [ng-animate] will override this directive since they are
* more specific.
*/
@NgDirective(selector: '[ng-animate-children]',
map: const {'ng-animate-children': '@option'})
class NgAnimateChildrenDirective extends NgAnimateDirectiveBase {
set option(value) {
_option = value;
_optimizer.alwaysAnimateChildren(_element, _option);
}

NgAnimateChildrenDirective(dom.Element element, AnimationOptimizer optimizer)
: super(element, optimizer);
}

/**
* Base class for directives that control animations with an
* [AnimationOptimizer].
*/
abstract class NgAnimateDirectiveBase implements NgDetachAware {
final AnimationOptimizer _optimizer;
final dom.Element _element;

String _option = "auto";
String get option => _option;
set option(value);

NgAnimateDirectiveBase(this._element, this._optimizer);

detach() {
_optimizer.detachAlwaysAnimateOptions(_element);
}
}
122 changes: 111 additions & 11 deletions test/animate/animation_optimizer_spec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,6 @@ main() {
optimizer = new AnimationOptimizer(expand);
}));

it('should track and forget animations on elements', () {
var animation = new NoOpAnimation();
_.compile('<div></div>');

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('<div><div></div></div>');
Expand Down Expand Up @@ -50,5 +39,116 @@ main() {
optimizer.forget(animation2);
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();
});

it('should always animate an element', () {
_.compile('<div><div></div></div>');
optimizer.alwaysAnimate(_.rootElement.children[0], "never");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy();
optimizer.alwaysAnimate(_.rootElement.children[0], "always");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();
optimizer.alwaysAnimate(_.rootElement.children[0], "auto");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();
});

it('alwaysAnimate should not affect children', () {
_.compile('<div><div></div></div>');
optimizer.alwaysAnimate(_.rootElement, "never");
expect(optimizer.shouldAnimate(_.rootElement)).toBeFalsy();
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();
optimizer.alwaysAnimate(_.rootElement, "always");
expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy();
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();
optimizer.alwaysAnimate(_.rootElement, "auto");
expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy();
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();
});


it('alwaysAnimateChildren should not affect element', () {
_.compile('<div><div></div></div>');

optimizer.alwaysAnimateChildren(_.rootElement, "never");
expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy();
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy();

optimizer.alwaysAnimateChildren(_.rootElement, "always");
expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy();
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();

optimizer.alwaysAnimateChildren(_.rootElement, "auto");
expect(optimizer.shouldAnimate(_.rootElement)).toBeTruthy();
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();
});

it('alwaysAnimate should take priority over alwaysAnimateChildren', () {
_.compile('<div><div></div></div>');

optimizer.alwaysAnimateChildren(_.rootElement, "never");
optimizer.alwaysAnimate(_.rootElement.children[0], "always");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();

optimizer.alwaysAnimateChildren(_.rootElement, "always");
optimizer.alwaysAnimate(_.rootElement.children[0], "never");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy();
});

it('alwaysAnimate should take priority over running animations', () {
_.compile('<div><div></div></div>');
var animation = new NoOpAnimation();

optimizer.track(animation, _.rootElement);
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy();

optimizer.alwaysAnimate(_.rootElement.children[0], "always");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();

optimizer.alwaysAnimate(_.rootElement.children[0], "auto");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy();

optimizer.forget(animation);
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();
});

it('alwaysAnimateChildren should take priority over running animations',
() {
_.compile('<div><div></div></div>');
var animation = new NoOpAnimation();

optimizer.track(animation, _.rootElement);
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy();

optimizer.alwaysAnimateChildren(_.rootElement, "always");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();

optimizer.alwaysAnimateChildren(_.rootElement, "auto");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy();

optimizer.forget(animation);
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeTruthy();

optimizer.alwaysAnimateChildren(_.rootElement, "never");
expect(optimizer.shouldAnimate(_.rootElement.children[0])).toBeFalsy();
});


it('alwaysAnimateChildren when nested should prioritize the closest'
+'element up the tree.',
() {
_.compile('<div><div><div></div></div></div>');

optimizer.alwaysAnimateChildren(_.rootElement, "always");
expect(optimizer.shouldAnimate(_.rootElement.children[0].children[0]))
.toBeTruthy();

optimizer.alwaysAnimateChildren(_.rootElement.children[0], "never");
expect(optimizer.shouldAnimate(_.rootElement.children[0].children[0]))
.toBeFalsy();


optimizer.alwaysAnimateChildren(_.rootElement, "never");
optimizer.alwaysAnimateChildren(_.rootElement.children[0], "always");
expect(optimizer.shouldAnimate(_.rootElement.children[0].children[0]))
.toBeTruthy();
});
});
}
Loading

0 comments on commit 88d2af6

Please sign in to comment.