Skip to content
This repository has been archived by the owner on Dec 1, 2023. It is now read-only.

Commit

Permalink
feat(tab): provide $tab module
Browse files Browse the repository at this point in the history
  • Loading branch information
mgcrea committed Jan 7, 2014
1 parent 3a1f902 commit f88631b
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/tab/docs/pane.tpl.demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<pre>{{pane.content}}</pre>
110 changes: 110 additions & 0 deletions src/tab/docs/tab.demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<div class="bs-docs-section" ng-controller="TabDemoCtrl">

<div class="page-header">
<h1 id="tabs">Tabs <a class="small" href="//github.com/mgcrea/angular-strap/blob/master/src/tab/tab.js" target="_blank">tab.js</a>
</h1>
<code>mgcrea.ngStrap.tab</code>
</div>


<h2 id="tabs-examples">Examples</h2>
<p>Add quick, dynamic tab functionality to transition through panes of local content.</p>

<h3>Live demo <a class="small" href="#" target="_blank"><i class="fa fa-edit" data-title="edit in plunker" data-placement="right" bs-tooltip></i></a>
</h2>
<pre class="bs-example-scope">$scope.tabs = {{tabs | json}};</pre>
<div class="bs-example" append-source>
<!-- ngModel is optional -->
<div ng-model="tabs.activeTab" bs-tabs="tabs">
</div>
</div>
<div class="bs-example" style="padding-bottom: 24px;">
<label>activeTab:</label>
<strong class="text-danger">{{tabs.activeTab}}</strong>&nbsp;
<div class="btn-group" ng-model="tabs.activeTab" bs-radio-group>
<label class="btn btn-default">
<input type="radio" class="btn btn-default" value="0">First</label>
<label class="btn btn-default">
<input type="radio" class="btn btn-default" value="1">Second</label>
<label class="btn btn-default">
<input type="radio" class="btn btn-default" value="2">Third</label>
</div>
</div>
</h3>

<h2 id="tabs-usage">Usage</h2>
<p>Append a <code>bs-tabs</code>attribute to any element to enable the directive.</p>

<div class="bs-callout bs-callout-info">
<h4>Custom animations</h4>
<p>Pane animation is done with <code>ngClass('active')</code> callbacks and require custom CSS.</p>
<pre class="bs-exemple-code">
<code class="css" highlight-block>
.tabs.animation-fade .tab-pane {
opacity: 1;
transition: opacity .3s ease;
min-height:60px;
&.active-add {
display: block;
opacity: 0;
&.active-add-active {
opacity: 1;
}
}
&.active-remove {
display: none;
}
}
</code>
</pre>
</div>

<h3>Options</h3>
<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
<code>bs-tabs</code>. For data attributes, append the option name to <code>data-</code>, as in <code>data-animation=""</code>.</p>
<div class="table-responsive">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">Name</th>
<th style="width: 100px;">type</th>
<th style="width: 50px;">default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>animation</td>
<td>string</td>
<td>animation-fade</td>
<td>apply a CSS animation to the popover with <code>ngAnimate</code></td>
</tr>
<tr>
<td>template</td>
<td>path</td>
<td>false</td>
<td>
<p>If a remote URL is provided, overrides the default template</p>
</td>
</tr>
</tbody>
</table>
</div>
<div class="callout callout-info">
<h4>Default options</h4>
<p>You can override global defaults for the plugin with <code>$tabProvider.defaults</code></p>
<div class="highlight">
<pre class="bs-exemple-code">
<code class="javascript" highlight-block>
angular.module('myApp')
.config(function($tabProvider) {
angular.extend($tabProvider.defaults, {
animation: 'animation-flipX'
});
})
</code>
</pre>
</div>
</div>

</div>
15 changes: 15 additions & 0 deletions src/tab/docs/tab.demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict';

angular.module('mgcrea.ngStrapDocs')

