diff --git a/src/collapse/collapse.js b/src/collapse/collapse.js index dda4b14a6b..9003882b1e 100644 --- a/src/collapse/collapse.js +++ b/src/collapse/collapse.js @@ -1,9 +1,14 @@ angular.module('ui.bootstrap.collapse', []) - .directive('uibCollapse', ['$animate', '$injector', function($animate, $injector) { + .directive('uibCollapse', ['$animate', '$q', '$parse', '$injector', function($animate, $q, $parse, $injector) { var $animateCss = $injector.has('$animateCss') ? $injector.get('$animateCss') : null; return { link: function(scope, element, attrs) { + var expandingExpr = $parse(attrs.expanding), + expandedExpr = $parse(attrs.expanded), + collapsingExpr = $parse(attrs.collapsing), + collapsedExpr = $parse(attrs.collapsed); + if (!scope.$eval(attrs.uibCollapse)) { element.addClass('in') .addClass('collapse') @@ -11,28 +16,32 @@ angular.module('ui.bootstrap.collapse', []) } function expand() { - element.removeClass('collapse') - .addClass('collapsing') - .attr('aria-expanded', true) - .attr('aria-hidden', false); + $q.resolve(expandingExpr(scope)) + .then(function() { + element.removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', true) + .attr('aria-hidden', false); - if ($animateCss) { - $animateCss(element, { - addClass: 'in', - easing: 'ease', - to: { height: element[0].scrollHeight + 'px' } - }).start()['finally'](expandDone); - } else { - $animate.addClass(element, 'in', { - to: { height: element[0].scrollHeight + 'px' } - }).then(expandDone); - } + if ($animateCss) { + $animateCss(element, { + addClass: 'in', + easing: 'ease', + to: { height: element[0].scrollHeight + 'px' } + }).start()['finally'](expandDone); + } else { + $animate.addClass(element, 'in', { + to: { height: element[0].scrollHeight + 'px' } + }).then(expandDone); + } + }); } function expandDone() { element.removeClass('collapsing') .addClass('collapse') .css({height: 'auto'}); + expandedExpr(scope); } function collapse() { @@ -40,34 +49,38 @@ angular.module('ui.bootstrap.collapse', []) return collapseDone(); } - element - // IMPORTANT: The height must be set before adding "collapsing" class. - // Otherwise, the browser attempts to animate from height 0 (in - // collapsing class) to the given height here. - .css({height: element[0].scrollHeight + 'px'}) - // initially all panel collapse have the collapse class, this removal - // prevents the animation from jumping to collapsed state - .removeClass('collapse') - .addClass('collapsing') - .attr('aria-expanded', false) - .attr('aria-hidden', true); + $q.resolve(collapsingExpr(scope)) + .then(function() { + element + // IMPORTANT: The height must be set before adding "collapsing" class. + // Otherwise, the browser attempts to animate from height 0 (in + // collapsing class) to the given height here. + .css({height: element[0].scrollHeight + 'px'}) + // initially all panel collapse have the collapse class, this removal + // prevents the animation from jumping to collapsed state + .removeClass('collapse') + .addClass('collapsing') + .attr('aria-expanded', false) + .attr('aria-hidden', true); - if ($animateCss) { - $animateCss(element, { - removeClass: 'in', - to: {height: '0'} - }).start()['finally'](collapseDone); - } else { - $animate.removeClass(element, 'in', { - to: {height: '0'} - }).then(collapseDone); - } + if ($animateCss) { + $animateCss(element, { + removeClass: 'in', + to: {height: '0'} + }).start()['finally'](collapseDone); + } else { + $animate.removeClass(element, 'in', { + to: {height: '0'} + }).then(collapseDone); + } + }); } function collapseDone() { element.css({height: '0'}); // Required so that collapse works when animation is disabled element.removeClass('collapsing') .addClass('collapse'); + collapsedExpr(scope); } scope.$watch(attrs.uibCollapse, function(shouldCollapse) { diff --git a/src/collapse/docs/readme.md b/src/collapse/docs/readme.md index d1b2d9d46a..30a4840811 100644 --- a/src/collapse/docs/readme.md +++ b/src/collapse/docs/readme.md @@ -7,3 +7,23 @@ _(Default: `false`)_ - Whether the element should be collapsed or not. + +* `collapsing()` + $ - + An optional expression called before the element begins collapsing. + If the expression returns a promise, animation won't start until the promise resolves. + If the returned promise is rejected, collapsing will be cancelled. + +* `collapsed()` + $ - + An optional expression called after the element finished collapsing. + +* `expanding()` + $ - + An optional expression called before the element begins expanding. + If the expression returns a promise, animation won't start until the promise resolves. + If the returned promise is rejected, expanding will be cancelled. + +* `expanded()` + $ - + An optional expression called after the element finished expanding. diff --git a/src/collapse/test/collapse.spec.js b/src/collapse/test/collapse.spec.js index 94c3b4b173..76539eb7cf 100644 --- a/src/collapse/test/collapse.spec.js +++ b/src/collapse/test/collapse.spec.js @@ -1,16 +1,23 @@ describe('collapse directive', function() { - var element, compileFn, scope, $compile, $animate; + var element, compileFn, scope, $compile, $animate, $q; beforeEach(module('ui.bootstrap.collapse')); beforeEach(module('ngAnimateMock')); - beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_) { + beforeEach(inject(function(_$rootScope_, _$compile_, _$animate_, _$q_) { scope = _$rootScope_; $compile = _$compile_; $animate = _$animate_; + $q = _$q_; })); beforeEach(function() { - element = angular.element('
Some Content
'); + element = angular.element( + '
' + + 'Some Content
'); compileFn = $compile(element); angular.element(document.body).append(element); }); @@ -19,11 +26,30 @@ describe('collapse directive', function() { element.remove(); }); + function initCallbacks() { + scope.collapsing = jasmine.createSpy('scope.collapsing'); + scope.collapsed = jasmine.createSpy('scope.collapsed'); + scope.expanding = jasmine.createSpy('scope.expanding'); + scope.expanded = jasmine.createSpy('scope.expanded'); + } + + function assertCallbacks(expected) { + ['collapsing', 'collapsed', 'expanding', 'expanded'].forEach(function(cbName) { + if (expected[cbName]) { + expect(scope[cbName]).toHaveBeenCalled(); + } else { + expect(scope[cbName]).not.toHaveBeenCalled(); + } + }); + } + it('should be hidden on initialization if isCollapsed = true', function() { + initCallbacks(); scope.isCollapsed = true; compileFn(scope); scope.$digest(); expect(element.height()).toBe(0); + assertCallbacks({ collapsed: true }); }); it('should collapse if isCollapsed = true on subsequent use', function() { @@ -31,18 +57,22 @@ describe('collapse directive', function() { compileFn(scope); scope.$digest(); $animate.flush(); + initCallbacks(); scope.isCollapsed = true; scope.$digest(); $animate.flush(); expect(element.height()).toBe(0); + assertCallbacks({ collapsing: true, collapsed: true }); }); it('should be shown on initialization if isCollapsed = false', function() { + initCallbacks(); scope.isCollapsed = false; compileFn(scope); scope.$digest(); $animate.flush(); expect(element.height()).not.toBe(0); + assertCallbacks({ expanding: true, expanded: true }); }); it('should expand if isCollapsed = false on subsequent use', function() { @@ -53,13 +83,15 @@ describe('collapse directive', function() { scope.isCollapsed = true; scope.$digest(); $animate.flush(); + initCallbacks(); scope.isCollapsed = false; scope.$digest(); $animate.flush(); expect(element.height()).not.toBe(0); + assertCallbacks({ expanding: true, expanded: true }); }); - it('should expand if isCollapsed = true on subsequent uses', function() { + it('should collapse if isCollapsed = true on subsequent uses', function() { scope.isCollapsed = false; compileFn(scope); scope.$digest(); @@ -70,10 +102,12 @@ describe('collapse directive', function() { scope.isCollapsed = false; scope.$digest(); $animate.flush(); + initCallbacks(); scope.isCollapsed = true; scope.$digest(); $animate.flush(); expect(element.height()).toBe(0); + assertCallbacks({ collapsing: true, collapsed: true }); }); it('should change aria-expanded attribute', function() { @@ -137,4 +171,95 @@ describe('collapse directive', function() { expect(element.height()).toBeLessThan(collapseHeight); }); }); + + describe('expanding callback returning a promise', function() { + var defer, collapsedHeight; + + beforeEach(function() { + defer = $q.defer(); + + scope.isCollapsed = true; + scope.expanding = function() { + return defer.promise; + }; + compileFn(scope); + scope.$digest(); + collapsedHeight = element.height(); + + // set flag to expand ... + scope.isCollapsed = false; + scope.$digest(); + + // ... shouldn't expand yet ... + expect(element.attr('aria-expanded')).not.toBe('true'); + expect(element.height()).toBe(collapsedHeight); + }); + + it('should wait for it to resolve before animating', function() { + defer.resolve(); + + // should now expand + scope.$digest(); + $animate.flush(); + + expect(element.attr('aria-expanded')).toBe('true'); + expect(element.height()).toBeGreaterThan(collapsedHeight); + }); + + it('should not animate if it rejects', function() { + defer.reject(); + + // should NOT expand + scope.$digest(); + + expect(element.attr('aria-expanded')).not.toBe('true'); + expect(element.height()).toBe(collapsedHeight); + }); + }); + + describe('collapsing callback returning a promise', function() { + var defer, expandedHeight; + + beforeEach(function() { + defer = $q.defer(); + scope.isCollapsed = false; + scope.collapsing = function() { + return defer.promise; + }; + compileFn(scope); + scope.$digest(); + + expandedHeight = element.height(); + + // set flag to collapse ... + scope.isCollapsed = true; + scope.$digest(); + + // ... but it shouldn't collapse yet ... + expect(element.attr('aria-expanded')).not.toBe('false'); + expect(element.height()).toBe(expandedHeight); + }); + + it('should wait for it to resolve before animating', function() { + defer.resolve(); + + // should now collapse + scope.$digest(); + $animate.flush(); + + expect(element.attr('aria-expanded')).toBe('false'); + expect(element.height()).toBeLessThan(expandedHeight); + }); + + it('should not animate if it rejects', function() { + defer.reject(); + + // should NOT collapse + scope.$digest(); + + expect(element.attr('aria-expanded')).not.toBe('false'); + expect(element.height()).toBe(expandedHeight); + }); + }); + });