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

Commit 809b92c

Browse files
committed
refactor(tabs): Change syntax, add features
* Tabs transclude to title elements instead of content elements, so the ordering is always correct (Closes #153) * Rename `<tabs>` to `<tabset>`, `<pane>` to `<tab>` (Closes #186) * Add `<tab-heading>` directive, which is a child of a `<tab>`. Allows HTML in tab headings (Closes #124) * Add `select` attribute callback when tab is selected (Closes #141) * Only the active tab's content is now actually ever in the DOM
1 parent f698717 commit 809b92c

File tree

10 files changed

+550
-280
lines changed

10 files changed

+550
-280
lines changed

Diff for: misc/demo-template.html

+4-4
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,10 @@ <h1><%= module.displayName %><small>
182182
<div class="pull-right">
183183
<button class="btn btn-info" id="plunk-btn" ng-click="edit('<%= ngversion%>', '<%= bsversion %>', '<%= version%>', '<%= module.name %>')"><i class="icon-edit icon-white"></i> Edit in plunker</button>
184184
</div>
185-
<tabs>
186-
<pane heading="Markup" plunker-content="markup"><pre ng-non-bindable><code data-language="html"><%- module.html %></code></pre></pane>
187-
<pane heading="JavaScript" plunker-content="javascript"><pre ng-non-bindable><code data-language="javascript"><%- module.js %></code></pre></pane>
188-
</tabs>
185+
<tabset>
186+
<tab heading="Markup" plunker-content="markup"><pre ng-non-bindable><code data-language="html"><%- module.html %></code></pre></tab>
187+
<tab heading="JavaScript" plunker-content="javascript"><pre ng-non-bindable><code data-language="javascript"><%- module.js %></code></pre></tab>
188+
</tabset>
189189
</div>
190190
</div>
191191
</section>

Diff for: src/tabs/docs/demo.html

+22-9
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
<div ng-controller="TabsDemoCtrl">
2-
<tabs>
3-
<pane heading="Static title">Static content</pane>
4-
<pane ng-repeat="pane in panes" heading="{{pane.title}}" active="pane.active">{{pane.content}}</pane>
5-
</tabs>
6-
<div class="row-fluid">
7-
<button class="btn" ng-click="panes[0].active = true">Select second tab</button>
8-
<button class="btn" ng-click="panes[1].active = true">Select third tab</button>
9-
</div>
10-
</div>
2+
Select a tab by setting active binding to true:
3+
<br />
4+
<button class="btn" ng-click="tabs[0].active = true">
5+
Select second tab
6+
</button>
7+
<button class="btn" ng-click="tabs[1].active = true">
8+
Select third tab
9+
</button>
10+
<br /><br />
11+
<tabset>
12+
<tab heading="Static title">Static content</tab>
13+
<tab ng-repeat="tab in tabs" heading="{{tab.title}}" active="tab.active">
14+
{{tab.content}}
15+
</tab>
16+
<tab select="alertMe()">
17+
<tab-heading>
18+
<i class="icon-bell"></i> Select me for alert!
19+
</tab-heading>
20+
I've got an HTML heading, and a select callback. Pretty cool!
21+
</tab>
22+
</tabset>
23+
</div>

Diff for: src/tabs/docs/demo.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
var TabsDemoCtrl = function ($scope) {
2-
$scope.panes = [
2+
$scope.tabs = [
33
{ title:"Dynamic Title 1", content:"Dynamic content 1" },
44
{ title:"Dynamic Title 2", content:"Dynamic content 2" }
55
];
6-
};
6+
7+
$scope.alertMe = function() {
8+
alert("You've selected the alert tab!");
9+
};
10+
};

Diff for: src/tabs/docs/readme.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
AngularJS version of the tabs directive.
1+
AngularJS version of the tabs directive.
2+
3+
Allows a `select` callback attribute, and `active` binding attribute.
4+
5+
Allows either `heading` text-heading as an attribute, or a `<tab-heading>` element inside as the heading.

Diff for: src/tabs/tabs.js

+152-51
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,176 @@
11
angular.module('ui.bootstrap.tabs', [])
2-
.controller('TabsController', ['$scope', '$element', function($scope, $element) {
3-
var panes = $scope.panes = [];
4-
5-
this.select = $scope.select = function selectPane(pane) {
6-
angular.forEach(panes, function(pane) {
7-
pane.selected = false;
8-
});
9-
pane.selected = true;
2+
3+
.controller('TabsetController', ['$scope', '$element',
4+
function TabsetCtrl($scope, $element) {
5+
var ctrl = this,
6+
tabs = ctrl.tabs = [];
7+
8+
ctrl.select = function(tab) {
9+
angular.forEach(tabs, ctrl.deselect);
10+
tab.active = true;
11+
ctrl.activeTab = tab;
12+
};
13+
14+
ctrl.deselect = function(tab) {
15+
if (ctrl.activeTab === tab) {
16+
ctrl.activeTab = null;
17+
}
18+
tab.active = false;
1019
};
1120

12-
this.addPane = function addPane(pane) {
13-
if (!panes.length) {
14-
$scope.select(pane);
21+
22+
ctrl.addTab = function addTab(tab) {
23+
if (!tabs.length) {
24+
ctrl.select(tab);
1525
}
16-
panes.push(pane);
26+
tabs.push(tab);
1727
};
1828

19-
this.removePane = function removePane(pane) {
20-
var index = panes.indexOf(pane);
21-
panes.splice(index, 1);
22-
//Select a new pane if removed pane was selected
23-
if (pane.selected && panes.length > 0) {
24-
$scope.select(panes[index < panes.length ? index : index-1]);
29+
ctrl.removeTab = function removeTab(tab) {
30+
var index = tabs.indexOf(tab);
31+
tabs.splice(index, 1);
32+
//Select a new tab if removed tab was selected
33+
if (tab.active && tabs.length > 0) {
34+
ctrl.select(tabs[index < tabs.length ? index : index-1]);
2535
}
2636
};
2737
}])
28-
.directive('tabs', function() {
38+
39+
.directive('tabset', function() {
2940
return {
3041
restrict: 'EA',
3142
transclude: true,
32-
scope: {},
33-
controller: 'TabsController',
34-
templateUrl: 'template/tabs/tabs.html',
35-
replace: true
43+
controller: 'TabsetController',
44+
templateUrl: 'template/tabs/tabset.html'
3645
};
3746
})
38-
.directive('pane', ['$parse', function($parse) {
47+
48+
.directive('tab', ['$parse', '$http', '$templateCache', '$compile',
49+
function($parse, $http, $templateCache, $compile) {
3950
return {
40-
require: '^tabs',
51+
require: '^tabset',
4152
restrict: 'EA',
53+
replace: true,
54+
templateUrl: 'template/tabs/tab.html',
4255
transclude: true,
43-
scope:{
44-
heading:'@'
56+
scope: {
57+
heading: '@'
4558
},
46-
link: function(scope, element, attrs, tabsCtrl) {
47-
var getSelected, setSelected;
48-
scope.selected = false;
49-
if (attrs.active) {
50-
getSelected = $parse(attrs.active);
51-
setSelected = getSelected.assign;
52-
scope.$watch(
53-
function watchSelected() {return getSelected(scope.$parent);},
54-
function updateSelected(value) {scope.selected = value;}
55-
);
56-
scope.selected = getSelected ? getSelected(scope.$parent) : false;
57-
}
58-
scope.$watch('selected', function(selected) {
59-
if(selected) {
60-
tabsCtrl.select(scope);
59+
controller: ['$scope', function TabCtrl($scope) {
60+
this.getHeadingElement = function() {
61+
return $scope.headingElement;
62+
};
63+
}],
64+
compile: function(elm, attrs, transclude) {
65+
return function postLink(scope, elm, attrs, tabsetCtrl) {
66+
var getActive, setActive;
67+
scope.active = false; // default value
68+
if (attrs.active) {
69+
getActive = $parse(attrs.active);
70+
setActive = getActive.assign;
71+
scope.$watch(function watchActive() {
72+
return getActive(scope.$parent);
73+
}, function updateActive(value) {
74+
scope.active = !!value;
75+
});
76+
} else {
77+
setActive = getActive = angular.noop;
6178
}
62-
if(setSelected) {
63-
setSelected(scope.$parent, selected);
79+
80+
scope.$watch('active', function(active) {
81+
setActive(scope.$parent, active);
82+
if (active) {
83+
tabsetCtrl.select(scope);
84+
if (attrs.select) {
85+
scope.$parent.$eval(attrs.select);
86+
}
87+
} else {
88+
tabsetCtrl.deselect(scope);
89+
}
90+
});
91+
92+
scope.select = function() {
93+
scope.active = true;
94+
};
95+
96+
tabsetCtrl.addTab(scope);
97+
scope.$on('$destroy', function() {
98+
tabsetCtrl.removeTab(scope);
99+
});
100+
//If the tabset sets this guy to active, set the parent too. We have to
101+
//do this because the parent watcher will fire and set active to false
102+
//again before the watcher on line 82 can go
103+
if (scope.active) {
104+
setActive(scope.$parent, true);
105+
}
106+
107+
//Transclude the collection of sibling elements. Use forEach to find
108+
//the heading if it exists. We don't use a directive for tab-heading
109+
//because it is problematic. Discussion @ http://git.io/MSNPwQ
110+
transclude(scope.$parent, function(clone) {
111+
//Look at every element in the clone collection. If it's tab-heading,
112+
//mark it as that. If it's not tab-heading, mark it as tab contents
113+
var contents = [], heading;
114+
angular.forEach(clone, function(el) {
115+
//See if it's a tab-heading attr or element directive
116+
//First make sure it's a normal element, one that has a tagName
117+
if (el.tagName &&
118+
(el.hasAttribute("tab-heading") ||
119+
el.hasAttribute("data-tab-heading") ||
120+
el.tagName.toLowerCase() == "tab-heading" ||
121+
el.tagName.toLowerCase() == "data-tab-heading"
122+
)) {
123+
heading = el;
124+
} else {
125+
contents.push(el);
126+
}
127+
});
128+
if (heading) {
129+
scope.headingElement = angular.element(heading);
130+
}
131+
//Everything else is the content we need, since we removed
132+
scope.contentElement = angular.element(contents);
133+
});
134+
};
135+
}
136+
};
137+
}])
138+
139+
.directive('tabHeadingTransclude', [function() {
140+
return {
141+
restrict: 'A',
142+
require: '^tab',
143+
link: function(scope, elm, attrs, tabCtrl) {
144+
scope.$watch(function getHeadingElement() {
145+
return tabCtrl.getHeadingElement();
146+
}, function(heading) {
147+
if (heading) {
148+
elm.html('');
149+
elm.append(heading);
64150
}
65151
});
152+
}
153+
};
154+
}])
66155

67-
tabsCtrl.addPane(scope);
68-
scope.$on('$destroy', function() {
69-
tabsCtrl.removePane(scope);
156+
.directive('tabContentTransclude', [function() {
157+
return {
158+
restrict: 'A',
159+
require: '^tabset',
160+
link: function(scope, elm, attrs, tabsetCtrl) {
161+
scope.$watch(function() {
162+
return tabsetCtrl.activeTab;
163+
}, function(activeTab, previousActiveTab) {
164+
if (previousActiveTab) {
165+
previousActiveTab.contentElement.remove();
166+
}
167+
if (activeTab) {
168+
elm.append(activeTab.contentElement);
169+
}
70170
});
71-
},
72-
templateUrl: 'template/tabs/pane.html',
73-
replace: true
171+
}
74172
};
75-
}]);
173+
}])
174+
175+
;
176+

0 commit comments

Comments
 (0)