.controller('TabDemoCtrl', function($scope, $templateCache) {

$scope.tabs = [
{title:'Home', content: 'Raw denim you probably haven\'t heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica.'},
{title:'Profile', content: '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.'},
{title:'About', template: 'tab/docs/pane.tpl.demo.html', content: 'Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney\'s organic lomo retro fanny pack lo-fi farm-to-table readymade.'}
];

$scope.tabs.activeTab = 1;

});
86 changes: 86 additions & 0 deletions src/tab/tab.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict';

angular.module('mgcrea.ngStrap.tab', [])

.run(function($templateCache) {

$templateCache.put('$pane', '{{pane.content}}');

var template = '<ul class="nav nav-tabs">' +
'<li ng-repeat="pane in panes" ng-class="{active:$index==active}">' +
'<a data-toggle="tab" ng-click="setActive($index, $event)" data-index="{{$index}}">{{pane.title}}</a>' +
'</li>' +
'</ul>' +
'<div class="tab-content">' +
'<div ng-repeat="pane in panes" class="tab-pane" ng-class="[$index==active?\'active\':\'\']" ng-include="pane.template || \'$pane\'"></div>' +
'</div>';

$templateCache.put('$tabs', template);

})

.provider('$tab', function() {

var defaults = this.defaults = {
animation: 'animation-fade',
template: '$tabs'
};

this.$get = function() {
return {defaults: defaults};
};

})

.directive('bsTabs', function($window, $animate, $tab) {

var defaults = $tab.defaults;

return {
restrict: 'EAC',
scope: true,
require: '?ngModel',
templateUrl: function(element, attr) {
return attr.template || defaults.template;
},
link: function postLink(scope, element, attr, controller) {

// Directive options
var options = defaults;
angular.forEach(['animation'/*, 'template'*/], function(key) {
if(angular.isDefined(attr[key])) options[key] = attr[key];
});

// Require scope as an object
attr.bsTabs && scope.$watch(attr.bsTabs, function(newValue, oldValue) {
scope.panes = newValue;
}, true);

// Add base class
element.addClass('tabs');

// Support animations
if(options.animation) {
element.addClass(options.animation);
}

scope.active = scope.activePane = 0;
// view -> model
scope.setActive = function(index, ev) {
scope.active = index;
if(controller) {
controller.$setViewValue(index);
}
};

// model -> view
if(controller) {
controller.$render = function() {
scope.active = controller.$modelValue * 1;
};
}

}
};

});
1 change: 1 addition & 0 deletions src/tab/test/.jshintrc
135 changes: 135 additions & 0 deletions src/tab/test/tab.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'use strict';

