JSDoc3 with Backbone and RequireJS (Models and Collections)
Recently I've been on a mission. I call this mission "The 'What happens to my project if I get hit by a bus?' Mission."
You see, I am the only front-end engineer, developer, and designer on my team. We spent the bulk of last year feverishly releasing feature after feature just to ensure the project didn't lose funding. However, in order to do this I all but neglected documenting anything about my browser-side code.
The Problem
While I've been very diligent in creating a maintainable code base and most of it is pretty straight-forward, I had no documentation around the event-driven parts of my application. To anyone else, it would be very hard to follow the flow of how and what happens when events are triggered.
I decided that I would start with a good code documentation tool. In the past I'd used JSDoc-Toolkit and was pleasantly surprised to see that it had been ported to NodeJS and continued on as JSDoc3.
The awesome part is that it includes several tags for documenting event driven code. The not-so-awesome part is that the tool's usage with Require and Backbone/Marionette is...well...it's less than intuitive at best. This is compounded by spotty documentation and seemingly contradictory advice on Stack Overflow and their Google Group.
The (My) Solution
What I quickly learned is that using JSDoc3 on your RequireJS + Backbone project is less of a science and more of an art. Depending on the coding style, you may be forced to override the functionality of many of the tags. Doing so gives you a lot of flexibility but also requires more work to be done by hand. You can also use this flexibility to tweek how the documentation looks in the generated documentation webpages.
The following is a brief overview of how I chose to implement JSDoc on my project. I'd encourage readers to experiment and explore if this isn't exactly what you're looking for.
The example project structure I'm going to use is as follows:
App
-Module1
-models
-model1.js
-collections
-collection1.js
-collection2.js
-views
-view1.js
-module1.js
-vent.js
-reqres.js
Models and Collections (Basic)
Lets start with the most basic documentation, a require definition that defines only one model or collection.
Basic Model
The first thing you have to do is to tell JSDoc what this require definition is defining. This is done by placing an @exports
tag prior to your define block.
/**
* @exports module:Module1.Module1/Models/Model1
*/
define(['backbone'], function (Backbone) {
"use strict";
...
});
You'll notice the module:Module1.
prefixing the model's path. If you're using a module or otherwise subdividing your code, you'll want to use this to ensure that the documentation webpage reflects this subdivision.
Also of note is that I'm using capitals for the path. This is partly due to the convention of capitalizing class names, but for me it is mainly stylistic for the webdocs.
Next, we need to define the model as a class. We do that using the @constructor
tag.
/**
* @name module:Module1.Module1/Models/Model1
* @constructor
* @augments Backbone.Model
*/
return Backbone.Model.extend({
...
});
Here also use the @name
tag to make sure that the class's name matches up with the name we told JSDoc that we're exporting and the @augments
tag to specify that we're just adding onto the Backbone.Model object.
Lastly, we need to tell JSDoc that the object being added to Backbone.Model via .extend()
is actually the same thing as the class we just told it about. This is done using some tag placement trickery and the @lends
tag. It looks like this...
return Backbone.Model.extend(
/** @lends module:Module1.Module1/Models/Model1.prototype **/
{
...
});
Be careful to place the tag comment inside the extend
openning parenthesis and the object definition's opening curly-bracket.
Once all of this is taken care of, JSDoc can figure out all of the attributes and functions you add in the model's definition using basic JSDoc techniques.
For example, adding a URLRoot function to the model would look like this...
return Backbone.Model.extend(
/** @lends module:Module1.Module1/Models/Model1.prototype **/
{
/**
* Specify the URL root to fetch from
* @returns {string}
*/
urlRoot: function(){
if(this.get("isCool")){
return "../cool/"
}
else{
return "../not-cool/";
}
}
});
Putting it all together...
model1.js
/**
* @exports module:Module1.Module1/Models/Model1
*/
define(['backbone'], function (Backbone) {
"use strict";
/**
* @name module:Module1.Module1/Models/Model1
* @constructor
* @augments Backbone.Model
*/
return Backbone.Model.extend(
/** @lends module:Module1.Module1/Models/Model1.prototype **/
{
/**
* Specify the URL root to fetch from
* @returns {string}
*/
urlRoot: function(){
if(this.get("isCool")){
return "../cool/"
}
else{
return "../not-cool/";
}
}
});
});
Basic Collection
Basic collections are documented very similarly to basic model definitions. However, there is one concept that is added: linking in your model in the documentation.
Documenting the link between the collection's model and the already documented model definition also links them on the generated documentation website, which is very convenient.
The linking is done via the @type
tag.
collection1.js
/**
* @exports module:Module1.Module1/Collections/Collection1
*/
define(['backbone', 'models/model1'], function (Backbone, Model1) {
"use strict";
/**
* @name module:Module1.Module1/Collections/Collection1
* @constructor
* @augments Backbone.Collection
*/
return Backbone.Collection.extend(
/** @lends module:Module1.Module1/Collections/Collection1.prototype */
{
/**
* This collection contains model1s
* @type {module:Module1.Module1/Models/Model1}
*/
model: Model1,
/** URL to item list */
url: "/collection1/"
});
});
Models and Collections (Advanced)
Sometimes you need/want to create multiple models, collections, or both in the same require definition. This can be a bit tricky to document in JSDoc.
My most common use case for this is with ItemViews and CollectionViews, which I'll cover in part II. Another common use case for me - and the one I'm going to use here for an example - is when I have a model that is tightly coupled to a collection (meaning that the model only exists for the purpose of filling this one particular collection).
In this scenario the file define's and returns a collection but also creates an internal model to be used by the collection. This seems to confuse JSDoc, so we have to give it a little extra help.
First, document the definition just as we did before. With the exception that we no longer need to reference an external model.
collection2.js
/**
* @exports module:Module1.Module1/Collections/Collection2
*/
define(['backbone'], function (Backbone) {
"use strict";
...
});
Then, we create the model.
/**
* @exports module:Module1.Module1/Collections/Collection2
*/
define(['backbone'], function (Backbone) {
"use strict";
/**
* @name module:Module1.Module1/Models/Model2
* @constructor
* @augments Backbone.Model
*/
var Model2 = Backbone.Model.extend(
/** @lends module:Module1.Module1/Models/Model2.prototype **/
{
/**
* Specify the URL root to fetch from
* @returns {string}
*/
urlRoot: function(){
if(this.get("isAdvanced")){
return "../cool/"
}
else{
return "../not-cool/";
}
}
});
...
});
You'll notice that Model2 is tagged in the same way that the stand alone Model1 is above.
Now comes the trickery. After this point, JSDoc really gets confused. We document the collection just like we did above with a "basic" collection, but unfortunately, we have to help it by adding in a @memberof!
tag to each of its attributes. The !
at the end of the tag serves to tell JSDoc that it needs to listen to this tag and not try to figure things out on its own.
Here's what the whole thing looks like:
collection2.js
/**
* @exports module:Module1.Module1/Collections/Collection2
*/
define(['backbone'], function (Backbone) {
"use strict";
/**
* @name module:Module1.Module1/Models/Model2
* @constructor
* @augments Backbone.Model
*/
var Model2 = Backbone.Model.extend(
/** @lends module:Module1.Module1/Models/Model2.prototype **/
{
/**
* Specify the URL root to fetch from
* @returns {string}
*/
urlRoot: function(){
if(this.get("isAdvanced")){
return "../cool/"
}
else{
return "../not-cool/";
}
}
});
/**
* @name module:Module1.Module1/Collections/Collection2
* @constructor
* @augments Backbone.Collection
*/
return Backbone.Collection.extend(
/** @lends module:Module1.Module1/Collections/Collection2.prototype */
{
/**
* This collection contains model1s
* @memberof! module:Module1.Module1/Collections/Collection2
* @type {module:Module1.Module1/Models/Model2}
*/
model: Model2,
/**
* URL to item list
* @memberof! module:Module1.Module1/Collections/Collection2
*/
url: "/collection2/"
});
});
Final Thoughts
Documenting this flavor of Marionette code with JSDoc can present some challenges and can be a downright hassle at times. However, the generated documentation webpage and in-code documentation provide clarity of intent and make it well worth it.
In part 2 I will cover the documentation of Views and Event-Driven code.
On Comments
I grew tired of the 1000's of spam bot comments on my previous blog engine so feel free to contact me on Twitter ( @d_naish ) if you like this article, hate it, agree with me, or think I've got it all wrong...or just want to say hi.