Example – Setup – Basic navigation – Navigating with selections – Behaviours – Guidelines – Build and test
Backbone.Cycle is a set of mixins for Backbone models and collections. Models gain the ability to be selected, and collections handle those selections. Methods for navigating a collection are also part of the package, such as model.ahead(3)
, collection.selectNext()
, collection.prev()
, collection.prevNoLoop()
.
With Backbone.Cycle options, you can enable predefined, common behaviours, like always selecting the first item in a new collection, or selecting an adjacent model when a selected model is removed. Models can be shared across multiple collections, and selections are synced among them.
If you are a happy user of this project already, you can support its development by donating to it. You absolutely don't have to, of course, but perhaps it is something you might actually want to do.
Backbone.Cycle is built on top of Backbone.Select. The selection features are identical. Backbone.Cycle adds navigation methods and options.
-
Backbone.Select is designed with a minimal surface area. As few methods as possible are added to your objects. Basically, all you can do is
select
anddeselect
. The idea is that you should be able to use Backbone.Select mixins pretty much everywhere, with a near-zero risk of conflicts. -
With Backbone.Cycle, you get a little more in terms of methods and behaviour. It may often be more convenient to use and probably is the better choice for a greenfield project.
Perhaps the best way to explain what Backbone.Cycle does, before getting into the details, is an example.
// --- (1) Setup ---
var Model = Backbone.Model.extend( {
initialize: function () {
// Applies the mixin:
Backbone.Cycle.SelectableModel.applyTo( this );
}
} );
var Collection = Backbone.Collection.extend( {
initialize: function ( models, options ) {
// Applies the mixin:
Backbone.Cycle.SelectableCollection.applyTo( this, models, options );
}
} );
var m1 = new Model( {id: "m1"} ),
m2 = new Model( {id: "m2"} ),
m3 = new Model( {id: "m3"} );
// --- (2) Defining behaviours ---
var collection = new Collection(
[m1, m2, m3],
{ autoSelect: "first", selectIfRemoved: "next" }
);
console.log( collection.selected.id ); // prints "m1" because of autoSelect: "first"
// --- (3) Navigating ---
console.log( m2.next().id ); // prints "m3"
console.log( m1.ahead( 2 ).id ); // prints "m3"
collection.selectNext(); // selects m2
// --- (4) Removal behaviour ---
collection.remove( m2 );
console.log( collection.selected.id ); // prints "m3" because of selectIfRemoved: "next"
If you are not familiar with Backbone.Select, you should have a look there, too.
There is an interactive demo you can play around with. The demo is kept simple, and is a good way to explore the features of Backbone.Cycle. Check it out at JSBin or Codepen.
In addition, there are many more interactive demos for Backbone.Select.
Underscore, Backbone and Backbone.Select are the only dependencies. Include backbone.cycle.js when they are loaded.
The stable version of Backbone.Cycle is available in the dist
directory (dev, prod). If you use Bower, fetch the files with bower install backbone.cycle
. With npm, it is npm install backbone.cycle
.
When loaded as a module (e.g. AMD, Node), Backbone.Cycle does not export a meaningful value. It solely lives in the Backbone namespace.
There are three components in this package:
- Backbone.Cycle.SelectableModel and Backbone.Cycle.SelectableCollection are used together. They provide the full feature set.
- Backbone.Cycle.Model just offers basic navigation features, and does not support making selections.
Backbone.Cycle.SelectableModel and Backbone.Cycle.SelectableCollection provide you with methods for navigation, and for selecting items. The collection also gives you options for automatic selections.
Both components must be used together. The model mixin, Backbone.Cycle.SelectableModel, has to be applied to all models in a Backbone.Cycle.SelectableCollection.
If you don't take care of that yourself, it happens automatically when models, or sets of raw model data, are added to a SelectableCollection. That mechanism is the same as in Backbone.Select. See there for more – including cases where you are better off applying the model mixin yourself.
Backbone.Cycle.SelectableModel and Backbone.Cycle.SelectableCollection inherit the features of Backbone.Select. A SelectableCollection allows one model to be selected at a time; it is derived from Backbone.Select.One.
As a result, you can select()
models, retrieve the selected
model from a collection, listen to reselect:one
or deselected
events, implement an onSelect
event handler etc. See the Backbone.Select documentation for more on selection-related methods, properties and events.
Backbone.Cycle.SelectableModel adds two kinds of navigation methods to a Backbone model.
model.next()
, model.prev()
, model.ahead(n)
, model.behind(n)
.
Looping navigation methods return the next or previous model in the collection, relative to the model the method is called on. ahead
and behind
return a model which is n items ahead or back. Once the final item of the collection is reached, the methods loop and continue from the first item, or from the last item when moving in the opposite direction.
model.nextNoLoop()
, model.prevNoLoop()
, model.aheadNoLoop(n)
, model.behindNoLoop(n)
.
When you try to access a model beyond the boundaries of the collection, these methods return undefined
.
Navigation always happens in the context of a collection. That collection is referenced in the collection
property of the model. If a model is part of more than one collection, model.collection
refers to the one the model was added to first.
You can override the collection context, though. Just pass a collection as an argument to any of the above methods. For instance, model.ahead(5, otherCollection)
returns the model which is five items ahead of model
in otherCollection
. Likewise, you'd call next
with a collection context as model.next(otherCollection)
.
The basic usage, plain and simple:
var Model = Backbone.Model.extend( {
initialize: function ( attributes, options ) {
Backbone.Cycle.SelectableModel.applyTo( this, options );
}
} );
var m1 = new Model( {id: "m1"} ),
m2 = new Model( {id: "m2"} ),
m3 = new Model( {id: "m3"} );
var collection = new Backbone.Collection( [m1, m2, m3] );
console.log( m2.next().id ); // prints "m3"
console.log( m1.ahead( 2 ).id ); // prints "m3"
If you share models among multiple collections, specify the collection:
// Model order is reversed in otherCollection
var otherCollection = new Backbone.Collection( [m3, m2, m1] );
console.log( m2.next( otherCollection ).id ); // prints "m1"
console.log( m3.ahead( 2, otherCollection ).id ); // prints "m1"
The methods of a SelectableCollection match those of a SelectableModel.
There is a difference, though. In a collection, navigation happens relative to the selected model. By contrast, navigation methods called on a model are relative to that model. A selection doesn't matter in that context.
collection.next( [options] )
, collection.prev( [options] )
collection.ahead(n, [options] )
, collection.behind(n, [options] )
.
The next
and prev
methods return the next or previous model in the collection, relative to the selected model. Likewise, ahead
and behind
return a model n items ahead or back.
Once the final item of the collection is reached, the methods loop and continue from the first item, or from the last item when moving in the opposite direction.
The looping navigation methods support the following option: label
.
collection.nextNoLoop( [options] )
, collection.prevNoLoop( [options] )
collection.aheadNoLoop(n, [options] )
, collection.behindNoLoop(n, [options] )
.
When you try to access a model beyond the boundaries of the collection, these methods return undefined
.
The non-looping navigation methods support the following option: label
.
collection.selectNext( [options] )
, collection.selectPrev( [options] )
collection.selectNextNoLoop( [options] )
, collection.selectPrevNoLoop( [options] )
.
Instead of returning the model, these methods select it. (They return the collection, and thus allow chaining.)
Looping methods always succeed. By contrast, if you call a non-looping, select*NoLoop
method to select a model beyond the boundaries of the collection, the method is a no-op, and the selection remains unchanged.
The selection methods support the following options: silent
, label
.
selectAt(n)
.
An unrelated convenience method, selects the model at index n.
The method returns the collection, and allows chaining.
Navigation methods, like next()
, appear in SelectableModel and SelectableCollection. Keep in mind, though, that SelectableModel methods calculate positions relative to the model they are invoked on. By contrast, SelectableCollection methods act relative to the selected model in the collection.
Unsurprisingly, then, SelectableCollection methods require that a model has been selected in the collection. Otherwise, an error is thrown. The only exception is selectAt
, which is purely index-based and works without an existing selection.
As mentioned above, the collection methods determine their destination based on the selected model. And by "selected model", we mean the one implied by the default label of the collection.
(Out of the box, that default label is "selected"
. Read more about labels in the Backbone.Select documentation.)
To base the navigation, or selection, on a different label, pass in an options object with its label
property set.
collection.ahead( 3, { label: "starred" } ); // => relative to the model selected
// with label "starred"
collection.selectNext( { label: "picked" } ); // => selects a model with label "picked",
// relative to the current "picked" model
Backbone.Cycle.SelectableModel and Backbone.Cycle.SelectableCollection must be used together. Only SelectableModels can be added to a SelectableCollection.
If you don't apply the SelectableModel mixin yourself, it happens automatically when models, or sets of raw model data, are added to a SelectableCollection. That mechanism is the same as in Backbone.Select. See there for more – including cases where you are better off applying the model mixin yourself.
Mixins are applied in initialize
:
// Collection mixin. You must apply this mixin.
var Collection = Backbone.Collection.extend( {
initialize: function ( models, options ) {
Backbone.Cycle.SelectableCollection.applyTo( this, models, options );
}
} );
// Model mixin. This is optional.
//
// You can also pass in ordinary models, or raw model data, and
// rely on automatic mixin application when the models are added
// to the collection.
var Model = Backbone.Model.extend( {
initialize: function ( attributes, options ) {
Backbone.Cycle.SelectableModel.applyTo( this, options );
}
} );
The Backbone.Cycle.SelectableCollection applyTo()
signature is:
Backbone.Cycle.SelectableModel.applyTo( collection, models, [options] );
You can pass an options hash as the third parameter to .applyTo()
. Backbone.Cycle.SelectableCollection recognizes the options of a Backbone.Select.One collection, and the following ones in addition: autoSelect
, selectIfRemoved
.
The Backbone.Cycle.SelectableModel applyTo()
signature is:
Backbone.Cycle.SelectableModel.applyTo( model, [options] );
You can pass an options hash as the second parameter to .applyTo()
. Backbone.Cycle.SelectableModel recognizes the options of Backbone.Select.Me models, and passes them on to the Select.Me mixin internally.
Even though Backbone.Cycle depends on Backbone.Select, there is no need to apply the Backbone.Select mixins in initialize
. The Backbone.Cycle mixins do that themselves, behind the scenes.
Backbone.Cycle.SelectableCollection allows only one selected item at a time. It is based on Backbone.Select.One. Its features make less sense if there are multiple selected items in a collection, so there is no corresponding component for Backbone.Select.Many in Backbone.Cycle.
When a SelectableCollection mixin is created with applyTo
, you can pass an options object to it. Options define the behaviour when models are passed to a collection, removed from it, or when models are shared among multiple collections.
The use of options is demonstrated in the introductory example. A SelectableCollection supports all options of a Backbone.Select.One collection, as well as the ones listed below.
Choices: "first"
, "last"
, model index, "none"
(default).
Set autoSelect
to "first"
if you want the first model in a collection to be selected automatically. You can also set the option to "last"
, or provide the index of the model you'd like to see auto-selected. If the index does not exist at the time, that's fine – autoSelect
just won't select anything then.
The autoSelect
setting kicks in when the initial set of models is passed to a collection – be it to the constructor, or with add()
, set()
, or reset()
. Auto select can also be triggered later on in the lifecycle of a collection: when you call add()
, set()
or reset()
while there is no selection in the collection.
It's important to note that autoSelect
will only spring into action when you add models to a collection, or when you reset it. The option won't guarantee that there is a selected item all the time. If you simply deselect a model, nothing will happen unless you add()
or reset()
later on.
If autoSelect
selects a model while the collection is being instantiated, or when models are passed to add()
or set()
, all selection-related events fire as usual.
During a reset()
, however, select:one
and deselect:one
events are silenced in the collection which is being reset. This is standard behaviour in Backbone.Select, and it also applies to selections made by autoSelect
.
The silencing effect is strictly limited to the collection being reset, though. The selected
and deselected
events on a model are triggered even during a reset. So are select:*
and deselect:one
events in other collections sharing the models. Again, this is how things always work in Backbone.Select.
Finally, if you call add()
or set()
with the silent
option, selection-related events are silenced as well.
When you set autoSelect
to a string, the option only affects the default label of the collection (usually "selected"
). Other, secondary labels are not treated to auto selection magic then.
However, instead of setting autoSelect
to a primitive value, you can set it to a hash. That hash must detail which labels you want auto selection behaviour for, and what that behaviour should be.
Backbone.Cycle.SelectableCollection.applyTo( this, models, {
autoSelect: { selected: "first", starred: "first", picked: "last" }
} );
If you want auto selection for the default label, make sure to include it explicitly in the hash, as seen above (selected: "first"
).
autoSelect
may have a performance impact when adding items to really large collections. Those are better handled without autoSelect
magic, at least if you add items frequently. The negative effect is limited to actual add()
calls, though – resets are not affected.
The autoSelect
option is off by default, with value autoSelect: "none"
.
Choices: "prev"
, "next"
, "prevNoLoop"
, "nextNoLoop"
, "none"
(default).
Use selectIfRemoved
if you want to select another model when the selected model is removed from the collection. The option value determines which model gets selected: "prev"
, "next"
, "prevNoLoop"
, "nextNoLoop"
.
The selectIfRemoved
setting responds to a remove()
call on the collection. It does not respond to reset()
. And obviously, if a model is shared with other collections, removing it from those other collections doesn't somehow trigger the local selectIfRemoved
behaviour.
If the behaviour causes to a model to be selected, all selection-related events fire as usual. However, if you call remove()
with the silent
option, the selection-related events are silenced as well.
When you set selectIfRemoved
to a string, the option only affects the default label of the collection (usually "selected"
). Other, secondary labels are not treated to auto selection magic then.
But just like autoSelect
, you can set selectIfRemoved
to a hash instead. That hash must detail which labels you want auto selection behaviour for, and what that behaviour should be.
Backbone.Cycle.SelectableCollection.applyTo( this, models, {
selectIfRemoved: { selected: "next", starred: "next", picked: "prevNoLoop" }
} );
Again, if you want auto selection for the default label, make sure to include it explicitly in the hash, as seen above (selected: "next"
).
The option is off by default, with value selectIfRemoved: "none"
.
The guidelines for Backbone.Cycle are the same as for Backbone.Select. Still, it may be worthwile to repeat the most important rule here.
When a collection is no longer in use, call close()
on it. That avoids memory leaks and ensures proper selection handling when models are shared between collections.
So don't just replace a collection like this:
var collection = new MySelectableCollection( [model] );
// ... do stuff
collection = new MySelectableCollection( [model] ); // WRONG!
Instead, call close()
before you let an obsolete collection fade away into oblivion:
var collection = new MySelectableCollection( [model] );
// ... do stuff
collection.close();
collection = new MySelectableCollection( [model] );
Check out the introductory example.
If you don't need to make selections, and you don't use Backbone.Cycle.SelectableCollection, you can keep things simple with a stripped-down model mixin. Backbone.Cycle.Model provides navigation methods, and there it ends.
Backbone.Cycle.Model exposes the same navigation methods as Backbone.Cycle.SelectableModel. Call next()
, ahead(n)
etc as described above.
Backbone.Cycle.Model does not inherit Backbone.Select functionality, though, and does not support making selections. Do not use it in collections which have the SelectableCollection mixin applied.
Backbone.Cycle.Model is applied to a model in initialize
:
var Model = Backbone.Model.extend( {
initialize: function () {
Backbone.Cycle.Model.applyTo( this );
}
} );
The Backbone.Cycle.Model applyTo()
signature is:
Backbone.Cycle.Model.applyTo( model );
The method does not accept an options argument.
See the examples for Backbone.Cycle.SelectableModel.
If you'd like to fix, customize or otherwise improve the project: here are your tools.
npm sets up the environment for you.
- The only thing you've got to have on your machine (besides Git) is Node.js. Download the installer here.
- Clone the project and open a command prompt in the project directory.
- Run the setup with
npm run setup
. - Make sure the Grunt CLI is installed as a global Node module. If not, or if you are not sure, run
npm install -g grunt-cli
from the command prompt.
Your test and build environment is ready now. If you want to test against specific versions of Backbone or Backbone.Select, edit bower.json
first.
The test tool chain: Grunt (task runner), Karma (test runner), Mocha (test framework), Chai (assertion library), Sinon (mocking framework). The good news: you don't need to worry about any of this.
A handful of commands manage everything for you:
- Run the tests in a terminal with
grunt test
. - Run the tests in a browser interactively, live-reloading the page when the source or the tests change:
grunt interactive
. - If the live reload bothers you, you can also run the tests in a browser without it:
grunt webtest
. - Run the linter only with
grunt lint
orgrunt hint
. (The linter is part ofgrunt test
as well.) - Build the dist files (also running tests and linter) with
grunt build
, or justgrunt
. - Build continuously on every save with
grunt ci
. - Change the version number throughout the project with
grunt setver --to=1.2.3
. Or just increment the revision withgrunt setver --inc
. (Remember to rebuild the project withgrunt
afterwards.) grunt getver
will quickly tell you which version you are at.
Finally, if need be, you can set up a quick demo page to play with the code. First, edit the files in the demo
directory. Then display demo/index.html
, live-reloading your changes to the code or the page, with grunt demo
. Libraries needed for the demo/playground should go into the Bower dev dependencies – in the project-wide bower.json
– or else be managed by the dedicated bower.json
in the demo directory.
The grunt interactive
and grunt demo
commands spin up a web server, opening up the whole project to access via http. So please be aware of the security implications. You can restrict that access to localhost in Gruntfile.js
if you just use browsers on your machine.
In case anything about the test and build process needs to be changed, have a look at the following config files:
karma.conf.js
(changes to dependencies, additional test frameworks)Gruntfile.js
(changes to the whole process)web-mocha/_index.html
(changes to dependencies, additional test frameworks)
New test files in the spec
directory are picked up automatically, no need to edit the configuration for that.
To my own surprise, a kind soul wanted to donate to one of my projects, but there hadn't been a link. Now there is.
Please don't feel obliged in the slightest. The license here is MIT, and so it's free. That said, if you do want to support the maintenance and development of this component, or any of my other open-source projects, I am thankful for your contribution.
Naturally, these things don't pay for themselves – not even remotely. The components I write aim to be well tested, performant, and reliable. These qualities may not seem particularly fascinating, but I put a lot of emphasis on them because they make all the difference in production. They are also rather costly to maintain, time-wise.
That's why donations are welcome, and be it as nod of appreciation to keep spirits up. Thank you!
Changes:
- Removed the separate AMD/Node builds in
dist/amd
. Module systems and browser globals are now supported by the same file,dist/backbone.cycle.js
(or.min.js
) - Updated to Backbone.Select 2.1, as a minimum requirement. Model sharing is always enabled now. Remember to call
.close()
when you no longer use a collection. - No more restrictions on using the
silent
option when callingadd()
,remove()
orreset()
on a collection. TheautoSelect
andselectIfRemoved
options now respond tosilent
calls.
Other:
- Collections are able to process the full set of input types during instantiation, and when calling
add()
,set()
, andreset()
. Collections now accept attribute hashes, raw model data requiringoptions.parse
, and models without the SelectableModel mixin applied. Previously, only models with the SelectableModel mixin have been accepted.
- Version is exposed in
Backbone.Cycle.version
- AMD demo allows testing r.js output
- Updated bower.json, package.json for Backbone 1.3.x
- Fixed
select:one
event being fired byautoSelect
during a reset, is silent now
- Made methods chainable which select a model
- Added
label
support
- Fixed compatibility with Backbone 1.2.x
- Added an
applyTo
setup method for Backbone.Cycle.Model, protecting the mixin from unintentional modification. The setup method must be used – applying the mixin just by extending the host model no longer works. - Fixed compatibility with Underscore 1.7.0
- Switched to plain objects as mixins internally
- Renamed
initialSelection
toautoSelect
;initialSelection
is deprecated but kept around as an alias in 1.x autoSelect
no longer triggers a deselection event under any circumstancesautoSelect
now accepts values "last" or an item index
- Minor bug and documentation fixes
- Fleshed out package.json for npm installs
- Minor bug fixes
- Relaxed dependency requirements
- Added _cycleType property to identify mixins in a model or collection
- Fixed line endings in minified AMD build, added source map
- Updated Backbone.Select dependency
- Fixed bower.json ignore list
- Fixed typos in readme
- Added documentation
- Initial version
MIT.
Copyright (c) 2014-2017 Michael Heim.