describe('tab', function () {

var $compile, $templateCache, scope, sandboxEl;

beforeEach(module('ngSanitize'));
beforeEach(module('mgcrea.ngStrap.tab'));

beforeEach(inject(function (_$rootScope_, _$compile_, _$templateCache_) {
scope = _$rootScope_.$new();
sandboxEl = $('<div>').attr('id', 'sandbox').appendTo($('body'));
$compile = _$compile_;
$templateCache = _$templateCache_;
}));

afterEach(function() {
scope.$destroy();
sandboxEl.remove();
});

// Templates

var templates = {
'default': {
scope: {tab: {active: 1, counter: 0},tabs: [
{title:'Home', content: 'Raw denim you probably haven\'t heard of...'},
{title:'Profile', content: 'Food truck fixie locavore...'},
{title:'About', content: 'Etsy mixtape wayfarers...'}
]},
element: '<div bs-tabs="tabs"></div>'
},
'binding-ngModel': {
element: '<div ng-model="tab.active" bs-tabs="tabs"></div>'
},
'options-animation': {
element: '<div data-animation="animation-flipX" bs-tabs="tabs"></div>'
},
'options-template': {
element: '<div data-template="custom" bs-tabs="tabs"></div>'
}
};

function compileDirective(template, locals) {
template = templates[template];
angular.extend(scope, template.scope || templates['default'].scope, locals);
var element = $(template.element).appendTo(sandboxEl);
element = $compile(element)(scope);
scope.$digest();
return jQuery(element[0]);
}

// Tests

describe('with default template', function () {

it('should correctly compile inner content', function() {
var elm = compileDirective('default');
expect(sandboxEl.find('.nav-tabs > li').length).toBe(scope.tabs.length);
expect(sandboxEl.find('.nav-tabs > li:eq(0)').text()).toBe(scope.tabs[0].title);
expect(sandboxEl.find('.tab-content > .tab-pane').length).toBe(scope.tabs.length);
expect(sandboxEl.find('.tab-content > .tab-pane:eq(0)').text()).toBe(scope.tabs[0].content);
});

it('should navigate between panes on click', function() {
var elm = compileDirective('default');
expect(sandboxEl.find('.nav-tabs > li.active').text()).toBe(scope.tabs[0].title);
expect(sandboxEl.find('.tab-content > .tab-pane.active').text()).toBe(scope.tabs[0].content);
sandboxEl.find('.nav-tabs > li:eq(1) > a').triggerHandler('click');
expect(sandboxEl.find('.nav-tabs > li.active').text()).toBe(scope.tabs[1].title);
expect(sandboxEl.find('.tab-content > .tab-pane.active').text()).toBe(scope.tabs[1].content);
sandboxEl.find('.nav-tabs > li:eq(0) > a').triggerHandler('click');
expect(sandboxEl.find('.nav-tabs > li.active').text()).toBe(scope.tabs[0].title);
expect(sandboxEl.find('.tab-content > .tab-pane.active').text()).toBe(scope.tabs[0].content);
});

});

describe('data-binding', function() {

it('should correctly apply model changes to the view', function() {
var elm = compileDirective('binding-ngModel');
expect(sandboxEl.find('.nav-tabs > li.active').index()).toBe(scope.tab.active);
expect(sandboxEl.find('.tab-content > .tab-pane.active').index()).toBe(scope.tab.active);
scope.tab.active = 2;
scope.$digest();
expect(sandboxEl.find('.nav-tabs > li.active').index()).toBe(scope.tab.active);
expect(sandboxEl.find('.tab-content > .tab-pane.active').index()).toBe(scope.tab.active);
});

it('should correctly apply view changes to the model', function() {
var elm = compileDirective('binding-ngModel');
sandboxEl.find('.nav-tabs > li:eq(0) > a').triggerHandler('click');
expect(scope.tab.active).toBe(0);
});

});

describe('options', function () {

describe('animation', function () {

it('should default to `animation-fade` animation', function() {
var elm = compileDirective('default');
expect(sandboxEl.children('.tabs').hasClass('animation-fade')).toBeTruthy();
});

it('should support custom animation', function() {
var elm = compileDirective('options-animation');
expect(sandboxEl.children('.tabs').hasClass('animation-flipX')).toBeTruthy();
});

});

describe('template', function () {

it('should support custom template', function() {
$templateCache.put('custom', '<div class="tabs"><div class="tab-pane" ng-repeat="pane in panes">foo: {{pane.content}}</div></div>');
var elm = compileDirective('options-template');
expect(sandboxEl.find('.tab-pane:eq(0)').text()).toBe('foo: ' + scope.tabs[0].content);
});

it('should support template with ngClick', function() {
$templateCache.put('custom', '<div class="tabs"><div class="tab-pane" ng-repeat="pane in panes"><a class="btn" ng-click="tab.counter=tab.counter+1">{{foo.title}}</a></div></div>');
var elm = compileDirective('options-template');
angular.element(elm[0]).triggerHandler('mouseenter');
expect(angular.element(sandboxEl.find('.tab-pane:eq(0) > .btn')[0]).triggerHandler('click'));
expect(scope.tab.counter).toBe(1);
});

});

});

});

0 comments on commit f88631b

Please sign in to comment.