I have a model relationship set-up with Backbone Relational, whereby an Item belongs to a Column. Adding items to columns works well, but how do I reference an existing view for an item, in-order to remove it from its old column? (Therefore, items are currently duplicating across columns.)
Please see this JSFiddle - http://jsfiddle.net/geVPp/1 (I presume code will need to be implemented in the ColumnView removeItem event handler, but I may be wrong).
(The example instantiation is at the bottom of the script, as I have no UI controls yet.)
Here is the ColumnView from the fiddle:
var ColumnView = Backbone.View.extend({
className: 'column',
tagName: 'div',
template: Handlebars.compile($('#column-template').html()),
initialize: function() {
_.bindAll(this, 'render', 'renderItem', 'removeItem');
this.model.bind('change', this.render);
this.model.bind('reset', this.render);
this.model.bind('add:items', this.renderItem);
this.model.bind('remove:items', this.removeItem);
},
render: function() {
return $(this.el).html(this.template(this.model.toJSON()));
},
renderItem: function(item) {
var itemView = new ItemView({model: item});
this.$('.items').append($(itemView.render()));
},
removeItem: function(item) {
// #todo -- How do I reference the item model's view, in order to remove the DOM element?
}
});
Unfortunately Backbone has no built-in way; you have to keep track of your child views (and which model they map to) manually.
Backbone.Marionette has a CollectionView class that can automate this for you. Or you could roll your own similar to UpdatingCollectionView.
If you want a quick fix, you could keep a reference to the view as a variable of the model object, but that's bad because it prevents you from showing multiple views of the same model.
Related
Below is the code I currently have that re-renders a collectionView on every addition or removal of a model. However, it seems inefficient as it has to render the whole thing every time, when all I really need is one modelView to be removed or one to be added. So how could I achieve this?
var CollectionView = Marionette.CollectionView.extend({
childView: ModelView,
initialize: function() {
[ "add", "remove" ].forEach(function(eventName) {
this.listenTo(this.collection, eventName, this.render, this);
}.bind(this));
}
});
Thanks in advance for any help you can give!
This is already done automatically in Marionette:
When a model is added to the collection, the collection view will
render that one model in to the collection of item views.
When a model is removed from a collection (or destroyed / deleted),
the collection view will close and remove that model's item view.
I am using backbone.js to create a page. My code contains many models and views. I wonder if it is possible to destroy a view and then redraw it without refreshing the page, and if so, what is the best way to do it.
$(document).ready(function() {
var myHomeCollectionView = new MyHomeCollectionView({});
});
var MyHomeCollection = Backbone.Collection.extend({
model: MyHome
});
var MyHomeCollectionView = Backbone.View.extend({
el: "#home",
initialize: function(options){
_.bindAll(this, 'render');
this.collection = new MyHomeCollection();
/-- Rest initialize the code --/
},
render: function(){
/-- Render code --/
}
})
this is a sample code of my view..
Yes. It is certainly possible. The main benefit of a JS framework is being able to change the content of the page without refreshing it.
I am not sure why you want to destroy the view, that is usually not necessary.
If you simply want to re-render the same view, you usually just listen for an event then call render. Take a look at the example below of re-rendering your view based on when the collection reloaded.
var MyHomeCollectionView = Backbone.View.extend({
el: "#home",
initialize: function(options){
_.bindAll(this, 'render');
this.collection = new MyHomeCollection();
// re-render view when collection is reloaded
this.listenTo(this.collection, 'reset sync', this.render);
/-- Rest initialize the code --/
},
render: function(){
/-- Render code --/
}
})
Or you can replace a view with another view. You can do this by simply rendering another view into the same element. Check out this jsfiddle for a very simple example of this: http://jsfiddle.net/1g1j7afa/2/.
If you want to get more advanced, you can check out Marionette LayoutView. It is a nice way to handle the adding/replacing of sub views.
Here is the general structure I have right now:
Collection 'A' which has view 'A'
to add a model, you can open a modal and choose from a list. The modal is it's own view, 'B', tied to its own collection, 'B'. the view 'B' is instantiated from view 'A' on a click event.
I was going to initialize view 'B' with a 'myParent' attribute so when a model is selected from collection b, I can say this from within view B:
this.myParent.collection.add(newModel).
I know this will work, but is this coupling things too tight that don't need to be? is there a good pattern for this? I was thinking about having several different events and triggers spread about different places but that seems to just complicate things without much added value.
I have come across a similar problem in an application which lists a collection of selectable models in a table. Based on the models which are selected, another view needs to determine which buttons to show to the user.
In this case, I went for an event driven approach. Every time a model from view A is selected, I fire an event which View B listens to and adds that model to Collection B.
For your situation, that could look something like this:
// Your collection view.
var ViewA = Backbone.View.extend({
initialize: function(){
Backbone.Events.on('modelSelected', this.onModelSelected, this);
this.listenTo(this.collection, 'add', this.renderList);
},
onModelSelected: function(model){
this.collection.add(model);
}
renderList: function(){
// Draw your collection
}
// Do some clean up of events
remove: function(){
Backbone.Events.off('modelSelected', this.onModelSelected, this);
Backbone.View.prototype.remove.call(this);
}
});
// Modal View
var ViewB = Backbone.View.extend({
initialize: function(){
this.collection = new YourCollection();
this.listenTo(this.collection, 'sync', this.renderCollection);
this.collection.fetch();
},
render: function(){
// Render your modal
return this;
},
renderCollection: function(){
// Render your collection
},
//
modalSubmit: function(){
var model = this.getSelectedModel();
Backbone.Events.trigger('modelSelected', modal);
this.close();
}
});
I should note that this comes with a few caveats. Firstly, I would recommend picking a robust and namespace event naming scheme. This is just an example. Secondly, fetching from the initialize function is often not ideal, but I'm leaving out the concept of controllers for brevity.
I am working on a single-page app using Backbone.js. An issue that has occurred to me is that since one is not reloading the page, that when one creates a instance of a View, then I assume, that the View object will remain in memory for the life of the app. This does not seem very efficient to me, since a particular view may no longer be needed if another route is called. However, a particular View may later need to be 'displayed' if one returns to that original route. So the question is, how to best manage views in Backbone with regards to routes?
In my app, many of the views are responsible for displaying a particular 'page' and as such share the same DOM element. When one of these 'page' views is called, it will replace the content in the DOM element previously put in place by the previous view. Thus the previous view is no longer needed.
Do I need to somehow manually destroy the previous View (or is this somehow handled by the Router object)? Or is it better to leave the views once they have been initialized?
Following sample code shows how views instances are being creating in the Router in the app.
/**
* View - List of contacts
*/
var ListContactsView = Backbone.View.extend({
el: '#content',
template: _.template($('#list-contacts-tpl').html()),
initialize: function() {
_.bindAll(this, 'render');
this.collection = new Contacts();
this.collection.bind('reset', this.render);
this.collection.fetch();
},
render: function() {
this.$el.hide();
this.$el.html(this.template({ contacts: this.collection }));
this.$el.fadeIn(500);
}
});
/**
* View - Display single contact
*/
var DisplayContactView = Backbone.View.extend({
el: '#content',
events: {
'click #delete-contact-button': 'deleteContact'
},
template: _.template($('#display-contact-tpl').html()),
initialize: function() {
_.bindAll(this, 'deleteContact', 'render');
// Create reference to event aggregator object.
if (typeof this.options.id === 'undefined') {
throw new Error('View DisplayContactView initialized without _id parameter.');
}
this.model = new Contact({ _id: this.options.id });
// Add parse method since parsing is not done by collection in this
// instance, as this model is not called in the scope of collection
// Contacts.
this.model.parse = function(response) {
return response.data;
};
this.model.bind('change', this.render);
this.model.fetch();
},
deleteContact: function(id) {
// Trigger deleteContact event.
this.eventAggregator.trigger('deleteContact', id);
},
render: function() {
this.$el.html(this.template({ contact: this.model.attributes }));
}
});
/**
* Page routes
*/
var $content = $('#content');
var ClientSideRouter = Backbone.Router.extend({
routes: {
'browse': 'browse',
'browse/view/:id': 'browseViewContact',
'orgs': 'orgs',
'orgs/:orgName': 'orgs',
'orgs/:orgName/:id': 'orgs',
'contact/add': 'addContact',
'contact/view/:id': 'viewContact',
'contact/delete/:id': 'confirmDelete',
'*path': 'defaultPage'
},
addContact: function() {
// Display contact edit form.
var editContactFormView = new EditContactFormView();
// Display email field in edit form.
},
browse: function() {
var listContactsView = new ListContactsView();
},
browseViewContact: function(id) {
var displayContactView = new DisplayContactView({ id: id });
},
defaultPage: function(path) {
$content.html('Default');
},
home: function() {
$content.html('Home');
},
viewContact: function(id) {
$.ajax({
url: '/contact/view/' + id,
dataType: 'html',
success: function(data) {
$content.html(data);
}
});
}
});
var clientSideRouter = new ClientSideRouter();
Backbone.history.start();
Routes do not destroy views
Routes provide you convenient manner to interact with url changes. By convenience i mean url semantics and context of current page. For example url #/!/create/ will invoke a method that should display a form to create a model. Context here is the view to create model.
Views should be managed by the developer
there still does not exists a well known manner to manage views in Backbone.js, but i prefer the way of global variables. This would ensure your view instances are available throughout application and all the modules have access to them. For example doing this
window.App.Contacts.ContactView = new App.Contacts.View.ContactView({model:BenContact}); will make view used to display Ben's contact information available to application modules through window object. All you need to do for any views that use same el is to destroy the ContactView and render the new view.
You have methods on view to remove them
Undelegate Events and Remove methods help you remove them. Inside the callback method that handles routes hash change events. For example in the callback method that handles #/!/view/all ( url to view all the contacts list) you might come across situation where both the views now use the same el so you should destroy the ContactView and render ListView so in the callback do this
App.Contacts.ContactView.undelegateEvents();
App.Contacts.ContactView.remove();
Since Backbone.js has no built in support for view compositions, there are several patterns that you could follow when it comes to keeping track of child views.
Derick Bailey illustrates extending Backbone.View to allow views to
clean up after themselves -
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Another alternative is to add on child views to a property of the
parent view and manually clean them up when the parent view state is
removed.
var ParentView = Backbone.View.extend({
initialize : function(){
this.childViews = [];
},
render: function(){
this.childViews.push(new ChildView);
}
});
A third alternative is to make the child views subscribe to events
that the parent views trigger, so that they can clean up when the
parent view publishes a "close" event.
Also I noticed from your code that you are actually fetching a model within your child view class. Ideally, I would suggest passing the model as a parameter to the constructor as this decouples the view from the data. It's more MVC-ish
I'm doing an app that basically works like the facebook wall.
Essentially Posts and Comments.
It's working, but in order to render the CommentView, I'm using code similar to this in my Post Template
<div class="wall-post">
<div class="wall-post-content">${PostContent}</div>
<div class="wall-post-comments" id="wall-post-comments-${PostId}"></div>
</div>
Then I use the id of the comment area for that post like this.
var comment_view = new PostCommentView({ model: post.get("Comments") });
this.$('#wall-post-comments-'+ post.get("PostId")).append($(comment_view.render()));
This works, but something tells me I shouldn't be manually binding against my own ID. I feel I should be doing something clever with this.el ?
Can anyone point me in the right direction.
I'm using BackBone Relational to manage the relationships.
//EDIT
As requested some more of the implementation
//Some functions relating to click evens and functionality removed, as I don't think they relate to my question.
PostModel = Backbone.RelationalModel.extend({
urlRoot: '/api/post',
idAttribute: 'PostId',
relations: [{
type: Backbone.HasMany,
key: 'Comments',
relatedModel: 'CommentModel',
reverseRelation: {
key: 'Post',
includeInJSON: 'PostId'
}
}]
});
CommentModel = Backbone.RelationalModel.extend({
urlRoot: '/api/comment',
idAttribute: 'PostId'
});
PostCollection = Backbone.Collection.extend({
url: '/api/post',
model: PostModel
});
PostListView = Backbone.View.extend({
tagName: 'div',
className: 'PostListView',
initialize: function(){
_.bindAll(this, 'render', 'render_thread_summary', 'on_submit', 'on_thread_created', 'on_error');
this.model.bind('reset', this.render);
this.model.bind('change', this.render);
this.model.bind('add', this.render_thread_summary);
},
template: $('#wall-post-template').html(),
render: function() {
$(this.el).html($(this.template).tmpl(this.model.toJSON()));
this.model.forEach(this.render_thread_summary);
return $(this.el).html();
},
render_thread_summary: function(post) {
var comment_view = new PostCommentView({ model: post.get("Comments") });
this.$('#wall-post-comments-'+ post.get("PostId")).append($(comment_view.render()));
}
});
PostCommentView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, 'render', 'on_click');
this.model.bind('change', this.render);
},
template: $('#wall-comments-template').html(),
render: function() {
var html = $(this.el).html($(this.template).tmpl(this.model.toJSON()));
return html;
}
});
I've just started digging into Backbone (and haven't done anything with Backbone Relational yet), so with that in mind here are my 2 cents:
Backbone defines id's for its Models, so no need to define your own id attr. If you inspect into a model instance, you'll see its id even if it's not specifically defined in your impl.
It seems to me that you're missing a comments collection, made up by single comment models. Then attach model events within your views appropriately. That way you wouldn't have to manage your comment view rendering manually (it's all done by Backbone based on event triggers).
If you haven't looked into the BB sample Todos app, I'd suggest giving it a look - that should help you design your comment(s) model and view better.
todos.js
todos app - Use Fire/ChromeBug to inspect the code
Hope this helps.
I was greatly underusing the this.el reference. There is no need to generally use id's for most things on the page, as within a view you can just reference $(this.el) and then reference from that part of the page. $(".className", this.el) will select any class's with in the item on the page. el is essentially a reference to the area on the page where the view was rendered to. It's really clean once you get the hang of it.