Dynamic persistent tabbed content for Angular.JS.
Useful for single-page interactive web apps which allow the user to organise content by tabs, e.g. a text editor.
Tabs...
-
each have a unique content element which is shown only while the tab is in 'focus'.
-
are 'typed'. A tab type specifies the template and (optionally) controller used to compile the tab's content element. By default, tabs get new scopes inheriting from their parent scope.
-
are arranged by 'areas'. A tab area represents a flat array of tabs, of which only one can be focused at a time.
-
can be moved to different areas, or to a different index within the same area.
-
can be parameterised by providing an
optionsobject when they are created. -
provide a simple events system for communicating with their instantiators.
-
are optionally persisted on a per-area basis.
You'll need to run npm install and then sudo npm install grunt-cli -g if you don't have grunt installed already.
Then it's just grunt, or grunt && grunt watch if developing.
Load tabangular.js or tabangular.min.js into your page
<script type="text/javascript" src="path/to/tabangular.js"></script>Include 'tabangular' in your module dependencies
var textEditor = angular.module('textEditor', ['tabangular']);Register named tab types at config time.
textEditor.config(function (TabsProvider) {
TabsProvider.registerTabType("editorTab", {
// you can supply a url from which to fetch the template
templateUrl: 'templates/editor.html',
// controllers are resolved as usual
controller: 'EditorCtrl'
});
TabsProvider.registerTabType("configTab", {
// templateUrl looks in the $templateCache so you can supply the id of a
// text/ng-template script element
templateUrl: 'config-template',
controller: function ($scope, Tab) { ... }
});
TabsProvider.registerTabType("hello", {
// or just a string
template: '<div>Hello world!</div>',
// set scope to false to avoid creating a new scope for the tab
scope: false
// don't supply a controller if the tab doesn't need a controller
});
});Additionally, or alternatively, you can automatically resolve named tab types by providing a 'fetcher' function.
textEditor.config(function (TabsProvider) {
TabsProvider.typeFetcherFactory(function($http) {
return function (deferred, typeID) {
// deferred is a $q Deferred object which must be resolved with the tab type
// typeID is the string ID of the tab type to resolve
var templateURL = "tabs/" + typeID + "/template.html";
// in this example, we load the controller from the server.
var controllerURL = "tabs/" + typeID + "/controller.js";
$http.get(controllerURL).success(function (result) {
var ctrl = eval("(" + result.data + ")");
// now the tab type can be resolved
deferred.resolve({
templateURL: templateURL,
controller: ctrl
});
});
};
});
});Create a tab area with the Tabs service
textEditor.controller('WindowCtrl', function (Tabs) {
$scope.docs = Tabs.newArea();Then define functions and stuff for manipulating docs.
$scope.openDocument = function (filename) {
$scope.docs.open('editorTab', {filename: filename});
};
$scope.newDocument = function () {
$scope.docs.open('editorTab', {filename: 'untitled'})
};
$scope.configTab = null;
$scope.config = function () {
if ($scope.configTab) {
// we only want one config tab open at a time
$scope.configTab.focus();
} else {
$scope.configTab = $scope.docs.open('configTab'); // options are optional
$scope.configTab.on("closed", function () {
delete $scope.configTab;
});
}
}
});It is up to you to specify the HTML/css for the tabs themselves, along with the dom node in which the content elements should be placed.
<body controller='WindowCtrl'>
<!-- tabs go here -->
<ul class='tabs-list'>
<li ng-repeat='tab in docs.list()' ng-class="{active: tab.focused}">
<a href="" ng-click="tab.focus()">{{tab.options.filename}}</a>
<span ng-show="tab.loading">...</span>
<a href="" ng-click="tab.close()">×</a>
</li>
<li class='new-document'>
<a href="" ng-click="newDocument()">new +</a>
</li>
</ul>
<div class="tabs-content" tab-content="docs">
<!-- the tab-content value should evaluate to the relevant TabArea object -->
</div>
<button ng-click="config()">Configure the Editor</button>
</body>Tab controllers can defer the 'loaded' event such that they don't get shown until they want to be shown.
function EditorCtrl ($scope, $http, Tab) {
if (Tab.options.filename !== 'untitled') {
Tab.deferLoading();
$http.get(Tab.options.filename).then(function (response) {
$scope.text = response.data;
Tab.options.savedText = $scope.text;
Tab.doneLoading();
});
} else {
$scope.text = "";
Tab.options.savedText = $scope.text;
}They can also intercept the 'close' event, which gets fired when Tab.close() is called, but not when Tab.close(true) is called.
Tab.disableAutoClose();
Tab.on("close", function () {
if ($scope.text === Tab.options.savedText
|| window.confirm("You have unsaved changes. Are you sure?")) {
Tab.close(true); // force close
}
});
$scope.save = function () {
Tab.options.savedText = $scope.text;
$http.post(whatever...);
};
}When providing a controller in the tab type, content templates should not use ng-controller, since the controller needs to be injected with the Tab pseudo-service by tabangular.
<!-- templates/editor.html -->
<textarea ng-model='text'></textarea>
<button ng-click='save()'>Save</button>However, if the tab doesn't need to know the fact that it's in a tab, ng-controller is fine.
<!-- assume config tab type is simply {templateID: 'config.html'} -->
<script type='text/javascript'>
function ConfigCtrl ($scope) {
... configuration logic
}
</script>
<script type='text/ng-template' id='config.html'>
<div ng-controller='ConfigCtrl'>
... configuration controls
</div>
</script>Persistence may be achieved by passing options to Tabs.newArea. There is a default localStorage persistence option which can be enabled by giving a string id to the tab area
$scope.docs = Tabs.newArea({id: "myEditor"});Alternatively, provide persist and getExisting functions to e.g. save state to a server.
$scope.docs = Tabs.newArea({
// persist is called whenever tabs are opened, closed, moved, and focused,
// so if bandwidth is an issue for you, it is probably best to wrap the
// post/put request somehow.
persist: function (state) {
// the state parameter is a string.
$http.post('/tabs-state', {state: state});
},
// getExisting is called only once
getExisting: function (cb) {
$http.get('/tabs-state').success(function (result) {
cb(result.data.state);
});
}
});A simple, lightweight events system. Extended by Tab and TabArea, cannot be instantiated directly.
tab.on('foo', function () {
console.log("i am foo");
});
tab.trigger('foo');
// => i am foo
tab.one('bar', function () {
console.log("bar happens only once");
});
tab.trigger('bar');
// => bar happens only once
tab.trigger('bar');
// nothing happens
tab.on('hello', function (name) {
console.log("Hello, " + name + "!");
});
tab.trigger('hello', 'Steve');
// => Hello, Steve!
// callbacks have their context set to the relevant object
function hello () {
console.log("Hello, " + this.options.name + "!");
}
tab1.on('hello', hello);
tab2.on('hello', hello);
tab1.options.name = "John";
tab2.options.name = "Wilbur";
tab1.trigger('hello');
// => Hello, John!
tab2.trigger('hello');
// => Hello, Wilbur!Binds callback as a handler for event. Returns a function which, when invoked, unbinds the callback.
##### `one` :: `(event : string, callback : function) : function`
As Evented.on but unbinds the callback automatically after being invoked for the first time.
##### `trigger` :: `(event : string [, data : object]) : void`
Fires an event event, passing data as the first parameter to any bound callbacks.
TabsProvider can be used to configure the Tabs service.
Registers a tab type. id should be a unique string id, options should be an object with some combination of the following:
-
scope::booleanSpecifies whether or not to define a new scope for tabs of this type. defaults to
true -
templateUrl::stringSpecifies a url from which to load a template, or the id of a template already in the dom (e.g. 'foo.html' for the template
<script type='text/ng-template' id='foo.html'>...</script>') -
template::stringSpecifies the template to use in the tab. Takes precedence over
templateUrl -
controller::function | stringSpecifies the controller to call against the scope. Should be a function or a string denoting the controller to use (see $controller).
Examples:
module.config(function (TabsProvider) {
TabsProvider.registerTabType("myTabType", {
templateUrl: "templates/my-tab-type.html",
controller: "MyTabCtrl"
});
TabsProvider.registerTabType("myOtherTabType", {
template: "<span>Hello {{name}}!</span>",
controller: function ($scope, Tab) { $scope.name = Tab.options.name; }
});
});##### `typeFetcherFactory` :: `(factory : function) : void`
Registers a factory function for a tab type fetcher. The tab type fetcher resolves named tab types dynamically, if they haven't been previously registered. The factory function is invoked using Angular's dependency injector, to allow the use of services such at $http when resolving tab types. It should return the fetcher function which has the signature (deferred : Deferred, typeID : string) : void. The fetcher function is responsible for resolving the deferred object with the relevant tab type (see registerTabType for the type options), or rejecting it when no such type can be found. See $q for the Deferred api.
Example which finds a template in Angular's template cache:
module.config(function (TabsProvider) {
TabsProvider.typeFetcherFactory(function ($templateCache) {
return function (dfd, id) {
var template = $templateCache.get(id + ".html");
if (template) {
dfd.resolve({
template: template
scope: false
});
} else {
dfd.reject("Couldn't find template: " + id + ".html");
}
};
});
});The 'tabs' service allows the creation of new tab areas.
Creates a new tab area. options should be an object with some combination of the following:
-
id::stringActivates the default localStorage persistence mechanism. Should be unique on a per-tab-area basis.
-
persist::function (state : string) : voidTakes a string representation of the tab area's current state and puts it somewhere for safe keeping. Called when tabs are opened, closed, and focused. Also called upon the window's
beforeunloadevent. By default it is a function which, ifidhas been defined, stores the state inlocalStorage['tabangular:'+id] -
getExisting::function (cb : function (state : string) : void) : voidTakes a callback which should be invoked with the stored state string at some point (or null if no state stored). Called once upon tab area construction. By default it is a function which, if
idhas been defined, looks up the state inlocalStorage['tabangular:'+id] -
transformOptions::function (options : object) : objectTakes the in-use version of a tab's options object and transforms it such that it is JSON stringifiable. By default it is the identity function.
-
parseOptions::function (options : object) : objectThe reverse of
transformOptions. Takes the deserialised version of a tab's options object and transforms it such that it is identical to how it was before being serialised. By default it is the identity function.
The TabArea class represents an ordered grouping of tabs and provides methods for creating new tabs. A tab area may have only one tab focused at one point in time. TabArea instances are created using the Tabs.newArea method.
Loads and returns a new tab. type should be a named tab type or an anonymous tab type object (see registerTabType for details).
options can be anything and is attached to the returned Tab object such that load(foo, bar).options === bar.
##### `open` :: `(type : string | object [, options : object]) : Tab`
Convenience method. As TabArea.load but calls Tab.focus before returning the tab.
##### `list` :: `() : [Tab]`
Returns an array of the tabs currently in this area. For performance reasons, it currently returns the internally-used array which should not be modified.
##### `handleExisting` :: `([cb : function (tab : object) : bool]) : void`
Triggers the reloading of tabs from persistent storage.
If called without arguments, simply reloads all tabs from storage.
If given the cb parameter, calls cb on each of the stored tab objects which have the structure:
-
type::string | objectThe tab type, as passed into
TabArea.loadorTabArea.openwhen the tab was created. -
focused::boolWhether or not the tab was focused when the area was persisted.
-
options::objectThe tab options, as gleaned from
Tab.optionswhen the area was persisted.
If cb returns true, the tab is automatically reloaded. Hence area.handleExisting() is shorthand for area.handleExisting(function () { return true; })
Example:
// Some tab types might require event listeners, so you can use `handleExisting`
// to reload the tabs and attach the event listeners.
area.handleExisting(function (tab) {
switch (tab.type) {
case "foo":
var fooID = tab.options.id;
var foo = area.load("foo", tab.options);
foo.on("foo_event", function () {
$http.post("react/to/foo_event", {fooID: fooID});
});
tab.focused && foo.focus();
return false;
case "blah":
.
.
.
}
})-
loadedTriggered when the tab area is connected to it's content element via the
tabContentdirective.Note that waiting on the
loadedevent to callTabAreamethods is not required, since actions not ready to be undertaken are automatically put in a queue and executed later at the appropriate time.
Tab instances are created using the TabArea.load or TabArea.open methods.
When a tab type has a controller specified, it may be injected with it's own Tab instance.
e.g.
function MyTabCtrl (Tab) {
// acquire resources
Tab.on("closed", function () {
// release resources
});
}Display's the tab's content element, sets Tab.focused to true, and triggers the focused event if/when the tab has finished loading. Returns the tab in question.
##### `close` :: `([silent : bool]) : Tab`
When called with Tab.autoClose set to false and no arguments or silent set to false, simply triggers the close event.
When called with silent set to true, closes the tab. This involves:
- Removing the tab's content element from the DOM.
- Destroying the tab's scope (if it has its own)
- Triggering the
closedevent. - Focusing the previously focused tab, if such a tab exists.
Returns the tab in question.
##### `move` :: `(toArea : TabArea, idx : integer) : Tab`
Moves the tab to toArea and positions it at the idxth place.
Returns the tab in question.
##### `deferLoading` :: `() : Tab`
When called by the tab's controller, or before the controller has been executed, deferLoading prevents the automatic triggering of the loaded event.
This is useful if it doesn't make sense to show the tab's content before its controller has had a chance to, e.g. asynchronously fetch some data.
Returns the tab in question.
##### `doneLoading` :: `() : Tab`
Triggers the loaded event and sets Tab.loading to false.
Returns the tab in question.
##### `enableAutoClose` :: `() : Tab`
Sets Tab.autoClose to true and returns the tab in question.
##### `disableAutoClose` :: `() : Tab`
Sets Tab.autoClose to false and returns the tab in question.
-
loadedIf
Tab.deferLoadingwas called, theloadedevent is triggered whenTab.doneLoadingis called. Otherwise it is triggered when the tab's content element has been placed in the DOM. -
dom_readyTriggered when the tab's content element has been placed in the DOM. This is useful if
Tab.deferLoadingwas called. -
focusedTriggered when the tab's content element is shown.
-
closeTriggered when
Tab.close()is called ifTab.autoCloseis set to false. -
closedTriggered when
Tab.close(true)is called, or whenTab.close()is called ifTab.autoCloseis set to true.
As passed into TabArea.load or TabArea.open.
Note that if it is an object and contains non-json-serializable data (e.g. a function), the persistence mechanism will not work. See Tabs.newArea.
As passed into TabArea.load or TabArea.open.
Note that if it is or contains non-json-serializable data (e.g. a function), the persistence mechanism will not work without specifying custom parseOptions and transformOptions functions. See Tabs.newArea.
When set to true, causes Tab.close() to be equivalent to Tab.close(true).
true if the tab's content element is visible, false otherwise. Should not be manually set.
true if the tab has been closed, false otherwise. Should not be manually set.
true if the tab has not finished loading, false otherwise. Should not be manually set.
Restricted to an attribute which should evaluate to a tab area. Registers the element with the TabArea so it knows where to put content elements.
Example:
<div tab-content="myTabArea"></div>Copyright (c) 2014 David Sheldrick. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.