var MyView = Backbone.View.extend({
action: function() {
console.log('action');
},
});
Can i override method at initialization, like:
var my_special_view = new MyView({
action: function() {
console.log('new action');
},
})
You can create your own custom views and then extend that instead of Bacbone.View.
For example
var MySpecialView = MyView.extend({
action: function() {
console.log('new action');
}
})
var my_special_view = new MySpecialView();
If you really just want to override a method for a specific instance without creating your own custom view, you can directly override the function on the instance. For example
var my_special_view = new MyView();
my_special_view.action = function () {
console.log('new action');
};
You can do
var my_special_view = new MyView();
my_special_view.action = function() {
console.log('new action');
};
It just creates an instance of your view then redefine its 'action' method. I think it's exactly what you want because it enables you to use this implementation of 'action' just once, so it is really a special view.
Plus, it is a javascript features so you can use this with any library/framework you use.
Related
I have a config.json that I am going to load into my app as a Backbone Model like:
var Config = Backbone.Model.extend({
defaults: {
base: ''
},
url: 'config.json'
});
Other models should be dependent on some data contained in Config like:
var ModelA = Backbone.Collection.extend({
initialize: function(){
//this.url should be set to Config.base + '/someEndpoint';
}
});
In above example, ModelA's url property is dependent on Config's base property's value.
How do I go about setting this up properly in a Backbone app?
As I see it, your basic questions are:
How will we get an instance of the configuration model?
How will we use the configuration model to set the dependent model's url?
How can we make sure we don't use the url function on the dependent model too early?
There are a lot of ways to handle this, but I'm going to suggest some specifics so that I can just provide guidance and code and "get it done," so to speak.
I think the best way to handle the first problem is to make that configuration model a singleton. I'm going to provide code from backbone-singleton GitHub page below, but I don't want the answer to be vertically long until I'm done with the explanation, so read on...
var MakeBackboneSingleton = function (BackboneClass, options) { ... }
Next, we make a singleton AppConfiguration as well as a deferred property taking advantage of jQuery. The result of fetch will provide always(callback), done(callback), etc.
var AppConfiguration = MakeBackboneSingleton(Backbone.Model.extend({
defaults: {
base: null
},
initialize: function() {
this.deferred = this.fetch();
},
url: function() {
return 'config.json'
}
}));
Now, time to define the dependent model DependentModel which looks like yours. It will call AppConfiguration() to get the instance.
Note that because of MakeBackboneSingleton the follow is all true:
var instance1 = AppConfiguration();
var instance2 = new AppConfiguration();
instance1 === instance2; // true
instance1 === AppConfiguration() // true
The model will automatically fetch when provided an id but only after we have completed the AppConfiguration's fetch. Note that you can use always, then, done, etc.
var DependentModel = Backbone.Model.extend({
initialize: function() {
AppConfiguration().deferred.then(function() {
if (this.id)
this.fetch();
});
},
url: function() {
return AppConfiguration().get('base') + '/someEndpoint';
}
});
Now finally, putting it all together, you can instantiate some models.
var newModel = new DependentModel(); // no id => no fetch
var existingModel = new DependentModel({id: 15}); // id => fetch AFTER we have an AppConfiguration
The second one will auto-fetch as long as the AppConfiguration's fetch was successful.
Here's MakeBackboneSingleton for you (again from the GitHub repository):
var MakeBackboneSingleton = function (BackboneClass, options) {
options || (options = {});
// Helper to check for arguments. Throws an error if passed in.
var checkArguments = function (args) {
if (args.length) {
throw new Error('cannot pass arguments into an already instantiated singleton');
}
};
// Wrapper around the class. Allows us to call new without generating an error.
var WrappedClass = function() {
if (!BackboneClass.instance) {
// Proxy class that allows us to pass through all arguments on singleton instantiation.
var F = function (args) {
return BackboneClass.apply(this, args);
};
// Extend the given Backbone class with a function that sets the instance for future use.
BackboneClass = BackboneClass.extend({
__setInstance: function () {
BackboneClass.instance = this;
}
});
// Connect the proxy class to its counterpart class.
F.prototype = BackboneClass.prototype;
// Instantiate the proxy, passing through any arguments, then store the instance.
(new F(arguments.length ? arguments : options.arguments)).__setInstance();
}
else {
// Make sure we're not trying to instantiate it with arguments again.
checkArguments(arguments);
}
return BackboneClass.instance;
};
// Immediately instantiate the class.
if (options.instantiate) {
var instance = WrappedClass.apply(WrappedClass, options.arguments);
// Return the instantiated class wrapped in a function so we can call it with new without generating an error.
return function () {
checkArguments(arguments);
return instance;
};
}
else {
return WrappedClass;
}
};
I'm fairly new to Backbone and I'm working on a project that is probably not ideal for Backbone really, but I'm trying anyway! I'm not working with a RESTful API, so all of my data is fetched on page load and that is it, it is then not updated, at all. The data is sent through as a single lump of JSON.
I have set up a new model CookingItem which gets created for every item in the JSON and is then added to the CookingItemList collection, this is fine. However, I need to store information about the data set somewhere so I have created another 'Properties model' that stores this information (state information, start items etc). This properties model will also have several methods that manipulate the data.. these are then called from the view (is this OK?).
This is working fine, but I just want to be sure what I'm doing is considered a good way of doing things. I'm sure there may be a better way to handle the Properties model?
// Models
var CookingItem = Backbone.Model.extend(),
CookingProperties = Backbone.Model.extend({
defaults:{
startItem: 'anything',
currentItems:[]
},
setStartItem:function (val) {
// Code Here to edit this models properties
},
addRemoveItem:function (val) {
// Code Here to edit this models properties
}
}),
// Collection
CookingItemList = Backbone.Collection.extend({
model:CookingItem,
url:function () {
return "json.js";
}
}),
// View
CookingView = Backbone.View.extend({
el:'.page',
events:{
// Buttons in the UI
'click .link1':function () {
this.cookingProperties.addRemoveItem('item name');
},
'click .link2':function () {
this.cookingProperties.addRemoveItem('item name');
}
},
initialize:function () {
// Scope
var _this = this;
// Instantiate
this.cookingItemList = new CookingItemList(); // PlayListItems Collection.
this.cookingProperties = new CookingProperties(); // Properties Model.
// Bindings
this.cookingProperties.on('change:startItem', function () {
_this.customMethod1();
_this.customMethod2();
});
// Render the view
this.render();
},
render:function () {
var _this = this;
this.cookingItemList.fetch({
success:function () {
_this.cookingProperties.setStartItem('item');
_this.customMethod1();
}
});
return this;
},
customMethod1:function () {
// Do something
},
customMethod2:function () {
// Do something
}
}),
// Start
cookingView = new CookingView({
collection:CookingItemList
});
Ok, so I am working on a method to override the fetch method on a model. I want to be able to pass it a list of URL's and have it do a fetch on each one, apply some processing to the results, then update its own attributes when they have all completed. Here's the basic design:
A Parent "wrapper" Model called AllVenues has a custom fetch function which reads a list of URL's it is given when it is instantiated
For each URL, it creates a Child Model and calls fetch on it specifying that URL as well as a success callback.
The AllVenues instance also has a property progress which it needs to update inside the success callback, so that it will know when all Child fetch's are complete.
And that's the part I'm having problems with. When the Child Model fetch completes, the success callback has no context of the Parent Model which originally called it. I've kind of hacked it because I have access to the Module and have stored the Parent Model in a variable, but this doesn't seem right to me. The Parent Model executed the Child's fetch so it should be able to pass the context along somehow. I don't want to hardcode the reference in there.
TL;DR
Here's my jsFiddle illustrating the problem. The interesting part starts on line 13. http://jsfiddle.net/tonicboy/64XpZ/5/
The full code:
// Define the app and a region to show content
// -------------------------------------------
var App = new Marionette.Application();
App.addRegions({
"mainRegion": "#main"
});
App.module("SampleModule", function (Mod, App, Backbone, Marionette, $, _) {
var MainView = Marionette.ItemView.extend({
template: "#sample-template"
});
var AllVenues = Backbone.Model.extend({
progress: 0,
join: function (model) {
this.progress++;
// do some processing of each model
if (this.progress === this.urls.length) this.finish();
},
finish: function() {
// do something when all models have completed
this.progress = 0;
console.log("FINISHED!");
},
fetch: function() {
successCallback = function(model) {
console.log("Returning from the fetch for a model");
Mod.controller.model.join(model);
};
_.bind(successCallback, this);
$.each(this.urls, function(key, val) {
var venue = new Backbone.Model();
venue.url = val;
venue.fetch({
success: successCallback
});
});
}
});
var Venue = Backbone.Model.extend({
toJSON: function () {
return _.clone(this.attributes.response);
}
});
var Controller = Marionette.Controller.extend({
initialize: function (options) {
this.region = options.region;
this.model = options.model;
this.listenTo(this.model, 'change', this.renderRegion);
},
show: function () {
this.model.fetch();
},
renderRegion: function () {
var view = new MainView({
model: this.model
});
this.region.show(view);
}
});
Mod.addInitializer(function () {
var allVenues = new AllVenues();
allVenues.urls = [
'https://api.foursquare.com/v2/venues/4a27485af964a52071911fe3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811',
'https://api.foursquare.com/v2/venues/4afc4d3bf964a520512122e3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811',
'https://api.foursquare.com/v2/venues/49cfde17f964a520d85a1fe3?oauth_token=EWTYUCTSZDBOVTYZQ3Z01E54HMDYEPZMWOC0AKLVFRBIEXV4&v=20130811'
];
Mod.controller = new Controller({
region: App.mainRegion,
model: allVenues
});
Mod.controller.show();
});
});
App.start();
I think you're misunderstanding how _.bind works. _.bind returns the bound function, it doesn't modify it in place. In truth, the documentation could be a bit clearer on this.
So this:
_.bind(successCallback, this);
is pointless as you're ignoring the bound function that _.bind is returning. I think you want to say this:
var successCallback = _.bind(function(model) {
console.log("Returning from the fetch for a model");
Mod.controller.model.join(model);
}, this);
Also note that I added a missing var, presumably you don't want successCallback to be global.
I am trying to write a Backbone view for an object browser which is designed to be implemented in several places with different object types and slightly different operation.
I have tried simply extending the backbone view in my browser and then extending the browser in my implementation however this leaves me with some properties which are shared. This is an undesired effect as the data is appended to all implementations with every browser creation.
Could someone shed light on a way to solve this problem or perhaps an alternative solution?
Here are some code examples to give you a better idea of how it currently stands:
var BrowserView = Backbone.View;
_.extend(BrowserView.prototype, Backbone.View.prototype, {
className: 'browser',
collections: [],
events: {
},
_events:{
},
initialize: function () {
this._initialize();
},
render: function () {
this._render();
},
_initialize: function () {
this.container = $( this.make('div', {class: 'container'} ) );
this.$el.append(this.container);
if ( this.options.collections ) {
this.collections = [];
_.each(this.options.collections, this.add, this);
}
_.extend(this.events, this._events);
this.delegateEvents();
},
_render: function () {
this.container.empty();
_.each(this.collections, function (view) {
this.container.append(view.el);
view.render();
}, this);
}
});
BrowserView.extend = Backbone.View.extend;
var ContactBrowserView = BrowserView.extend({
});
EDIT
My issue is that the sub classes are sharing the collections property. Here is an example of my own solution which initialises the collections property through an inherited method. jsfiddle.net/JhZXh/3
I think I've figured out the answer to my own problem.
I believe the right way to achieve what I am looking for is to move the initialization of properties in to the initialize method provided by Backbone views. This way they are initialized
var BrowserView = Backbone.View.extend({
initialize: function () {
this.collections = [];
}
});
var FileBrowserView = BrowserView.extend({
initialize: function () {
BrowserView.prototype.initialize.apply(this);
this.collections.push({name: 'Example Collection' + Math.rand()});
}
});
var FileBrowserInstance1 = new FileBrowserView;
console.log(FileBrowserInstance1.collections);
var FileBrowserInstance2 = new FileBrowserView;
console.log(FileBrowserInstance2.collections);
http://jsfiddle.net/yssAT/2/
It's hard to see what exactly your goal is.
but this is how i see it
if you have an view object
var myView = Backbone.View.extend({
foo: "bar"
});
and you have it extend the backbone.View... then you actually have a new view object with everything of backbone.view, and the extra options you give as parameters.
if you then go and create a second view, that extends your first one
it will get everything from your first view, + it's own extras
var mySecondView = myView.extend({
foobar: "f00b#r"
});
if you would create an instance of the second view and log it's foo property it will still hold "bar" as value
var mySecondViewInstance = new mySecondView();
console.log("mySecondViewInstance.foo: ", mySecondViewInstance.foo);
console.log("mySecondViewInstance.foobar: ", mySecondViewInstance.foobar);
if i create a new instance of my first view, and change foo into "changed-foo"
the log of foo on mySecondViewInstance will still be "bar"
var myViewInstance = new myView();
myViewInstance.foo = "changed-foo";
console.log("mySecondViewInstance.foo: ", mySecondViewInstance.foo);
console.log("mySecondViewInstance.foobar: ", mySecondViewInstance.foobar);
a JS-Fiddle to play around with it can be found here:
http://jsfiddle.net/saelfaer/uNBSW/
Inherit from Backbone.View doesn't work, or is quite complex.
You should create a common object, which every of your view will inherit from, ie :
var ViewInterface = {
events : { /* ... */ },
initialize : function (options) { /* ... */ },
otherFunction : function (options) { /* ... */ },
}
each of your view would extend from this object :
var BrowserView = Backbone.View.extend(_.extend(ViewInterface, {
anotherFunction : function (options) { /* ... */ },
})
var AnotherView = Backbone.View.extend(_.extend(ViewInterface, {
yetAnotherFunction : function (options) { /* ... */ },
})
This gist shows a better alternative:
https://gist.github.com/2287018
I'm testing a backbone view with Jasmin, Simon and jasmin-simon.
Here is the code:
var MessageContainerView = Backbone.View.extend({
id: 'messages',
initialize: function() {
this.collection.bind('add', this.addMessage, this);
},
render: function( event ) {
this.collection.each(this.addMessage);
return this;
},
addMessage: function( message ) {
console.log('addMessage called', message);
var view = new MessageView({model: message});
$('#' + this.id).append(view.render().el);
}
});
Actually, all my tests pass but one. I would like to check that addMessage is called whenever I add an item to this.collection.
describe('Message Container tests', function(){
beforeEach(function(){
this.messageView = new Backbone.View;
this.messageViewStub = sinon.stub(window, 'MessageView').returns(this.messageView);
this.message1 = new Backbone.Model({message: 'message1', type:'error'});
this.message2 = new Backbone.Model({message: 'message2', type:'success'});
this.messages = new Backbone.Collection([
this.message1, this.message2
]);
this.view = new MessageContainerView({ collection: this.messages });
this.view.render();
this.eventSpy = sinon.spy(this.view, 'addMessage');
this.renderSpy = sinon.spy(this.messageView, 'render');
setFixtures('<div id="messages"></div>');
});
afterEach(function(){
this.messageViewStub.restore();
this.eventSpy.restore();
});
it('check addMessage call', function(){
var message = new Backbone.Model({message: 'newmessage', type:'success'});
this.messages.add(message);
// TODO: this fails not being called at all
expect(this.view.addMessage).toHaveBeenCalledOnce();
// TODO: this fails similarly
expect(this.view.addMessage).toHaveBeenCalledWith(message, 'Expected to have been called with `message`');
// these pass
expect(this.messageView.render).toHaveBeenCalledOnce();
expect($('#messages').children().length).toEqual(1);
});
});
As you can see addMessage is called indeed. (It logs to the console and it calls this.messageView as it should. What do I miss in spying for addMessage calls?
thanks, Viktor
I'm not quit sure but, as I understand it, the following happens:
You create a new view which calls the initialize function and bind your view.addMessage to your collection.
Doing this, Backbone take the function and store it in the event store of your collection.
Then you spy on view.addMessage which means you overwrite it with a spy function. Doing this will have no effect on the function that is stored in the collection event store.
So their are some problems with your test. You view has a lot of dependencies that you not mock out. You create a bunch of additional Backbone Models and Collections, which means you not test only your view but also Backbones Collection and Model functionality.
You should not test that collection.bind will work, but that you have called bind on the collection with the parameters 'add', this.addMessage, this
initialize: function() {
//you dont
this.collection.bind('add', this.addMessage, this);
},
So, its easy to mock the collection:
var messages = {bind:function(){}, each:function(){}}
spyOn(messages, 'bind');
spyOn(messages, 'each');
this.view = new MessageContainerView({ collection: messages });
expect(message.bind).toHaveBeenCalledWith('bind', this.view.addMessage, this.view);
this.view.render()
expect(message.each).toHaveBeenCalledWith(this.view.addMessage);
... and so on
Doing it this way you test only your code and have not dependencies to Backbone.
As Andreas said in point 3:
Then you spy on view.addMessage which means you overwrite it with a spy function. Doing this will have no effect on the function that is stored in the collection event store.
The direct answer to the question, disregarding all the awesome refactoring Andreas suggested, would be to spy on MessageContainerView.prototype.addMessage like so:
describe('Message Container tests', function(){
beforeEach(function(){
this.messageView = new Backbone.View;
this.messageViewStub = sinon.stub(window, 'MessageView').returns(this.messageView);
this.message1 = new Backbone.Model({message: 'message1', type:'error'});
this.message2 = new Backbone.Model({message: 'message2', type:'success'});
this.messages = new Backbone.Collection([
this.message1, this.message2
]);
// Here
this.addMessageSpy = sinon.spy(MessageContainerView.prototype, 'addMessage');
this.view = new MessageContainerView({ collection: this.messages });
this.view.render();
this.eventSpy = sinon.spy(this.view, 'addMessage');
this.renderSpy = sinon.spy(this.messageView, 'render');
setFixtures('<div id="messages"></div>');
});
afterEach(function(){
this.messageViewStub.restore();
MessageContainerView.prototype.addMessage.restore();
});
it('check addMessage call', function(){
var message = new Backbone.Model({message: 'newmessage', type:'success'});
this.messages.add(message);
// TODO: this fails not being called at all
expect(this.addMessageSpy).toHaveBeenCalledOnce();
// TODO: this fails similarly
expect(this.addMessageSpy).toHaveBeenCalledWith(message, 'Expected to have been called with `message`');
// these pass
expect(this.messageView.render).toHaveBeenCalledOnce();
expect($('#messages').children().length).toEqual(1);
});
});
Regardless, I do recommend implementing Andreas' suggestions. :)