From c4cbaf7f1feff7e6f25377274d6f55ac1c1994b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 12 Aug 2015 15:21:40 -0700 Subject: [PATCH] feat(ngAnimate): expose a core version of `$animateCss` A core version of `$animateCss` can now be injected when ngAnimate is not present. This core version doesn't trigger any animations in any way. All that it does is apply the provided from and/or to styles as well as the addClass and removeClass values. The motivation for this feature is to allow for directives to activate animations automatically when ngAnimate is included without the need to use `$animate`. Closes #12509 --- angularFiles.js | 1 + src/AngularPublic.js | 2 + src/ng/animateCss.js | 84 ++++++++++++++++++++++++++ test/ng/animateCssSpec.js | 120 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+) create mode 100644 src/ng/animateCss.js create mode 100644 test/ng/animateCssSpec.js 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(); + })); + }); + +});