From fc7d2d273748847ef6ce69df2dd522419aee7d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 16 Jul 2015 16:06:50 -0700 Subject: [PATCH] chore($$forceReflow): create service for issuing reflows in animations --- angularFiles.js | 1 + src/AngularPublic.js | 2 ++ src/ng/forceReflow.js | 24 +++++++++++++++++ src/ngMock/angular-mocks.js | 16 ++++++++++-- test/ng/forceReflowSpec.js | 52 +++++++++++++++++++++++++++++++++++++ 5 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/ng/forceReflow.js create mode 100644 test/ng/forceReflowSpec.js diff --git a/angularFiles.js b/angularFiles.js index 524b53c49929..59bed0b138cd 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -21,6 +21,7 @@ var angularFiles = { 'src/ng/controller.js', 'src/ng/document.js', 'src/ng/exceptionHandler.js', + 'src/ng/forceReflow.js', 'src/ng/http.js', 'src/ng/httpBackend.js', 'src/ng/interpolate.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 44f9932ece39..78901de03cda 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -63,6 +63,7 @@ $DocumentProvider, $ExceptionHandlerProvider, $FilterProvider, + $$ForceReflowProvider, $InterpolateProvider, $IntervalProvider, $$HashMapProvider, @@ -219,6 +220,7 @@ function publishExternalAPI(angular) { $document: $DocumentProvider, $exceptionHandler: $ExceptionHandlerProvider, $filter: $FilterProvider, + $$forceReflow: $$ForceReflowProvider, $interpolate: $InterpolateProvider, $interval: $IntervalProvider, $http: $HttpProvider, diff --git a/src/ng/forceReflow.js b/src/ng/forceReflow.js new file mode 100644 index 000000000000..deca55f76a2f --- /dev/null +++ b/src/ng/forceReflow.js @@ -0,0 +1,24 @@ +'use strict'; + +var $$ForceReflowProvider = function() { + this.$get = ['$document', function($document) { + return function(domNode) { + //the line below will force the browser to perform a repaint so + //that all the animated elements within the animation frame will + //be properly updated and drawn on screen. This is required to + //ensure that the preparation animation is properly flushed so that + //the active state picks up from there. DO NOT REMOVE THIS LINE. + //DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH + //WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND + //WILL TAKE YEARS AWAY FROM YOUR LIFE. + if (domNode) { + if (!domNode.nodeType && domNode instanceof jqLite) { + domNode = domNode[0]; + } + } else { + domNode = $document[0].body; + } + return domNode.offsetWidth + 1; + }; + }]; +}; diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 5d1b8b06b54d..12fb19c40b31 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -755,11 +755,23 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) .config(['$provide', function($provide) { - $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', - function($delegate, $timeout, $browser, $$rAF) { + $provide.factory('$$forceReflow', function() { + function reflowFn() { + reflowFn.totalReflows++; + } + reflowFn.totalReflows = 0; + return reflowFn; + }); + + $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', '$$forceReflow', + function($delegate, $timeout, $browser, $$rAF, $$forceReflow) { + var animate = { queue: [], cancel: $delegate.cancel, + get reflows() { + return $$forceReflow.totalReflows; + }, enabled: $delegate.enabled, triggerCallbackEvents: function() { $$rAF.flush(); diff --git a/test/ng/forceReflowSpec.js b/test/ng/forceReflowSpec.js new file mode 100644 index 000000000000..6a1885791635 --- /dev/null +++ b/test/ng/forceReflowSpec.js @@ -0,0 +1,52 @@ +'use strict'; + +describe('$$forceReflow', function() { + it('should issue a reflow by touching the `document.body.client` when no param is provided', function() { + module(function($provide) { + var doc = jqLite('
'); + doc[0].body = {}; + doc[0].body.offsetWidth = 10; + $provide.value('$document', doc); + }); + inject(function($$forceReflow) { + var value = $$forceReflow(); + expect(value).toBe(11); + }); + }); + + it('should issue a reflow by touching the `domNode.offsetWidth` when a domNode param is provided', + inject(function($$forceReflow) { + + var elm = {}; + elm.offsetWidth = 100; + expect($$forceReflow(elm)).toBe(101); + })); + + it('should issue a reflow by touching the `jqLiteNode[0].offsetWidth` when a jqLite node param is provided', + inject(function($$forceReflow) { + + var elm = {}; + elm.offsetWidth = 200; + elm = jqLite(elm); + expect($$forceReflow(elm)).toBe(201); + })); + + describe('$animate with ngAnimateMock', function() { + beforeEach(module('ngAnimateMock')); + + it('should keep track of how many reflows have been issued', + inject(function($$forceReflow, $animate) { + + var elm = {}; + elm.offsetWidth = 10; + + expect($animate.reflows).toBe(0); + + $$forceReflow(elm); + $$forceReflow(elm); + $$forceReflow(elm); + + expect($animate.reflows).toBe(3); + })); + }); +});