Skip to content

Commit 5992f96

Browse files
committed
feat(collapse): provide collapse module
1 parent ab182ac commit 5992f96

File tree

6 files changed

+395
-0
lines changed

6 files changed

+395
-0
lines changed

src/collapse/collapse.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
'use strict';
2+
3+
angular.module('mgcrea.ngStrap.collapse', [])
4+
5+
.provider('$collapse', function() {
6+
7+
var defaults = this.defaults = {
8+
animation: 'am-collapse'
9+
};
10+
11+
var controller = this.controller = function($scope, $element, $attrs) {
12+
var self = this;
13+
14+
// Attributes options
15+
self.$options = angular.copy(defaults);
16+
angular.forEach(['animation'], function(key) {
17+
if(angular.isDefined($attrs[key])) self.$options[key] = $attrs[key];
18+
});
19+
20+
self.$toggles = [];
21+
self.$targets = [];
22+
23+
self.$viewChangeListeners = [];
24+
25+
self.$registerToggle = function(element) {
26+
self.$toggles.push(element);
27+
};
28+
self.$registerTarget = function(element) {
29+
self.$targets.push(element);
30+
};
31+
32+
self.$targets.$active = 0;
33+
self.$setActive = $scope.$setActive = function(value) {
34+
self.$targets.$active = self.$targets.$active === value ? -1 : value;
35+
self.$viewChangeListeners.forEach(function(fn) {
36+
fn();
37+
});
38+
};
39+
40+
};
41+
42+
this.$get = function() {
43+
var $collapse = {};
44+
$collapse.defaults = defaults;
45+
$collapse.controller = controller;
46+
return $collapse;
47+
};
48+
49+
})
50+
51+
.directive('bsCollapse', function($window, $animate, $collapse) {
52+
53+
var defaults = $collapse.defaults;
54+
55+
return {
56+
require: ['?ngModel', 'bsCollapse'],
57+
controller: $collapse.controller,
58+
link: function postLink(scope, element, attrs, controllers) {
59+
60+
var ngModelCtrl = controllers[0];
61+
var bsCollapseCtrl = controllers[1];
62+
63+
if(ngModelCtrl) {
64+
65+
// Update the modelValue following
66+
bsCollapseCtrl.$viewChangeListeners.push(function() {
67+
ngModelCtrl.$setViewValue(bsCollapseCtrl.$targets.$active);
68+
});
69+
70+
// modelValue -> $formatters -> viewValue
71+
ngModelCtrl.$formatters.push(function(modelValue) {
72+
// console.warn('$formatter("%s"): modelValue=%o (%o)', element.attr('ng-model'), modelValue, typeof modelValue);
73+
bsCollapseCtrl.$setActive(modelValue * 1);
74+
return modelValue;
75+
});
76+
77+
}
78+
79+
}
80+
};
81+
82+
})
83+
84+
.directive('bsCollapseToggle', function() {
85+
86+
return {
87+
require: ['^?ngModel', '^bsCollapse'],
88+
link: function postLink(scope, element, attrs, controllers) {
89+
90+
var ngModelCtrl = controllers[0];
91+
var bsCollapseCtrl = controllers[1];
92+
93+
// Add base attr
94+
element.attr('data-toggle', 'collapse');
95+
96+
// Push pane to parent bsTabs controller
97+
bsCollapseCtrl.$registerToggle(element);
98+
element.on('click', function() {
99+
var index = attrs.bsCollapseToggle || bsCollapseCtrl.$toggles.indexOf(element);
100+
bsCollapseCtrl.$setActive(index * 1);
101+
scope.$apply();
102+
});
103+
104+
}
105+
};
106+
107+
})
108+
109+
.directive('bsCollapseTarget', function($animate) {
110+
111+
return {
112+
require: ['^?ngModel', '^bsCollapse'],
113+
// scope: true,
114+
link: function postLink(scope, element, attrs, controllers) {
115+
116+
var ngModelCtrl = controllers[0];
117+
var bsCollapseCtrl = controllers[1];
118+
119+
// Add base class
120+
element.addClass('collapse');
121+
122+
// Add animation class
123+
if(bsCollapseCtrl.$options.animation) {
124+
element.addClass(bsCollapseCtrl.$options.animation);
125+
}
126+
127+
// Push pane to parent bsTabs controller
128+
bsCollapseCtrl.$registerTarget(element);
129+
130+
function render() {
131+
var index = bsCollapseCtrl.$targets.indexOf(element);
132+
var active = bsCollapseCtrl.$targets.$active;
133+
$animate[index === active ? 'addClass' : 'removeClass'](element, 'in');
134+
}
135+
136+
bsCollapseCtrl.$viewChangeListeners.push(function() {
137+
render();
138+
});
139+
render();
140+
141+
}
142+
};
143+
144+
});
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<div class="bs-docs-section" ng-controller="CollapseDemoCtrl">
2+
3+
<div class="page-header">
4+
<h1 id="tabs">Collapses <a class="small" href="//github.com/mgcrea/angular-strap/blob/master/src/collapse/collapse.js" target="_blank">collapse.js</a>
5+
</h1>
6+
<code>mgcrea.ngStrap.collapse</code>
7+
</div>
8+
9+
<h2 id="collapses-examples">Examples</h2>
10+
<p>Add quick, dynamic collapsable functionality to transition through panels of local content.</p>
11+
12+
<h3>Live demo <a class="small edit-plunkr" data-module-name="mgcrea.ngStrapDocs" data-content-html-url="collapses/docs/collapse.demo.html" data-content-js-url="collapses/docs/collapse.demo.js" ng-plunkr data-title="edit in plunker" data-placement="right" bs-tooltip>clog.info</a></h3>
13+
<pre class="bs-example-scope">$scope.panels = {{panels | json}};
14+
$scope.panels.activePanel = {{ panels.activePanel }};
15+
</pre>
16+
<div class="bs-example" append-source>
17+
<!-- ngModel is optional -->
18+
<div class="panel-group" bs-collapse>
19+
<div class="panel panel-default" ng-repeat="panel in panels">
20+
<div class="panel-heading">
21+
<h4 class="panel-title">
22+
<a bs-collapse-toggle>
23+
{{ panel.title }}
24+
</a>
25+
</h4>
26+
</div>
27+
<div class="panel-collapse" bs-collapse-target>
28+
<div class="panel-body">
29+
{{ panel.body }}
30+
</div>
31+
</div>
32+
</div>
33+
</div>
34+
</div>
35+
<div class="bs-example" style="padding-bottom: 24px;" append-source>
36+
<!-- control a collapse panel with ngModel -->
37+
<div class="btn-group" ng-model="panels.activePanel" bs-radio-group>
38+
<label class="btn btn-default" ng-repeat="panel in panels">
39+
<input type="radio" class="btn btn-default" value="{{ $index }}">Panel n°{{ $index + 1 }}
40+
</label>
41+
</div>
42+
<div class="btn btn-default" ng-click="pushPanel()">Add new panel</div>
43+
</div>
44+
45+
<h2 id="tabs-usage">Usage</h2>
46+
<p>Append a <code>bs-collapse</code> attribute to any element and several <code>bs-collapse-toggle</code>,<code>bs-collapse-target</code> attributes to children elements to enable the directive.</p>
47+
48+
<div class="callout callout-info">
49+
<h4>Custom animations</h4>
50+
<p>Pane animation is done with the <code>active</code> class and requires custom CSS.</p>
51+
<pre class="bs-exemple-code">
52+
<code class="css" highlight-block>
53+
.panel-collapse.am-collapse {
54+
animation-duration: .3s;
55+
animation-timing-function: ease;
56+
animation-fill-mode: backwards;
57+
overflow: hidden;
58+
&.in-remove {
59+
animation-name: collapse;
60+
display: block;
61+
}
62+
&.in-add {
63+
animation-name: expand;
64+
}
65+
}
66+
</code>
67+
</pre>
68+
</div>
69+
70+
<h3>Options</h3>
71+
<p>Options can be passed via data attributes or as an <a href="http://docs.angularjs.org/guide/expression">AngularJS expression</a> to evaluate as an object on
72+
<code>bs-tabs</code>. For data attributes, append the option name to <code>data-</code>, as in <code>data-animation=""</code>.</p>
73+
<p><code>bs-collapse-toggle</code> can be hard mapped to a <code>bs-collapse-target</code> by passing its target index to the attribute (<code>bs-collapse-toggle="1"</code>)</p>
74+
<div class="table-responsive">
75+
<table class="table table-bordered table-striped">
76+
<thead>
77+
<tr>
78+
<th style="width: 100px;">Name</th>
79+
<th style="width: 100px;">type</th>
80+
<th style="width: 50px;">default</th>
81+
<th>description</th>
82+
</tr>
83+
</thead>
84+
<tbody>
85+
<tr>
86+
<td>animation</td>
87+
<td>string</td>
88+
<td>am-fade</td>
89+
<td>apply a CSS animation to the popover with <code>ngAnimate</code></td>
90+
</tr>
91+
</tbody>
92+
</table>
93+
</div>
94+
<div class="callout callout-info">
95+
<h4>Default options</h4>
96+
<p>You can override global defaults for the plugin with <code>$tabProvider.defaults</code></p>
97+
<div class="highlight">
98+
<pre class="bs-exemple-code">
99+
<code class="javascript" highlight-block>
100+
angular.module('myApp')
101+
.config(function($tabProvider) {
102+
angular.extend($tabProvider.defaults, {
103+
animation: 'am-flip-x'
104+
});
105+
})
106+
</code>
107+
</pre>
108+
</div>
109+
</div>
110+
111+
</div>

