Skip to content
This repository has been archived by the owner on May 29, 2019. It is now read-only.

Commit

Permalink
feat(collapse): add callback hooks
Browse files Browse the repository at this point in the history
- Adds support for collapsing, collapsed, expanding, and expanded
  callbacks

Closes #5194
Closes #5226
  • Loading branch information
nonplus authored and wesleycho committed Jan 14, 2016
1 parent e8201d1 commit 446364a
Show file tree
Hide file tree
Showing 3 changed files with 199 additions and 41 deletions.
87 changes: 50 additions & 37 deletions src/collapse/collapse.js
Original file line number Diff line number Diff line change
@@ -1,73 +1,86 @@
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')
.css({height: 'auto'});
}

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() {
if (!element.hasClass('collapse') && !element.hasClass('in')) {
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) {
Expand Down
20 changes: 20 additions & 0 deletions src/collapse/docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,23 @@
<i class="glyphicon glyphicon-eye-open"></i>
_(Default: `false`)_ -
Whether the element should be collapsed or not.

* `collapsing()`
<small class="badge">$</small> -
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()`
<small class="badge">$</small> -
An optional expression called after the element finished collapsing.

* `expanding()`
<small class="badge">$</small> -
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()`
<small class="badge">$</small> -
An optional expression called after the element finished expanding.
133 changes: 129 additions & 4 deletions src/collapse/test/collapse.spec.js
Original file line number Diff line number Diff line change
@@ -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('<div uib-collapse="isCollapsed">Some Content</div>');
element = angular.element(
'<div uib-collapse="isCollapsed" '
+ 'expanding="expanding()" '
+ 'expanded="expanded()" '
+ 'collapsing="collapsing()" '
+ 'collapsed="collapsed()">'
+ 'Some Content</div>');
compileFn = $compile(element);
angular.element(document.body).append(element);
});
Expand All @@ -19,30 +26,53 @@ 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() {
scope.isCollapsed = false;
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() {
Expand All @@ -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();
Expand All @@ -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() {
Expand Down Expand Up @@ -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);
});
});

});

0 comments on commit 446364a

Please sign in to comment.