I have a view that represents a modal. This view has some attributes, like the footer extra class.
I'm having a trouble that, when some modal change it's value, all of them receive the same attribute.
This is the example:
var ModalView = AlertingView.extend({
className : "modal",
paramsTemplate: { footerClass: "" }
});
After that, I used the modal that has the changed attribute.
SpecialModal = ModalView.extends({
initialize: function() {
this.paramsTemplate.footerClass = "white";
}
})
So, if I instanciate a normal Modal, this parameter is set! :'(
NormalModal = ModalView.extends({
initialize: function() {
console.log(this.paramsTemplate.footerClass);//shows 'white'!
}
})
Do you have any idea of how solving it?
thanks!
http://jsfiddle.net/JQu5Q/11/
This fiddle demonstrates your problem.
As long as you've not created any instance of SpecialModal every instance of NormalModal works.
This is because paramsTemplate is a class variable, not an instance variable. As soon as an instance of SpecialModal is created, it updates the classVariable and hence, all objects reflect that change.
Fortunately, I've just had a similar issue few days back Backbone.js view instance variables?
Solution :
http://jsfiddle.net/JQu5Q/12/
var ModalView = Backbone.View.extend({
className : "modal",
initialize:function(){
this.paramsTemplate= { footerClass: "" } ;
}
});
SpecialModal = ModalView.extend({
initialize: function() {
ModalView.prototype.initialize.call(this);
this.paramsTemplate.footerClass = "white";
console.log(this.paramsTemplate.footerClass);
}
})
NormalModal = ModalView.extend({
initialize: function() {
ModalView.prototype.initialize.call(this);
console.log(this.paramsTemplate.footerClass);
}
})
Related
I have a Backbone model that looks like this
...
var Address = Backbone.Model.extend({
urlRoot: '/address/'
});
return { address: Address }
});
I have a template that prints out an address in a form. The template is rendered by a view that is passed an address id in it's 'render' function. The view is reached by a route like this 'address/:id'.
The view looks like this:
var AddressView = Backbone.View.extend({
el: $('#myclass'),
render: function(options) {
var that = this;
var addr = new A.address({id: options.aid});
addr.fetch({
reset: true,
success: function(address) {
var template = _.template(ATemplate, {address: address});
that.$el.html(template);
}
});
return this;
},
events: {
'submit .edit-address-form': 'editAddress'
},
editAddress: function(ev) {
//serializeObject creates JSON object from form data
var addressDetails = $(ev.currentTarget).serializeObject();
addr.save(addressDetails, function(addr) {
R.router.navigate('', {trigger: true});
});
return false;
}
});
return {
addressView: new AddressView()
};
});
There are two problems. The first problem is that the 'editAddress' function is never getting called, even though the class name is correct and the button type = is 'submit'.
The second problem is when I submit the address form the URL is garbled, a query string is interpolated between the base URL and /#/address, as in
http:///ldmanclient/address=2500+Moffitt+Library&address2=University+of+California%2C+Berkeley&city=Berkeley&zipcode=94720&mailcode=6000&aid=1#/address/1
Has anyone seen this type of behavior before? What am I doing wrong?
As mu said, the form is being submitted the standard way before Backbone gets to it. Try preventing the submit action:
editAddress: function(ev) {
ev.preventDefault();
// same code as above
}
In this code...
_.each(this.photos, function(element,index,list) {
console.log('element...');
console.log(element);
var photoView = new PhotoView({photo:element});
self.$el.append(photoView.render());
});
element is the entire this.photos collection. Why is not just one photo element of the 10 in the collection?
EDIT: Here is my method that populates the photos collection....
loadPhotos: function(memberId) {
var self = this;
this.photos = new PhotosCollection([]);
this.photos.on('error', this.eventSyncError, this);
this.photos.fetch({
url: this.photos.urlByMember + memberId,
success: function(collection,response,options) {
console.log('Fetch photos success!');
self.render();
}
});
},
The collection loads with models just fine. In the Chrome console, I can see the collection of models. I'm not sure what's wrong. I cannot iterate the collection with any of the methods recommended by posters below.
You are using the _.each method incorrectly. The underscore methods needs to called directly on the collection:
this.photos.each(function(element,index,list) {
console.log('element...');
console.log(element);
var photoView = new PhotoView({photo:element});
self.$el.append(photoView.render());
});
Or you if want to use the _.each from you need to pass in the models property and not the collection object itself as the list:
_.each(this.photos.models, function(element,index,list) {
console.log('element...');
console.log(element);
var photoView = new PhotoView({photo:element});
self.$el.append(photoView.render());
});
One should use this.photos.each(function(elt, index, list){...}) instead of _.each(this.photos,...) because this.photos is not an underscorejs _.chain object.
Thank you for your suggestions! I would never have figured this out without all your advice above. So here was the problem...
In the parent view, this loads up photo records for a particular member...
loadPhotos: function(memberId) {
var self = this;
this.photos = new PhotosCollection([]);
this.photos.on('error',this.eventSyncError,this);
this.photos.fetch({
url: this.photos.urlByMember + memberId,
success: function(collection,response,options) {
self.render();
}
});
},
Still in the parent view, Backbone.Subviews uses this to call each child view when it renders. Note how I'm passing this.photos to the subvw-photos...
subviewCreators: {
"subvw-profile": function() {
var options = {member: this.member};
// do any logic required to create initialization options, etc.,
// then instantiate and return new subview object
return new ProfileView( options );
},
"subvw-photos": function() {
var options = {photos: this.photos};
return new PhotosView( options );
},
"subvw-comments": function() {
var options = {};
return new CommentsView( options );
}
},
This is in the subvw-photos child view. Note how the intialize is accepting the collection as a parameter. See this problem?...
initialize: function(photos) {
Backbone.Courier.add(this);
this.photos = photos;
},
render: function() {
console.log('rendering photosview now...');
var self = this;
this.photos.each(function(element,index,list) {
var photoView = new PhotoView({photo:element});
$(self.el).append(photoView.render());
});
return this;
},
I was passing an object wrapping the photos collection in to initalize but then treating it like it was just a ref to the photos collection. I had to change the subvw-photos initialize to the following...
initialize: function(args) {
Backbone.Courier.add(this);
this.photos = args.photos;
},
Then of course all the other code magically began working :-/
Thank you again for your tips! You definitely kept me on track :-)
Ok, so I'm having quite some difficulty getting event binding to work with Coffeescript and Backbone. I have a feeling it has to do with how I'm initializing everything; I feel like the event delegation isn't even being run.
Here is my view code:
$ ->
class AppName.TitleView extends Backbone.View
template: JST['templates/title']
collection: new AppName.Members
events:
"keypress #search" : "search",
initialize: =>
$('#search').keypress(#search) #doing it manually as a hack
search: ->
console.log('search handler call')
render: =>
$('#app').html(#template)
#delegateEvents() #this doesn't seem to do anything, nor does #delegateEvents(#events)
#
Which, when compiled, looks something like this:
(function() {
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = Object.prototype.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; };
$(function() {
AppName.TitleView = (function(_super) {
__extends(TitleView, _super);
function TitleView() {
this.render = __bind(this.render, this);
this.initialize = __bind(this.initialize, this);
TitleView.__super__.constructor.apply(this, arguments);
}
TitleView.prototype.template = JST['templates/title'];
TitleView.prototype.collection = new AppName.Members;
TitleView.prototype.events = {
"keypress #search": "search",
};
TitleView.prototype.initialize = function() {
return $('#search').keypress(this.search);
};
TitleView.prototype.search = function() {
console.log('search handler call'););
});
};
TitleView.prototype.render = function() {
$('#app').html(this.template);
this.delegateEvents();
return this;
};
return TitleView;
})(Backbone.View);
});
}).call(this);
AppName.TitleView is kicked off from my router (which is in turn kicked off by the main app.coffee) with:
$ ->
class AppName.Router extends Backbone.Router
routes:
".*": "main"
main: ->
#titleView ||= new AppName.TitleView el: $('#app')[0]
#titleView.render()
But for the life of me, I cannot get the binding to #search from Backbone Events to bind. My hack (which is in the code) is just to bind via jQuery in the initialize function.
Any idea what's going on? I'm hoping this is a simple typo or bad initialization.
View events are bound to the view's el using the delegation form of jQuery's on. That means that everything you mention in the view's events object must be inside the view's el or the event handlers won't be triggered.
By default, a view's el is an empty <div> and the view will create that <div> when it needs to and bind the events to that <div>. You might notice that you're not using this.el or this.$el anywhere in your code; your events are being bound properly but they're being bound to anything that you put in the DOM and so it looks like the events aren't working.
Two immediate possibilities arise:
Use #app as the view's el:
class AppName.TitleView extends Backbone.View
#...
el: '#app'
#...
render: =>
#$el.html(#template)
#
Do things the usual Backbone way and create an el just for your view:
class AppName.TitleView extends Backbone.View
#...
render: =>
#$el.html(#template)
#
v = new AppName.TitleView
$('#app').append(v.render().el)
I'd recommend the latter as it is easier to manage and will help you avoid attaching multiple views to the same DOM element (and the zombies that tend to come from that).
Also, #template is almost always a function so you want to say:
$('#app').html(#template())
When the view is initialized, how can I bind the model to the specific View that is created? The view is current initialized at the start of the application. Also, how can I bind the model to the collection?
(function ($) { //loads at the dom everything
//Creation, Edit, Deletion, Date
var Note = Backbone.Model.extend({
defaults: {
text: "write here...",
done: false
},
initialize: function (){
if(!this.get("text")){
this.set({"text": this.default.text});
}
},
edit: function (){
this.save({done: !this.get("done")});
},
clear: function (){
this.destroy();
}
});
var NoteList = Backbone.Collection.extend({
model:Note
});
var NoteView = Backbone.View.extend ({
el: "body",
initialize: function(){
alert("initialized");
var list = new NoteList;
return list;
},
events: {
"click #lol" : "createNote"
},
createNote : function(){
var note = new Note;
this.push(note);
alert("noted");
}
});
var ninja = new NoteView;
})(jQuery);
Update
I just took a look at #James Woodruff's answer, and that prompted me to take another look at your code. I didn't look closely enough the first time, but I'm still not sure what you're asking. If you're asking how to have a model or view listen for and handle events triggered on the other, then check out James's example of calling bind() to have the view listen for change (or change:attr) events on the model (although I'd recommend using on() instead of bind(), depending what version of Backbone you're using).
But based on looking at your code again, I've revised my answer, because I see some things you're trying to do in ways that don't make sense, so maybe that's what you're asking about.
New Answer
Here's the code from your question, with comments added by me:
var NoteView = Backbone.View.extend ({
// JMM: This doesn't make sense. You wouldn't normally pass `el`
// to extend(). I think what you really mean here is
// passing el : $( "body" )[0] to your constructor when you
// instantiate the view, as there can only be one BODY element.
el: "body",
initialize: function(){
alert("initialized");
// JMM: the next 2 lines of code won't accomplish anything.
// Your NoteList object will just disappear into thin air.
// Probably what you want is one of the following:
// this.collection = new NoteList;
// this.list = new NoteList;
// this.options.list = new NoteList;
var list = new NoteList;
// Returning something from initialize() won't normally
// have any effect.
return list;
},
events: {
"click #lol" : "createNote"
},
createNote : function(){
var note = new Note;
// JMM: the way you have your code setup, `this` will be
// your view object when createNote() is called. Depending
// what variable you store the NoteList object in (see above),
// you want something here like:
// this.collection.push( note ).
this.push(note);
alert("noted");
}
});
Here is a revised version of your code incorporating changes to the things I commented on:
var NoteView = Backbone.View.extend( {
initialize : function () {
this.collection = new NoteList;
},
// initialize
events : {
"click #lol" : "createNote"
},
// events
createNote : function () {
this.collection.push( new Note );
// Or, because you've set the `model` property of your
// collection class, you can just pass in attrs.
this.collection.push( {} );
}
// createNote
} );
var note = new NoteView( { el : $( "body" )[0] } );
You have to bind views to models so when a model updates [triggers an event], all of the corresponding views that are bound to the model update as well. A collection is a container for like models... for example: Comments Collection holds models of type Comment.
In order to bind a view to a model they both have to be instantiated. Example:
var Note = Backbone.Model.extend({
defaults: {
text: "write here..."
},
initialize: function(){
},
// More code here...
});
var NoteView = Backbone.View.extend({
initialize: function(){
// Listen for a change in the model's text attribute
// and render the change in the DOM.
this.model.bind("change:text", this.render, this);
},
render: function(){
// Render the note in the DOM
// This is called anytime a 'Change' event
// from the model is fired.
return this;
},
// More code here...
});
Now comes the Collection.
var NoteList = Backbone.Collection.extend({
model: Note,
// More code here...
});
Now it is time to instantiate everything.
var Collection_NoteList = new NoteList();
var Model_Note = new Note();
var View_Note = new NoteView({el: $("Some Element"), model: Model_Note});
// Now add the model to the collection
Collection_NoteList.add(Model_Note);
I hope this answers your question(s) and or leads you in the right direction.
I have a BoardView containing a CellCollection of CellModels. I fetch the collection from the db and then create the CellViews.
This all works swimmingly until I try to access a CellModel via a click event on the BoardView. I can't get to the underlying models at all... only the views. Is there a way to do this?
I've attempted to include the relevant code below:
CellModel = Backbone.Model.extend({});
CellCollection = Backbone.Collection.extend({
model : CellModel
});
CellView = Backbone.View.extend({
className : 'cell',
});
BoardView = Backbone.View.extend({
this.model.cells = new CellCollection();
render : function() {
this.cellList = this.$('.cells');
return this;
},
allCells : function(cells) {
this.cellList.html('');
this.model.cells.each(this.addCell);
return this;
},
addCell : function(cell) {
var view = new Views.CellView({
model : cell
}).render();
this.cellList.append(view.el);
},
events : {
'click .cell' : 'analyzeCellClick',
},
analyzeCellClick : function(e) {
// ?????????
}
});
I need the click to "happen" on the BoardView, not the CellView, because it involves board-specific logic.
Good question! I think the best solution would be to implement an
EventBus aka EventDispatcher
to coordinate all events among the different areas of your application.
Going that route seems clean, loosely coupled, easy to implement, extendable and it is actually suggested by the backbone documentation, see Backbone Docs
Please also read more on the topic here and here because (even though I tried hard) my own explanation seems kind of mediocre to me.
Five step explanation:
Create an EventBus in your main or somewhere else as a util and include/require it
var dispatcher = _.clone(Backbone.Events); // or _.extends
Add one or more callback hanlder(s) to it
dispatcher.CELL_CLICK = 'cellClicked'
Add a trigger to the Eventlistener of your childView (here: the CellView)
dispatcher.trigger(dispatcher.CELL_CLICK , this.model);
Add a Listener to the Initialize function of your parentView (here: the BoardView)
eventBus.on(eventBus.CARD_CLICK, this.cardClick);
Define the corresponding Callback within of your parentView (and add it to your _.bindAll)
cellClicked: function(model) {
// do what you want with your data here
console.log(model.get('someFnOrAttribute')
}
I can think of at least two approaches you might use here:
Pass the BoardView to the CellView at initialization, and then handle the event in the CellView:
var CellView = Backbone.View.extend({
className : 'cell',
initialize: function(opts) {
this.parent = opts.parent
},
events : {
'click' : 'analyzeCellClick',
},
analyzeCellClick : function() {
// pass the relevant CellModel to the BoardView
this.parent.analyzeCellClick(this.model);
}
});
var BoardView = Backbone.View.extend({
// ...
addCell : function(cell) {
var view = new Views.CellView({
model : cell,
parent : this
}).render();
this.cellList.append(view.el);
},
analyzeCellClick : function(cell) {
// do something with cell
}
});
This would work, but I prefer to not have views call each other's methods, as it makes them more tightly coupled.
Attach the CellModel id to the DOM when you render it:
var CellView = Backbone.View.extend({
className : 'cell',
render: function() {
$(this.el).data('cellId', this.model.id)
// I assume you're doing other render stuff here as well
}
});
var BoardView = Backbone.View.extend({
// ...
analyzeCellClick : function(evt) {
var cellId = $(evt.target).data('cellId'),
cell = this.model.cells.get(cellId);
// do something with cell
}
});
This is probably a little cleaner, in that it avoids the tight coupling mentioned above, but I think either way would work.
I would let the CellView handle the click event, but it will just trigger a Backbone event:
var CellView = Backbone.View.extend({
className : 'cell',
initialize: function() {
_.bindAll(this, 'analyzeCellClick');
}
events : {
'click' : 'analyzeCellClick',
},
analyzeCellClick : function() {
this.trigger('cellClicked', this.model);
}
});
var BoardView = Backbone.View.extend({
// ...
addCell : function(cell) {
var view = new Views.CellView({
model : cell
}).render();
this.cellList.append(view.el);
view.bind('cellClicked', function(cell) {
this.analyzeCellClick(cell);
};
},
analyzeCellClick : function(cell) {
// do something with cell
}
});