src/collapse/docs/collapse.demo.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
'use strict';
2+
3+
angular.module('mgcrea.ngStrapDocs')
4+
5+
.controller('CollapseDemoCtrl', function($scope, $templateCache) {
6+
7+
$scope.panels = [
8+
{title:'Collapsible Group Item #1', body: 'Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch.'},
9+
{title:'Collapsible Group Item #2', body: 'Food truck fixie locavore, accusamus mcsweeney\'s marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee.'},
10+
{title:'Collapsible Group Item #3', body: 'Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney\'s organic lomo retro fanny pack lo-fi farm-to-table readymade.'}
11+
];
12+
13+
$scope.panels.activePanel = 1;
14+
15+
$scope.pushPanel = function() {
16+
$scope.panels.push({title: 'Collapsible Group Item #4', body: 'Reprehenderit butcher retro keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid.'});
17+
};
18+
19+
});

src/collapse/test/.jshintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./../../../test/.jshintrc

src/collapse/test/collapse.spec.js

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
'use strict';
2+
3+
describe('tab', function () {
4+
5+
var $compile, $templateCache, $animate, scope, sandboxEl;
6+
7+
beforeEach(module('ngSanitize'));
8+
beforeEach(module('mgcrea.ngStrap.collapse'));
9+
10+
beforeEach(inject(function (_$rootScope_, _$compile_, _$templateCache_, _$animate_) {
11+
scope = _$rootScope_.$new();
12+
sandboxEl = $('<div>').attr('id', 'sandbox').appendTo($('body'));
13+
$compile = _$compile_;
14+
$templateCache = _$templateCache_;
15+
$animate = _$animate_;
16+
}));
17+
18+
afterEach(function() {
19+
scope.$destroy();
20+
sandboxEl.remove();
21+
});
22+
23+
// Templates
24+
// '<div class="panel-group" bs-collapse><div class="panel panel-default" ng-repeat="panel in panels"><div class="panel-heading"><h4 class="panel-title"><a bs-collapse-toggle>title-1</a></h4></div><div class="panel-collapse" bs-collapse-target><div class="panel-body">content-1</div></div></div></div>'
25+
26+
27+
var templates = {
28+
'default': {
29+
element: '<div class="panel-group" bs-collapse><div class="panel panel-default"><div class="panel-heading"><h4 class="panel-title"><a bs-collapse-toggle>title-1</a></h4></div><div class="panel-collapse" bs-collapse-target><div class="panel-body">content-1</div></div></div><div class="panel panel-default"><div class="panel-heading"><h4 class="panel-title"><a bs-collapse-toggle>title-2</a></h4></div><div class="panel-collapse" bs-collapse-target><div class="panel-body">content-2</div></div></div></div>'
30+
},
31+
'template-ngRepeat': {
32+
scope: {panels: [
33+
{title:'Collapsible Group Item #1', body: 'Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch.'},
34+
{title:'Collapsible Group Item #2', body: 'Food truck fixie locavore, accusamus mcsweeney\'s marfa nulla single-origin coffee squid. Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee.'},
35+
{title:'Collapsible Group Item #3', body: 'Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney\'s organic lomo retro fanny pack lo-fi farm-to-table readymade.'}
36+
]},
37+
element: '<div class="panel-group" bs-collapse><div class="panel panel-default" ng-repeat="panel in panels"><div class="panel-heading"><h4 class="panel-title"><a bs-collapse-toggle>title-1</a></h4></div><div class="panel-collapse" bs-collapse-target><div class="panel-body">content-1</div></div></div></div>'
38+
},
39+
'binding-ngModel': {
40+
scope: {panel: {active: 1}},
41+
element: '<div class="panel-group" ng-model="panel.active" bs-collapse><div class="panel panel-default"><div class="panel-heading"><h4 class="panel-title"><a bs-collapse-toggle>title-1</a></h4></div><div class="panel-collapse" bs-collapse-target><div class="panel-body">content-1</div></div></div><div class="panel panel-default"><div class="panel-heading"><h4 class="panel-title"><a bs-collapse-toggle>title-2</a></h4></div><div class="panel-collapse" bs-collapse-target><div class="panel-body">content-2</div></div></div></div>'
42+
},
43+
'options-animation': {
44+
element: '<div data-animation="am-flip-x" class="panel-group" bs-collapse><div class="panel panel-default"><div class="panel-heading"><h4 class="panel-title"><a bs-collapse-toggle>title-1</a></h4></div><div class="panel-collapse" bs-collapse-target><div class="panel-body">content-1</div></div></div><div class="panel panel-default"><div class="panel-heading"><h4 class="panel-title"><a bs-collapse-toggle>title-2</a></h4></div><div class="panel-collapse" bs-collapse-target><div class="panel-body">content-2</div></div></div></div>'
45+
}
46+
};
47+
48+
function compileDirective(template, locals) {
49+
template = templates[template];
50+
angular.extend(scope, template.scope || templates['default'].scope, locals);
51+
var element = $(template.element).appendTo(sandboxEl);
52+
element = $compile(element)(scope);
53+
scope.$digest();
54+
return jQuery(element[0]);
55+
}
56+
57+
// Tests
58+
59+
describe('with default template', function () {
60+
61+
it('should navigate between panels on click', function() {
62+
var elm = compileDirective('default');
63+
expect(sandboxEl.find('[bs-collapse-target]:eq(0)').hasClass('in')).toBeTruthy();
64+
sandboxEl.find('[bs-collapse-toggle]:eq(1)').triggerHandler('click');
65+
expect(sandboxEl.find('[bs-collapse-target]:eq(0)').hasClass('in')).toBeFalsy();
66+
expect(sandboxEl.find('[bs-collapse-target]:eq(1)').hasClass('in')).toBeTruthy();
67+
});
68+
69+
});
70+
71+
describe('with ngRepeat template', function () {
72+
73+
it('should navigate between panels on click', function() {
74+
var elm = compileDirective('template-ngRepeat');
75+
expect(sandboxEl.find('[bs-collapse-target]:eq(0)').hasClass('in')).toBeTruthy();
76+
sandboxEl.find('[bs-collapse-toggle]:eq(1)').triggerHandler('click');
77+
expect(sandboxEl.find('[bs-collapse-target]:eq(0)').hasClass('in')).toBeFalsy();
78+
expect(sandboxEl.find('[bs-collapse-target]:eq(1)').hasClass('in')).toBeTruthy();
79+
});
80+
81+
});
82+
83+
describe('data-binding', function() {
84+
85+
it('should correctly apply model changes to the view', function() {
86+
var elm = compileDirective('binding-ngModel');
87+
expect(sandboxEl.find('[bs-collapse-target].in').parent('.panel-default').index()).toBe(scope.panel.active);
88+
scope.panel.active = 0;
89+
scope.$digest();
90+
expect(sandboxEl.find('[bs-collapse-target].in').parent('.panel-default').index()).toBe(scope.panel.active);
91+
});
92+
93+
it('should correctly apply view changes to the model', function() {
94+
var elm = compileDirective('binding-ngModel');
95+
sandboxEl.find('[bs-collapse-toggle]:eq(0)').triggerHandler('click');
96+
expect(scope.panel.active).toBe(0);
97+
});
98+
99+
});
100+
101+
describe('options', function () {
102+
103+
describe('animation', function () {
104+
105+
it('should default to `am-collapse` animation', function() {
106+
var elm = compileDirective('default');
107+
expect(sandboxEl.find('[bs-collapse-target]').hasClass('am-collapse')).toBeTruthy();
108+
});
109+
110+
it('should support custom animation', function() {
111+
var elm = compileDirective('options-animation');
112+
expect(sandboxEl.find('[bs-collapse-target]').hasClass('am-flip-x')).toBeTruthy();
113+
});
114+
115+
});
116+
117+
});
118+
119+
});

0 commit comments

Comments
 (0)