diff --git a/angularFiles.js b/angularFiles.js index d5cd891c9240..f40b03ec241c 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -14,6 +14,7 @@ var angularFiles = { 'src/ng/anchorScroll.js', 'src/ng/animate.js', + 'src/ng/animateCss.js', 'src/ng/browser.js', 'src/ng/cacheFactory.js', 'src/ng/compile.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 78901de03cda..c12403838bfe 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -55,6 +55,7 @@ $AnchorScrollProvider, $AnimateProvider, + $CoreAnimateCssProvider, $$CoreAnimateQueueProvider, $$CoreAnimateRunnerProvider, $BrowserProvider, @@ -212,6 +213,7 @@ function publishExternalAPI(angular) { $provide.provider({ $anchorScroll: $AnchorScrollProvider, $animate: $AnimateProvider, + $animateCss: $CoreAnimateCssProvider, $$animateQueue: $$CoreAnimateQueueProvider, $$AnimateRunner: $$CoreAnimateRunnerProvider, $browser: $BrowserProvider, diff --git a/src/ng/animateCss.js b/src/ng/animateCss.js new file mode 100644 index 000000000000..3da66f3d1def --- /dev/null +++ b/src/ng/animateCss.js @@ -0,0 +1,84 @@ +'use strict'; + +/** + * @ngdoc service + * @name $animateCss + * @kind object + * + * @description + * This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included, + * then the `$animateCss` service will actually perform animations. + * + * Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}. + */ +var $CoreAnimateCssProvider = function() { + this.$get = ['$$rAF', '$q', function($$rAF, $q) { + + var RAFPromise = function() {}; + RAFPromise.prototype = { + done: function(cancel) { + this.defer && this.defer[cancel === true ? 'reject' : 'resolve'](); + }, + end: function() { + this.done(); + }, + cancel: function() { + this.done(true); + }, + getPromise: function() { + if (!this.defer) { + this.defer = $q.defer(); + } + return this.defer.promise; + }, + then: function(f1,f2) { + return this.getPromise().then(f1,f2); + }, + 'catch': function(f1) { + return this.getPromise().catch(f1); + }, + 'finally': function(f1) { + return this.getPromise().finally(f1); + } + }; + + return function(element, options) { + if (options.from) { + element.css(options.from); + options.from = null; + } + + var closed, runner = new RAFPromise(); + return { + start: run, + end: run + }; + + function run() { + $$rAF(function() { + close(); + if (!closed) { + runner.done(); + } + closed = true; + }); + return runner; + } + + function close() { + if (options.addClass) { + element.addClass(options.addClass); + options.addClass = null; + } + if (options.removeClass) { + element.removeClass(options.removeClass); + options.removeClass = null; + } + if (options.to) { + element.css(options.to); + options.to = null; + } + } + }; + }]; +}; diff --git a/test/ng/animateCssSpec.js b/test/ng/animateCssSpec.js new file mode 100644 index 000000000000..52b697ba2834 --- /dev/null +++ b/test/ng/animateCssSpec.js @@ -0,0 +1,120 @@ +'use strict'; + +describe("$animateCss", function() { + + var triggerRAF, element; + beforeEach(inject(function($$rAF, $rootElement, $document) { + triggerRAF = function() { + $$rAF.flush(); + }; + + var body = jqLite($document[0].body); + element = jqLite('
'); + $rootElement.append(element); + body.append($rootElement); + })); + + describe("without animation", function() { + + it("should apply the provided [from] CSS to the element", inject(function($animateCss) { + $animateCss(element, { from: { height: '50px' }}).start(); + expect(element.css('height')).toBe('50px'); + })); + + it("should apply the provided [to] CSS to the element after the first frame", inject(function($animateCss) { + $animateCss(element, { to: { width: '50px' }}).start(); + expect(element.css('width')).not.toBe('50px'); + triggerRAF(); + expect(element.css('width')).toBe('50px'); + })); + + it("should apply the provided [addClass] CSS classes to the element after the first frame", inject(function($animateCss) { + $animateCss(element, { addClass: 'golden man' }).start(); + expect(element).not.toHaveClass('golden man'); + triggerRAF(); + expect(element).toHaveClass('golden man'); + })); + + it("should apply the provided [removeClass] CSS classes to the element after the first frame", inject(function($animateCss) { + element.addClass('silver'); + $animateCss(element, { removeClass: 'silver dude' }).start(); + expect(element).toHaveClass('silver'); + triggerRAF(); + expect(element).not.toHaveClass('silver'); + })); + + it("should return an animator with a start method which returns a promise", inject(function($animateCss) { + var promise = $animateCss(element, { addClass: 'cool' }).start(); + expect(isPromiseLike(promise)).toBe(true); + })); + + it("should return an animator with an end method which returns a promise", inject(function($animateCss) { + var promise = $animateCss(element, { addClass: 'cool' }).end(); + expect(isPromiseLike(promise)).toBe(true); + })); + + it("should only resolve the promise once both a digest and RAF have passed after start", + inject(function($animateCss, $rootScope) { + + var doneSpy = jasmine.createSpy(); + var runner = $animateCss(element, { addClass: 'cool' }).start(); + + runner.then(doneSpy); + expect(doneSpy).not.toHaveBeenCalled(); + + triggerRAF(); + expect(doneSpy).not.toHaveBeenCalled(); + + $rootScope.$digest(); + expect(doneSpy).toHaveBeenCalled(); + })); + + it("should resolve immediately if runner.end() is called", + inject(function($animateCss, $rootScope) { + + var doneSpy = jasmine.createSpy(); + var runner = $animateCss(element, { addClass: 'cool' }).start(); + + runner.then(doneSpy); + runner.end(); + expect(doneSpy).not.toHaveBeenCalled(); + + $rootScope.$digest(); + expect(doneSpy).toHaveBeenCalled(); + })); + + it("should reject immediately if runner.end() is called", + inject(function($animateCss, $rootScope) { + + var cancelSpy = jasmine.createSpy(); + var runner = $animateCss(element, { addClass: 'cool' }).start(); + + runner.catch(cancelSpy); + runner.cancel(); + expect(cancelSpy).not.toHaveBeenCalled(); + + $rootScope.$digest(); + expect(cancelSpy).toHaveBeenCalled(); + })); + + it("should not resolve after the next frame if the runner has already been cancelled", + inject(function($animateCss, $rootScope) { + + var doneSpy = jasmine.createSpy(); + var cancelSpy = jasmine.createSpy(); + var runner = $animateCss(element, { addClass: 'cool' }).start(); + + runner.then(doneSpy, cancelSpy); + runner.cancel(); + + $rootScope.$digest(); + expect(cancelSpy).toHaveBeenCalled(); + expect(doneSpy).not.toHaveBeenCalled(); + + triggerRAF(); + expect(cancelSpy).toHaveBeenCalled(); + expect(doneSpy).not.toHaveBeenCalled(); + })); + }); + +});