http://rvl.io/adamterlson/
@adamterlson
.
├── app
│ ├── app.js
│ ├── config.js
│ ├── main.js
│ ├── router.js
│ └── styles
│ | └── index.css
├── vendor
│ ├── h5bp
│ │ └── css
│ │ | ├── main.css
│ │ | └── normalize.css
│ └── jam
│ | ├── backbone
│ | ├── backbone.layoutmanager
│ | ├── jquery
│ | ├── lodash
│ | ├── underscore
│ | ├── require.config.js
│ | └── require.js
│ └── js
│ | ├── libs
│ | │ ├── almond.js
│ | │ └── require.js
├── favicon.ico
├── grunt.js
├── package.json
├── index.html
└── test
├── jasmine
└── qunit
No one answer to file structure -- Consistency is most important
var myExampleView = Backbone.View.extend({
// Properties
// Backbone
// Bootstrap
// Rendering
// Backbone Events
// UI Events
// Methods
});
myBusinessLogic: function () {
if (this.myCollection.length && this.someValue) { // Where did these come from?!
// ...
}
},
//... 50 lines later
anotherFunction: function () {
this.someValue = 10;
this.myCollection = new Backbone.Collection(); // Oh, there it is!
},
el
model
collection
options
var myExampleView = Backbone.View.extend({
// Properties
pageSize: 10,
activePage: 0,
pages: null,
additionalCollection: null,
additionalModel: null
});
urlRoot/url
, idAttribute
, defaults
(make this a function by the way), initialize, save/fetch/sync/destroy/etc...
model
, urlRoot/url
, initialize...
template
(though this isn't technically an override), events
, initialize
, id
, className
, tagName...
// Backbone
template: _.template(exampleTemplate),
initialize: function () { },
events: {
'mouseenter': 'onViewMouseEnter', // This binds to $el directly
'submit form': 'onFormSubmit',
'click .switch': 'onSwitchClick',
'change input[name=property]': 'onPropertyInputChange'
}
// Backbone
urlRoot: '/api/exmaple',
idAttribute: 'ExampleId',
defaults: function () {
return {
foo: '',
bar: { baz: 'baz' },
listofstuff: []
};
},
initialize: function () { }
.toJSON()
when passing data to templates// Rendering
render: function () {
this.$el.html(this.template({ person: this.model.toJSON() }));
},
renderSpotA: function () {
this.$('#SpotA').html(...);
},
renderSpotB: function () {
this.$('#SpotB').html(...);
}
on<Object><Event>
// Backbone Events
onModelSync: function () {
this.somePresentationLogic();
this.someMorePresentationLogic();
}
// UI Events
onItemDeleteClick: function (e) {
e.preventDefault();
var itemId = $(e.target).data('itemId');
this.deleteItemById(itemId);
}
initialize: function (options) {
this.pageSize = options.pageSize || this.pageSize;
this.additionalCollection = new Backbone.Collection();
this.listenTo(this.model, 'sync', this.onModelSync)
.listenTo(this.model, 'destroy', this.onModelDestroy)
.listenTo(this.additionalCollection, 'add', this.onAdditionalCollectionAdd);
}
I don't like this.options
--It's not explicit.
// Bootstrap - Hydration with spinner
bootstrap: function () {
this.renderFrame(); //shows spinner(s)
this.primaryModel.fetch().then(this.renderPrimary);
this.secondaryModel.fetch().then(this.renderSecondary);
return this;
}
// Bootstrap - Hydration before any render
bootstrap: function () {
$.when(
this.primaryModel.fetch(),
this.secondaryModel.fetch()
).then(_.bind(this.render, this));
return this;
}
// Bootstrap - No hydration
bootstrap: function () {
return this.render();
}
Testvar myView = new MyView({ el: $spot }); myView.bootstrap();
// or, if you return `this`
var myView = new MyView({ el: $spot }).bootstrap();
test("...", function() {
// Arrange
var myView = new MyView(); // Calls initialize
// Act
var result = myView.functionToTest();
// Assert
strictEqual(result, 1);
});
// Do not do this
presentationLogicAndClickEvent: function (e) {
// ...
this.model.set({ foo: this.$('#foo').val() }); // Mock val?!
// ...
}
// Do not do this
presentationLogicAndClickEvent: function (e) {
// ...
this.$('#foo').val('new value');
// ...
this.render(); // Bye bye
}
render()
around
Silence (Delay < 0.9.10) events with { silent: true }initialize: function () { this.listenTo(this.model, 'change', this.onModelChange); }, onModelChange: function () { this.render(); },
//... presentationLogic: function (value) { this.model.set({ foo: value }); // trigger change, calls render }
// Backbone
template: _.template(frameTemplate),
// Rendering
render: function () {
this.$el.html(this.template());
this.renderItemList();
},
renderItemList: function () {
var $ul = $('<ul/>');
this.itemsCollection.each(function (item) {
var itemView = new ItemView({ tagName: 'li', model: item }).bootstrap();
$ul.append(itemView.$el);
});
this.$('#itemList').html($ul);
}
Construct subviews in render or initialize
// Backbone
templates: {
frame: _.template(frameTemplate),
order: _.template(orderTemplate),
user: _.template(userTemplate)
},
initialize: function () {
this.orderModel = new orderModel();
this.userModel = new userModel();
this.listenTo(this.orderModel, 'change', this.onOrderModelChange);
this.listenTo(this.userModel, 'change', this.onUserModelChange);
},
// Bootstrap
bootstrap: function () {
$.when(
this.orderModel.fetch(),
this.userModel.fetch()
).then(this.render);
return this;
},
// Rendering
render: function () {
this.$el.html(this.templates.frame());
this.renderOrder();
this.renderUser();
return this;
},
renderOrder: function () {
this.$('#order').html(this.templates.order({
order: this.orderModel.toJSON()
}));
},
renderUserInformation: function () {
this.$('#user').html(this.templates.user({
user: this.userModel.toJSON()
}));
}
// Backbone Events
onOrderModelChange: function () {
this.renderOrder();
},
onUserModelChange: function () {
this.renderUser();
}
router
├── view 1
│ ├── template
│ └── model
└── view 2
├── template
├── collection
│ └── model
└── first child view
├── template
├── model
└── second child view
└── template
// In parent view
renderModalForm: function () {
var modalForm = new ModalFormView();
modalForm.bootstrap().then(this.onModalFormDone);
},
// In child view
_dfd: null,
bootstrap: function () {
this._dfd = $.Deferred();
this.render();
return this._dfd.promise();
},
something: function () {
this._dfd.resolve(someObj);
}
_dfd: null,
constructor: function () {
this._dfd = $.Deferred();
this._dfd.promise(this); // Make this view a promise!
this.always(_.bind(this.remove, this)); // Cleanup
Backbone.View.prototype.constructor.apply(this, arguments);
}
something: function () {
this._dfd.resolve();
}
// In parent view
renderModalForm: function () {
var modalForm = new ModalFormView({ model: this.model });
this.listenTo(modalForm, 'saved', this.onModalFormSaved);
modalForm.bootstrap();
},
// In child view
something: function () {
this.trigger('saved', this.model);
}
el
, Not The Live DOMvar MyView = Backbone.View.extend({
template: _.template('<div id="foo">initial text</div>'),
initialize: function () {
this.render();
},
render: function () {
this.$el.html(this.template());
$('#foo').html('changed text');
}
});
var instance = new MyView();
$('#somespot').append(instance.$el);
this
var instance = new MyView({ el: this.$('#MyViewSpot') }).render();
Warning: Giving an el which is already attached will paint immediately.
Warning: remove()
will remove el
, which isn't always desired.
var instance = new MyView().render();
this.$('#MyViewSpot').html(instance.$el);
var view = Backbone.View.extend({
el: $('#MyViewSpot')
});
Views shouldn't know about the DOM, remember?var view = new Backbone.View();
view.setElement($someElement);
This is ~50% slower.var view = Backbone.View.extend({
events: {
'mouseenter': 'onViewMouseEnter', // bound directly on $el
'click #foo': 'onFooClick', // delegated on $el
'submit form': 'onFormSubmit', // delegated on $el
'scroll .some-div': 'onSomeDivScroll' // will fail, scroll doesn't bubble
}
});
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
var MyView = Backbone.View.extend({
template: _.template('<div id="foo">initial text</div>'),
initialize: function () {
this.render();
},
render: function () {
this.$el.html(this.template());
$('#foo').html('changed text');
}
});
var instance = new MyView();
$('#somespot').append(instance.$el);
Sync
(and derivative) overrides
test('...', function () {
expect(100000000000000);
var foo = bar;
equal(foo.myFunctionToTest(), 'somevalue');
foo.someLogic();
equal(foo.property, 'newvalue');
var baz = foo.createBaz();
ok(baz.property);
// and on and on and on...
});
test('...', function () {
// Arrange
// Object creation
// Act
// Execute method being tested
// Assert
// *All* of your assertions
});
test('...', function () {
// Arrange
var foo = new bar();
// Act
var result = foo.someLogic();
// Assert
strictEqual(result, 'some value');
});
test('...', function () {
var page = $('<div><a href="#"></a></div>');
var model = new MyModel({ id: 10 });
var view = new MyView({ el: page, model: model });
page.find('a').click();
ok(view.model.get('disabled'));
model.set('something', false);
equal(view.foo, 'bar');
//etc
});
var MyView = Backbone.View.extend({
initialize: function () { },
render: function () { },
someFunction: function () {
return this.model.fetch().done(this.render);
}
});
module("...", {
setup: function () {
this.sandbox = sinon.sandbox.create();
},
teardown: function () {
this.sandbox.restore();
}
});
test("Positive Test", function() {
// Arrange
var dfd = $.Deferred();
var promise = dfd.promise();
var model = new Backbone.Model();
var instance = new MyView({ model: model });
this.sandbox.stub(instance, 'render');
this.sandbox.stub(model, 'fetch').returns(promise);
dfd.resolve();
// Act
var result = instance.someFunction();
// Assert
strictEqual(result, promise);
ok(instance.render.called);
});
test("Negative Test", function() {
// Arrange
var dfd = $.Deferred();
var promise = dfd.promise();
var model = new Backbone.Model();
var instance = new MyView({ model: model });
this.sandbox.stub(instance, 'render');
this.sandbox.stub(model, 'fetch').returns(promise);
dfd.reject();
// Act
var result = instance.someFunction();
// Assert
strictEqual(result, promise);
ok(!instance.render.called);
});
var MyView = Backbone.View.extend({
initialize: function (options) {
this.foo = options.foo;
this.additionalCollection = new Backbone.Collection();
this.model.on('sync', this.onModelSync);
this.someFunction();
},
someFunction: function () { },
onModelSync: function () { }
});
module("...", {
setup: function () {
this.sandbox = sinon.sandbox.create();
},
teardown: function () {
this.sandbox.restore();
}
});
test("...", function() {
// Arrange
var someFunctionStub = this.sandbox.stub(MyView.prototype, 'someFunction');
var options = {
foo: 'somevalue',
model: new Backbone.Model()
};
this.sandbox.stub(options.model, 'on');
// Act
var instance = new MyView(options);
// Assert
ok(someFunctionStub.calledOnce);
ok(instance.additionalCollection instanceof Backbone.Collection);
strictEqual(instance.foo, options.foo);
ok(options.model.on.calledWith('sync', instance.onModelSync));
});