aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Angular.js5
-rw-r--r--src/AngularPublic.js2
-rw-r--r--src/loader.js27
-rw-r--r--src/ng/animation.js65
-rw-r--r--src/ng/animator.js312
-rw-r--r--src/ng/directive/ngInclude.js28
-rw-r--r--src/ng/directive/ngRepeat.js29
-rw-r--r--src/ng/directive/ngShowHide.js54
-rw-r--r--src/ng/directive/ngSwitch.js92
-rw-r--r--src/ng/directive/ngView.js20
-rw-r--r--src/ng/sniffer.js26
-rw-r--r--src/ngMock/angular-mocks.js51
12 files changed, 639 insertions, 72 deletions
diff --git a/src/Angular.js b/src/Angular.js
index d7648d72..dc2d530a 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -247,6 +247,11 @@ function inherit(parent, extra) {
return extend(new (extend(function() {}, {prototype:parent}))(), extra);
}
+var START_SPACE = /^\s*/;
+var END_SPACE = /\s*$/;
+function stripWhitespace(str) {
+ return isString(str) ? str.replace(START_SPACE, '').replace(END_SPACE, '') : str;
+}
/**
* @ngdoc function
diff --git a/src/AngularPublic.js b/src/AngularPublic.js
index 3c5e46cb..a66c35b3 100644
--- a/src/AngularPublic.js
+++ b/src/AngularPublic.js
@@ -107,6 +107,8 @@ function publishExternalAPI(angular){
directive(ngEventDirectives);
$provide.provider({
$anchorScroll: $AnchorScrollProvider,
+ $animation: $AnimationProvider,
+ $animator: $AnimatorProvider,
$browser: $BrowserProvider,
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,
diff --git a/src/loader.js b/src/loader.js
index ecb16608..5b74a4f3 100644
--- a/src/loader.js
+++ b/src/loader.js
@@ -165,6 +165,33 @@ function setupModuleLoader(window) {
/**
* @ngdoc method
+ * @name angular.Module#animation
+ * @methodOf angular.Module
+ * @param {string} name animation name
+ * @param {Function} animationFactory Factory function for creating new instance of an animation.
+ * @description
+ *
+ * Defines an animation hook that can be later used with {@link ng.directive:ngAnimate ngAnimate}
+ * alongside {@link ng.directive:ngAnimate#Description common ng directives} as well as custom directives.
+ * <pre>
+ * module.animation('animation-name', function($inject1, $inject2) {
+ * return {
+ * //this gets called in preparation to setup an animation
+ * setup : function(element) { ... },
+ *
+ * //this gets called once the animation is run
+ * start : function(element, done, memo) { ... }
+ * }
+ * })
+ * </pre>
+ *
+ * See {@link ng.$animationProvider#register $animationProvider.register()} and
+ * {@link ng.directive:ngAnimate ngAnimate} for more information.
+ */
+ animation: invokeLater('$animationProvider', 'register'),
+
+ /**
+ * @ngdoc method
* @name angular.Module#filter
* @methodOf angular.Module
* @param {string} name Filter name.
diff --git a/src/ng/animation.js b/src/ng/animation.js
new file mode 100644
index 00000000..9a5a5a95
--- /dev/null
+++ b/src/ng/animation.js
@@ -0,0 +1,65 @@
+/**
+ * @ngdoc object
+ * @name ng.$animationProvider
+ * @description
+ *
+ * The $AnimationProvider provider allows developers to register and access custom JavaScript animations directly inside
+ * of a module.
+ *
+ */
+$AnimationProvider.$inject = ['$provide'];
+function $AnimationProvider($provide) {
+ var suffix = 'Animation';
+
+ /**
+ * @ngdoc function
+ * @name ng.$animation#register
+ * @methodOf ng.$animationProvider
+ *
+ * @description
+ * Registers a new injectable animation factory function. The factory function produces the animation object which
+ * has these two properties:
+ *
+ * * `setup`: `function(Element):*` A function which receives the starting state of the element. The purpose
+ * of this function is to get the element ready for animation. Optionally the function returns an memento which
+ * is passed to the `start` function.
+ * * `start`: `function(Element, doneFunction, *)` The element to animate, the `doneFunction` to be called on
+ * element animation completion, and an optional memento from the `setup` function.
+ *
+ * @param {string} name The name of the animation.
+ * @param {function} factory The factory function that will be executed to return the animation object.
+ *
+ */
+ this.register = function(name, factory) {
+ $provide.factory(camelCase(name) + suffix, factory);
+ };
+
+ this.$get = ['$injector', function($injector) {
+ /**
+ * @ngdoc function
+ * @name ng.$animation
+ * @function
+ *
+ * @description
+ * The $animation service is used to retrieve any defined animation functions. When executed, the $animation service
+ * will return a object that contains the setup and start functions that were defined for the animation.
+ *
+ * @param {String} name Name of the animation function to retrieve. Animation functions are registered and stored
+ * inside of the AngularJS DI so a call to $animate('custom') is the same as injecting `customAnimation`
+ * via dependency injection.
+ * @return {Object} the animation object which contains the `setup` and `start` functions that perform the animation.
+ */
+ return function $animation(name) {
+ if (name) {
+ try {
+ return $injector.get(camelCase(name) + suffix);
+ } catch (e) {
+ //TODO(misko): this is a hack! we should have a better way to test if the injector has a given key.
+ // The issue is that the animations are optional, and if not present they should be silently ignored.
+ // The proper way to fix this is to add API onto the injector so that we can ask to see if a given
+ // animation is supported.
+ }
+ }
+ }
+ }];
+};
diff --git a/src/ng/animator.js b/src/ng/animator.js
new file mode 100644
index 00000000..4bd5ae3c
--- /dev/null
+++ b/src/ng/animator.js
@@ -0,0 +1,312 @@
+'use strict';
+
+// NOTE: this is a pseudo directive.
+
+/**
+ * @ngdoc directive
+ * @name ng.directive:ngAnimate
+ *
+ * @description
+ * The `ngAnimate` directive works as an attribute that is attached alongside pre-existing directives.
+ * It effects how the directive will perform DOM manipulation. This allows for complex animations to take place while
+ * without burduning the directive which uses the animation with animation details. The built dn directives
+ * `ngRepeat`, `ngInclude`, `ngSwitch`, `ngShow`, `ngHide` and `ngView` already accept `ngAnimate` directive.
+ * Custom directives can take advantage of animation through {@link ng.$animator $animator service}.
+ *
+ * Below is a more detailed breakdown of the supported callback events provided by pre-exisitng ng directives:
+ *
+ * * {@link ng.directive:ngRepeat#animations ngRepeat} — enter, leave and move
+ * * {@link ng.directive:ngView#animations ngView} — enter and leave
+ * * {@link ng.directive:ngInclude#animations ngInclude} — enter and leave
+ * * {@link ng.directive:ngSwitch#animations ngSwitch} — enter and leave
+ * * {@link ng.directive:ngShow#animations ngShow & ngHide} - show and hide respectively
+ *
+ * You can find out more information about animations upon visiting each directive page.
+ *
+ * Below is an example of a directive that makes use of the ngAnimate attribute:
+ *
+ * <pre>
+ * <!-- you can also use data-ng-animate, ng:animate or x-ng-animate as well -->
+ * <ANY ng-directive ng-animate="{event1: 'animation-name', event2: 'animation-name-2'}"></ANY>
+ *
+ * <!-- you can also use a short hand -->
+ * <ANY ng-directive ng-animate=" 'animation' "></ANY>
+ * <!-- which expands to -->
+ * <ANY ng-directive ng-animate="{ enter: 'animation-enter', leave: 'animation-leave', ...}"></ANY>
+ *
+ * <!-- keep in mind that ng-animate can take expressions -->
+ * <ANY ng-directive ng-animate=" computeCurrentAnimation() "></ANY>
+ * </pre>
+ *
+ * The `event1` and `event2` attributes refer to the animation events specific to the directive that has been assigned.
+ *
+ * <h2>CSS-defined Animations</h2>
+ * By default, ngAnimate attaches two CSS3 classes per animation event to the DOM element to achieve the animation.
+ * This is up to you, the developer, to ensure that the animations take place using cross-browser CSS3 transitions.
+ * All that is required is the following CSS code:
+ *
+ * <pre>
+ * <style type="text/css">
+ * /&#42;
+ * The animate-enter prefix is the event name that you
+ * have provided within the ngAnimate attribute.
+ * &#42;/
+ * .animate-enter-setup {
+ * -webkit-transition: 1s linear all; /&#42; Safari/Chrome &#42;/
+ * -moz-transition: 1s linear all; /&#42; Firefox &#42;/
+ * -ms-transition: 1s linear all; /&#42; IE10 &#42;/
+ * -o-transition: 1s linear all; /&#42; Opera &#42;/
+ * transition: 1s linear all; /&#42; Future Browsers &#42;/
+ *
+ * /&#42; The animation preparation code &#42;/
+ * opacity: 0;
+ * }
+ *
+ * /&#42;
+ * Keep in mind that you want to combine both CSS
+ * classes together to avoid any CSS-specificity
+ * conflicts
+ * &#42;/
+ * .animate-enter-setup.animate-enter-start {
+ * /&#42; The animation code itself &#42;/
+ * opacity: 1;
+ * }
+ * </style>
+ *
+ * <div ng-directive ng-animate="{enter: 'animate-enter'}"></div>
+ * </pre>
+ *
+ * Upon DOM mutation, the setup class is added first, then the browser is allowed to reflow the content and then,
+ * the start class is added to trigger the animation. The ngAnimate directive will automatically extract the duration
+ * of the animation to determine when the animation ends. Once the animation is over then both CSS classes will be
+ * removed from the DOM. If a browser does not support CSS transitions then the animation will start and end
+ * immediately resulting in a DOM element that is at it's final state. This final state is when the DOM element
+ * has no CSS animation classes surrounding it.
+ *
+ * <h2>JavaScript-defined Animations</h2>
+ * In the event that you do not want to use CSS3 animations or if you wish to offer animations to browsers that do not
+ * yet support them, then you can make use of JavaScript animations defined inside ngModule.
+ *
+ * <pre>
+ * var ngModule = angular.module('YourApp', []);
+ * ngModule.animation('animate-enter', function() {
+ * return {
+ * setup : function(element) {
+ * //prepare the element for animation
+ * element.css({ 'opacity': 0 });
+ * var memo = "..."; //this value is passed to the start function
+ * return memo;
+ * },
+ * start : function(element, done, memo) {
+ * //start the animation
+ * element.animate({
+ * 'opacity' : 1
+ * }, function() {
+ * //call when the animation is complete
+ * done()
+ * });
+ * }
+ * }
+ * });
+ * </pre>
+ *
+ * As you can see, the JavaScript code follows a similar template to the CSS3 animations. Once defined, the animation
+ * can be used in the same way with the ngAnimate attribute. Keep in mind that, when using JavaScript-enabled
+ * animations, ngAnimate will also add in the same CSS classes that CSS-enabled animations do (even if you're using
+ * JavaScript animations) to animated the element, but it will not attempt to find any CSS3 transition duration value.
+ * It will instead close off the animation once the provided done function is executed. So it's important that you
+ * make sure your animations remember to fire off the done function once the animations are complete.
+ *
+ * @param {expression} ngAnimate Used to configure the DOM manipulation animations.
+ *
+ */
+
+/**
+ * @ngdoc function
+ * @name ng.$animator
+ *
+ * @description
+ * The $animator service provides the DOM manipulation API which is decorated with animations.
+ *
+ * @param {Scope} scope the scope for the ng-animate.
+ * @param {Attributes} attr the attributes object which contains the ngAnimate key / value pair. (The attributes are
+ * passed into the linking function of the directive using the `$animator`.)
+ * @return {object} the animator object which contains the enter, leave, move, show, hide and animate methods.
+ */
+var $AnimatorProvider = function() {
+ this.$get = ['$animation', '$window', '$sniffer', function($animation, $window, $sniffer) {
+ return function(scope, attrs) {
+ var ngAnimateAttr = attrs.ngAnimate;
+ var animator = {};
+
+ /**
+ * @ngdoc function
+ * @name ng.animator#enter
+ * @methodOf ng.$animator
+ * @function
+ *
+ * @description
+ * Injects the element object into the DOM (inside of the parent element) and then runs the enter animation.
+ *
+ * @param {jQuery/jqLite element} element the element that will be the focus of the enter animation
+ * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation
+ * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation
+ */
+ animator.enter = animateActionFactory('enter', insert, noop);
+
+ /**
+ * @ngdoc function
+ * @name ng.animator#leave
+ * @methodOf ng.$animator
+ * @function
+ *
+ * @description
+ * Runs the leave animation operation and, upon completion, removes the element from the DOM.
+ *
+ * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation
+ * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the leave animation
+ */
+ animator.leave = animateActionFactory('leave', noop, remove);
+
+ /**
+ * @ngdoc function
+ * @name ng.animator#move
+ * @methodOf ng.$animator
+ * @function
+ *
+ * @description
+ * Fires the move DOM operation. Just before the animation starts, the animator will either append it into the parent container or
+ * add the element directly after the after element if present. Then the move animation will be run.
+ *
+ * @param {jQuery/jqLite element} element the element that will be the focus of the move animation
+ * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation
+ * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation
+ */
+ animator.move = animateActionFactory('move', move, noop);
+
+ /**
+ * @ngdoc function
+ * @name ng.animator#show
+ * @methodOf ng.$animator
+ * @function
+ *
+ * @description
+ * Reveals the element by setting the CSS property `display` to `block` and then starts the show animation directly after.
+ *
+ * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden
+ */
+ animator.show = animateActionFactory('show', show, noop);
+
+ /**
+ * @ngdoc function
+ * @name ng.animator#hide
+ * @methodOf ng.$animator
+ *
+ * @description
+ * Starts the hide animation first and sets the CSS `display` property to `none` upon completion.
+ *
+ * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden
+ */
+ animator.hide = animateActionFactory('hide', noop, hide);
+ return animator;
+
+ function animateActionFactory(type, beforeFn, afterFn) {
+ var ngAnimateValue = ngAnimateAttr && scope.$eval(ngAnimateAttr);
+ var className = ngAnimateAttr
+ ? isObject(ngAnimateValue) ? ngAnimateValue[type] : ngAnimateValue + '-' + type
+ : '';
+ var animationPolyfill = $animation(className);
+
+ var polyfillSetup = animationPolyfill && animationPolyfill.setup;
+ var polyfillStart = animationPolyfill && animationPolyfill.start;
+
+ if (!className) {
+ return function(element, parent, after) {
+ beforeFn(element, parent, after);
+ afterFn(element, parent, after);
+ }
+ } else {
+ var setupClass = className + '-setup';
+ var startClass = className + '-start';
+
+ return function(element, parent, after) {
+ if (!$sniffer.supportsTransitions && !polyfillSetup && !polyfillStart) {
+ beforeFn(element, parent, after);
+ afterFn(element, parent, after);
+ return;
+ }
+
+ element.addClass(setupClass);
+ beforeFn(element, parent, after);
+ if (element.length == 0) return done();
+
+ var memento = (noop || polyfillSetup)(element);
+
+ // $window.setTimeout(beginAnimation, 0); this was causing the element not to animate
+ // keep at 1 for animation dom rerender
+ $window.setTimeout(beginAnimation, 1);
+
+ function beginAnimation() {
+ element.addClass(startClass);
+ if (polyfillStart) {
+ polyfillStart(element, done, memento);
+ } else if (isFunction($window.getComputedStyle)) {
+ var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition';
+ var w3cTransitionProp = 'transition'; //one day all browsers will have this
+
+ var durationKey = 'Duration';
+ var duration = 0;
+ //we want all the styles defined before and after
+ forEach(element, function(element) {
+ var globalStyles = $window.getComputedStyle(element) || {};
+ duration = Math.max(
+ parseFloat(globalStyles[w3cTransitionProp + durationKey]) ||
+ parseFloat(globalStyles[vendorTransitionProp + durationKey]) ||
+ 0,
+ duration);
+ });
+
+ $window.setTimeout(done, duration * 1000);
+ } else {
+ dump(3)
+ done();
+ }
+ }
+
+ function done() {
+ afterFn(element, parent, after);
+ element.removeClass(setupClass);
+ element.removeClass(startClass);
+ }
+ }
+ }
+ }
+ }
+
+ function show(element) {
+ element.css('display', 'block');
+ }
+
+ function hide(element) {
+ element.css('display', 'none');
+ }
+
+ function insert(element, parent, after) {
+ if (after) {
+ after.after(element);
+ } else {
+ parent.append(element);
+ }
+ }
+
+ function remove(element) {
+ element.remove();
+ }
+
+ function move(element, parent, after) {
+ // Do not remove element before insert. Removing will cause data associated with the
+ // element to be dropped. Insert will implicitly do the remove.
+ insert(element, parent, after);
+ }
+ }];
+};
diff --git a/src/ng/directive/ngInclude.js b/src/ng/directive/ngInclude.js
index d4eacbe3..a385d00b 100644
--- a/src/ng/directive/ngInclude.js
+++ b/src/ng/directive/ngInclude.js
@@ -12,6 +12,13 @@
* (e.g. ngInclude won't work for cross-domain requests on all browsers and for
* file:// access on some browsers).
*
+ * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**
+ * and **leave** effects.
+ *
+ * @animations
+ * enter - happens just after the ngInclude contents change and a new DOM element is created and injected into the ngInclude container
+ * leave - happens just after the ngInclude contents change and just before the former contents are removed from the DOM
+ *
* @scope
*
* @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant,
@@ -78,8 +85,8 @@
* @description
* Emitted every time the ngInclude content is reloaded.
*/
-var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile',
- function($http, $templateCache, $anchorScroll, $compile) {
+var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animator',
+ function($http, $templateCache, $anchorScroll, $compile, $animator) {
return {
restrict: 'ECA',
terminal: true,
@@ -88,7 +95,8 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
onloadExp = attr.onload || '',
autoScrollExp = attr.autoscroll;
- return function(scope, element) {
+ return function(scope, element, attr) {
+ var animate = $animator(scope, attr);
var changeCounter = 0,
childScope;
@@ -97,8 +105,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
childScope.$destroy();
childScope = null;
}
-
- element.html('');
+ animate.leave(element.contents(), element);
};
scope.$watch(srcExp, function ngIncludeWatchAction(src) {
@@ -110,9 +117,12 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
if (childScope) childScope.$destroy();
childScope = scope.$new();
+ animate.leave(element.contents(), element);
- element.html(response);
- $compile(element.contents())(childScope);
+ var contents = jqLite('<div/>').html(response).contents();
+
+ animate.enter(contents, element);
+ $compile(contents)(childScope);
if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
$anchorScroll();
@@ -123,7 +133,9 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile'
}).error(function() {
if (thisChangeId === changeCounter) clearContent();
});
- } else clearContent();
+ } else {
+ clearContent();
+ }
});
};
}
diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js
index a00bc9e4..fada0696 100644
--- a/src/ng/directive/ngRepeat.js
+++ b/src/ng/directive/ngRepeat.js
@@ -16,6 +16,13 @@
* * `$middle` – `{boolean}` – true if the repeated element is between the first and last in the iterator.
* * `$last` – `{boolean}` – true if the repeated element is last in the iterator.
*
+ * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**,
+ * **leave** and **move** effects.
+ *
+ * @animations
+ * enter - when a new item is added to the list or when an item is revealed after a filter
+ * leave - when an item is removed from the list or when an item is filtered out
+ * move - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
*
* @element ANY
* @scope
@@ -75,13 +82,15 @@
</doc:scenario>
</doc:example>
*/
-var ngRepeatDirective = ['$parse', function($parse) {
+var ngRepeatDirective = ['$parse', '$animator', function($parse, $animator) {
+ var NG_REMOVED = '$$NG_REMOVED';
return {
transclude: 'element',
priority: 1000,
terminal: true,
compile: function(element, attr, linker) {
return function($scope, $element, $attr){
+ var animate = $animator($scope, $attr);
var expression = $attr.ngRepeat;
var match = expression.match(/^\s*(.+)\s+in\s+(.*?)\s*(\s+track\s+by\s+(.+)\s*)?$/),
trackByExp, hashExpFn, trackByIdFn, lhs, rhs, valueIdentifier, keyIdentifier,
@@ -130,6 +139,7 @@ var ngRepeatDirective = ['$parse', function($parse) {
$scope.$watchCollection(rhs, function ngRepeatAction(collection){
var index, length,
cursor = $element, // current position of the node
+ nextCursor,
// Same as lastBlockMap but it has the current state. It will become the
// lastBlockMap on the next iteration.
nextBlockMap = {},
@@ -184,7 +194,8 @@ var ngRepeatDirective = ['$parse', function($parse) {
for (key in lastBlockMap) {
if (lastBlockMap.hasOwnProperty(key)) {
block = lastBlockMap[key];
- block.element.remove();
+ animate.leave(block.element);
+ block.element[0][NG_REMOVED] = true;
block.scope.$destroy();
}
}
@@ -200,12 +211,17 @@ var ngRepeatDirective = ['$parse', function($parse) {
// associated scope/element
childScope = block.scope;
- if (block.element == cursor) {
+ nextCursor = cursor[0];
+ do {
+ nextCursor = nextCursor.nextSibling;
+ } while(nextCursor && nextCursor[NG_REMOVED]);
+
+ if (block.element[0] == nextCursor) {
// do nothing
cursor = block.element;
} else {
// existing item which got moved
- cursor.after(block.element);
+ animate.move(block.element, null, cursor);
cursor = block.element;
}
} else {
@@ -221,8 +237,8 @@ var ngRepeatDirective = ['$parse', function($parse) {
childScope.$middle = !(childScope.$first || childScope.$last);
if (!block.element) {
- linker(childScope, function(clone){
- cursor.after(clone);
+ linker(childScope, function(clone) {
+ animate.enter(clone, null, cursor);
cursor = clone;
block.scope = childScope;
block.element = clone;
@@ -236,3 +252,4 @@ var ngRepeatDirective = ['$parse', function($parse) {
}
};
}];
+
diff --git a/src/ng/directive/ngShowHide.js b/src/ng/directive/ngShowHide.js
index 74195468..418a43ff 100644
--- a/src/ng/directive/ngShowHide.js
+++ b/src/ng/directive/ngShowHide.js
@@ -6,7 +6,18 @@
*
* @description
* The `ngShow` and `ngHide` directives show or hide a portion of the DOM tree (HTML)
- * conditionally.
+ * conditionally based on **"truthy"** values evaluated within an {expression}. In other
+ * words, if the expression assigned to **ngShow evaluates to a true value** then **the element is set to visible**
+ * (via `display:block` in css) and **if false** then **the element is set to hidden** (so display:none).
+ * With ngHide this is the reverse whereas true values cause the element itself to become
+ * hidden.
+ *
+ * Additionally, you can also provide animations via the ngAnimate attribute to animate the **show**
+ * and **hide** effects.
+ *
+ * @animations
+ * show - happens after the ngShow expression evaluates to a truthy value and the contents are set to visible
+ * hide - happens before the ngShow expression evaluates to a non truthy value and just before the contents are set to hidden
*
* @element ANY
* @param {expression} ngShow If the {@link guide/expression expression} is truthy
@@ -33,11 +44,14 @@
</doc:example>
*/
//TODO(misko): refactor to remove element from the DOM
-var ngShowDirective = ngDirective(function(scope, element, attr){
- scope.$watch(attr.ngShow, function ngShowWatchAction(value){
- element.css('display', toBoolean(value) ? '' : 'none');
- });
-});
+var ngShowDirective = ['$animator', function($animator) {
+ return function(scope, element, attr) {
+ var animate = $animator(scope, attr);
+ scope.$watch(attr.ngShow, function ngShowWatchAction(value){
+ animate[toBoolean(value) ? 'show' : 'hide'](element);
+ });
+ };
+}];
/**
@@ -45,8 +59,19 @@ var ngShowDirective = ngDirective(function(scope, element, attr){
* @name ng.directive:ngHide
*
* @description
- * The `ngHide` and `ngShow` directives hide or show a portion of the DOM tree (HTML)
- * conditionally.
+ * The `ngShow` and `ngHide` directives show or hide a portion of the DOM tree (HTML)
+ * conditionally based on **"truthy"** values evaluated within an {expression}. In other
+ * words, if the expression assigned to **ngShow evaluates to a true value** then **the element is set to visible**
+ * (via `display:block` in css) and **if false** then **the element is set to hidden** (so display:none).
+ * With ngHide this is the reverse whereas true values cause the element itself to become
+ * hidden.
+ *
+ * Additionally, you can also provide animations via the ngAnimate attribute to animate the **show**
+ * and **hide** effects.
+ *
+ * @animations
+ * show - happens after the ngHide expression evaluates to a non truthy value and the contents are set to visible
+ * hide - happens after the ngHide expression evaluates to a truthy value and just before the contents are set to hidden
*
* @element ANY
* @param {expression} ngHide If the {@link guide/expression expression} is truthy then
@@ -73,8 +98,11 @@ var ngShowDirective = ngDirective(function(scope, element, attr){
</doc:example>
*/
//TODO(misko): refactor to remove element from the DOM
-var ngHideDirective = ngDirective(function(scope, element, attr){
- scope.$watch(attr.ngHide, function ngHideWatchAction(value){
- element.css('display', toBoolean(value) ? 'none' : '');
- });
-});
+var ngHideDirective = ['$animator', function($animator) {
+ return function(scope, element, attr) {
+ var animate = $animator(scope, attr);
+ scope.$watch(attr.ngHide, function ngHideWatchAction(value){
+ animate[toBoolean(value) ? 'hide' : 'show'](element);
+ });
+ };
+}];
diff --git a/src/ng/directive/ngSwitch.js b/src/ng/directive/ngSwitch.js
index f22e634d..8b0dab31 100644
--- a/src/ng/directive/ngSwitch.js
+++ b/src/ng/directive/ngSwitch.js
@@ -6,15 +6,30 @@
* @restrict EA
*
* @description
- * Conditionally change the DOM structure. Elements within ngSwitch but without
- * ngSwitchWhen or ngSwitchDefault directives will be preserved at the location
- * as specified in the template
+ * The ngSwitch directive is used to conditionally swap DOM structure on your template based on a scope expression.
+ * Elements within ngSwitch but without ngSwitchWhen or ngSwitchDefault directives will be preserved at the location
+ * as specified in the template.
+ *
+ * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it
+ * from the template cache), ngSwitch simply choses one of the nested elements and makes it visible based on which element
+ * matches the value obtained from the evaluated expression. In other words, you define a container element
+ * (where you place the directive), place an expression on the **on="..." attribute**
+ * (or the **ng-switch="..." attribute**), define any inner elements inside of the directive and place
+ * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on
+ * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default
+ * attribute is displayed.
+ *
+ * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**
+ * and **leave** effects.
+ *
+ * @animations
+ * enter - happens after the ngSwtich contents change and the matched child element is placed inside the container
+ * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM
*
* @usage
* <ANY ng-switch="expression">
* <ANY ng-switch-when="matchValue1">...</ANY>
* <ANY ng-switch-when="matchValue2">...</ANY>
- * ...
* <ANY ng-switch-default>...</ANY>
* </ANY>
*
@@ -67,43 +82,48 @@
</doc:scenario>
</doc:example>
*/
-var NG_SWITCH = 'ng-switch';
-var ngSwitchDirective = valueFn({
- restrict: 'EA',
- require: 'ngSwitch',
- // asks for $scope to fool the BC controller module
- controller: ['$scope', function ngSwitchController() {
- this.cases = {};
- }],
- link: function(scope, element, attr, ctrl) {
- var watchExpr = attr.ngSwitch || attr.on,
- selectedTranscludes,
- selectedElements,
- selectedScopes = [];
+var ngSwitchDirective = ['$animator', function($animator) {
+ return {
+ restrict: 'EA',
+ require: 'ngSwitch',
- scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
- for (var i= 0, ii=selectedScopes.length; i<ii; i++) {
- selectedScopes[i].$destroy();
- selectedElements[i].remove();
- }
+ // asks for $scope to fool the BC controller module
+ controller: ['$scope', function ngSwitchController() {
+ this.cases = {};
+ }],
+ link: function(scope, element, attr, ngSwitchController) {
+ var animate = $animator(scope, attr);
+ var watchExpr = attr.ngSwitch || attr.on,
+ selectedTranscludes,
+ selectedElements,
+ selectedScopes = [];
- selectedElements = [];
- selectedScopes = [];
+ scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
+ for (var i= 0, ii=selectedScopes.length; i<ii; i++) {
+ selectedScopes[i].$destroy();
+ animate.leave(selectedElements[i]);
+ }
- if ((selectedTranscludes = ctrl.cases['!' + value] || ctrl.cases['?'])) {
- scope.$eval(attr.change);
- forEach(selectedTranscludes, function(selectedTransclude) {
- var selectedScope = scope.$new();
- selectedScopes.push(selectedScope);
- selectedTransclude.transclude(selectedScope, function(caseElement) {
- selectedElements.push(caseElement);
- selectedTransclude.element.after(caseElement);
+ selectedElements = [];
+ selectedScopes = [];
+
+ if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) {
+ scope.$eval(attr.change);
+ forEach(selectedTranscludes, function(selectedTransclude) {
+ var selectedScope = scope.$new();
+ selectedScopes.push(selectedScope);
+ selectedTransclude.transclude(selectedScope, function(caseElement) {
+ var anchor = selectedTransclude.element;
+
+ selectedElements.push(caseElement);
+ animate.enter(caseElement, anchor.parent(), anchor);
+ });
});
- });
- }
- });
+ }
+ });
+ }
}
-});
+}];
var ngSwitchWhenDirective = ngDirective({
transclude: 'element',
diff --git a/src/ng/directive/ngView.js b/src/ng/directive/ngView.js
index 6e92c2d8..2ffd64da 100644
--- a/src/ng/directive/ngView.js
+++ b/src/ng/directive/ngView.js
@@ -12,6 +12,13 @@
* Every time the current route changes, the included view changes with it according to the
* configuration of the `$route` service.
*
+ * Additionally, you can also provide animations via the ngAnimate attribute to animate the **enter**
+ * and **leave** effects.
+ *
+ * @animations
+ * enter - happens just after the ngView contents are changed (when the new view DOM element is inserted into the DOM)
+ * leave - happens just after the current ngView contents change and just before the former contents are removed from the DOM
+ *
* @scope
* @example
<example module="ngView">
@@ -105,15 +112,16 @@
* Emitted every time the ngView content is reloaded.
*/
var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile',
- '$controller',
+ '$controller', '$animator',
function($http, $templateCache, $route, $anchorScroll, $compile,
- $controller) {
+ $controller, $animator) {
return {
restrict: 'ECA',
terminal: true,
link: function(scope, element, attr) {
var lastScope,
- onloadExp = attr.onload || '';
+ onloadExp = attr.onload || '',
+ animate = $animator(scope, attr);
scope.$on('$routeChangeSuccess', update);
update();
@@ -127,7 +135,7 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c
}
function clearContent() {
- element.html('');
+ animate.leave(element.contents(), element);
destroyLastScope();
}
@@ -136,8 +144,8 @@ var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$c
template = locals && locals.$template;
if (template) {
- element.html(template);
- destroyLastScope();
+ clearContent();
+ animate.enter(jqLite('<div></div>').html(template).contents(), element);
var link = $compile(element.contents()),
current = $route.current,
diff --git a/src/ng/sniffer.js b/src/ng/sniffer.js
index 9342fbd5..19877b87 100644
--- a/src/ng/sniffer.js
+++ b/src/ng/sniffer.js
@@ -9,6 +9,7 @@
*
* @property {boolean} history Does the browser support html5 history api ?
* @property {boolean} hashchange Does the browser support hashchange event ?
+ * @property {boolean} supportsTransitions Does the browser support CSS transition events ?
*
* @description
* This is very simple implementation of testing browser's features.
@@ -16,8 +17,25 @@
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
- android = int((/android (\d+)/.exec(lowercase($window.navigator.userAgent)) || [])[1]),
- document = $document[0];
+ android = int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
+ document = $document[0] || {},
+ vendorPrefix,
+ vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/,
+ bodyStyle = document.body && document.body.style,
+ transitions = false,
+ match;
+
+ if (bodyStyle) {
+ for(var prop in bodyStyle) {
+ if(match = vendorRegex.exec(prop)) {
+ vendorPrefix = match[0];
+ vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1);
+ break;
+ }
+ }
+ transitions = !!(vendorPrefix + 'Transition' in bodyStyle);
+ }
+
return {
// Android has history.pushState, but it does not update location correctly
@@ -41,7 +59,9 @@ function $SnifferProvider() {
return eventSupport[event];
},
- csp: document.securityPolicy ? document.securityPolicy.isActive : false
+ csp: document.securityPolicy ? document.securityPolicy.isActive : false,
+ vendorPrefix: vendorPrefix,
+ supportsTransitions : transitions
};
}];
}
diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js
index 8c5686ac..1f96c1e5 100644
--- a/src/ngMock/angular-mocks.js
+++ b/src/ngMock/angular-mocks.js
@@ -587,6 +587,57 @@ angular.mock.$LogProvider = function() {
angular.mock.TzDate.prototype = Date.prototype;
})();
+/**
+ * @ngdoc function
+ * @name angular.mock.createMockWindow
+ * @description
+ *
+ * This function creates a mock window object useful for controlling access ot setTimeout, but mocking out
+ * sufficient window's properties to allow Angular to execute.
+ *
+ * @example
+ *
+ * <pre>
+ beforeEach(module(function($provide) {
+ $provide.value('$window', window = angular.mock.createMockWindow());
+ }));
+
+ it('should do something', inject(function($window) {
+ var val = null;
+ $window.setTimeout(function() { val = 123; }, 10);
+ expect(val).toEqual(null);
+ window.setTimeout.expect(10).process();
+ expect(val).toEqual(123);
+ });
+ * </pre>
+ *
+ */
+angular.mock.createMockWindow = function() {
+ var mockWindow = {};
+ var setTimeoutQueue = [];
+
+ mockWindow.document = window.document;
+ mockWindow.getComputedStyle = angular.bind(window, window.getComputedStyle);
+ mockWindow.scrollTo = angular.bind(window, window.scrollTo);
+ mockWindow.navigator = window.navigator;
+ mockWindow.setTimeout = function(fn, delay) {
+ setTimeoutQueue.push({fn: fn, delay: delay});
+ };
+ mockWindow.setTimeout.queue = setTimeoutQueue;
+ mockWindow.setTimeout.expect = function(delay) {
+ if (setTimeoutQueue.length > 0) {
+ return {
+ process: function() {
+ setTimeoutQueue.shift().fn();
+ }
+ };
+ } else {
+ expect('SetTimoutQueue empty. Expecting delay of ').toEqual(delay);
+ }
+ };
+
+ return mockWindow;
+};
/**
* @ngdoc function