I have a view and I need to re-render it in the same page.
If I call .render() again the first rendered is gone.
Cloning view object with jQuery.extend() has same result.
var cloneView = $.extend(true, {}, view);
$('#container').append(cloneView.render().el);
I cannot call new View() because there are various view classes.
How can I make a proper clone of a view?
You can't just clone your view and use it the way you want, because there's a lot of work done in the background, like :
cid generation : unique id of your view in your application.
$el generation : the view main DOM element
events delegation : the view events delegation
So if you insist in cloning your view, I will suggest you to create a clone of it var cloneView = $.extend(true, {}, view); that do what exactly new View do.
And as mu is too short suggested, event $.extend won't work.
So the best way to do it, is to instantiate a new View
It is dangerous to go blindly clone any view. You need to check for subtle things like hard-coded IDs in the template and in the view, styles of the template when it's re-used in different locations,..etc.
I would suggest either:
Create a new instance of the view. (if you're sure it's safe to render the another instance simultaneously with the previous instance of the view on the same page).
var my_view = new MyView({ el: $("#id") });
Create a new view extending that view. (this is a better bet)
CloneView = MyView.extend({
// Stuff for CloneView
});
var my_view = new CloneView({ el: $("#id") });
Related
giving a parent and a child view, I'd like 2 things:
the child view should render itself on instantiation
the child's render method should know the current parent's dom element
In practice, from the parent view, instead of this:
,add_bannerbox_view:function(model, collection, options){
var bannerbox = new BannerBoxView({ model: model });
this.bannerbox_views[model.cid] = bannerbox;
this.bannerbox_container.append(bannerbox.el);
bannerbox.render();
}
I'd like simply this;
,add_bannerbox_view:function(model, collection, options){
//here, BannerBoxView is supposed to render itself from initialize()
this.bannerbox_views[model.cid] = new BannerBoxView({ model: model, parent:this.el });
}
But I was wondering: is passing a parent's elem to the child a good practice? Or does it have some bad drawback?
Loose coupling is almost always preferable to tight coupling. The two best reasons I can think of are:
Reusable. Can be used by anywhere in your app without worrying about dependencies.
Testable. Can be tested independent of other components.
By requiring the child view to have a reference to the parent view, you are promoting tight coupling i.e. the child view becomes dependent on the parent view. This makes reusability extremely difficult, and if you're writing unit tests, you're going to have to instantiate or mock a parent class just so you can test the child. This is unnecessary and tedious.
If really what you're trying to do is have the child view automatically render, just extend the core Backbone.View and include a helper function that your parent views can call.
var MyView = Backbone.View.extend({
renderChild: function(view, options) {
var childView = new view(options);
this.views[options.model.cid] = childView;
this.$el.append(childView.el);
childView.render();
}
});
Then, you can define your parent views like so:
var ParentView = MyView.extend({
add_bannerbox_view: function() {
this.renderChild(BannerBoxView, {model: model});
}
});
The helper function we made will let you instantiate, append and render your child views with a single line of code.
I partially answer to myself. More than circular references (I'm passing only a dom element), drawbacks could arise for the self-appending functionality I'd like to use in child's render() method. The reason is possible memory leaks when having large number of views. There is a good explanation here:
http://ozkatz.github.io/avoiding-common-backbonejs-pitfalls.html
I should use var container = document.createDocumentFragment() in the parent view and then maybe pass container to the child view.
Also, following discussions above, and still not fully convinced of the various points (mine first :P) I'm using sort of bridge code. For now, I like doing this: I don't pass parent's dom element as a constructor argument. Instead, I pass it directly to the child's render(). The code is cleaned out to the bare bones:
//parent
var CustomBannersView = Backbone.View.extend({
initialize:function(){
this.groups_container = $('.groups-container');
this.group_views = {};
this.init();
this.set_events();
}
,init:function(){
//instantiate views without rendering for later use
this.collection.each(function(model){
this.group_views[model.cid] = new GroupView({ model:model, id:'group-' + model.cid });
},this);
}
,render:function(){
var temp_box = document.createDocumentFragment();
//render views without dom refresh. Passing the box.
_.each(this.group_views, function(groupview){ groupview.render(temp_box); });
//add container
this.groups_container.append(temp_box);
}
//dom events ----
,events:{
'click .create-gcontainer-button': function(){
this.collection.add(new Group());
}
}
,set_events:function(){
this.listenTo(this.collection,'add',function(model, collection, options){
//render a single subview, passing the main container
//no refresh problem here since it's a single view
this.group_views[model.cid] = new GroupView({ model: model, id:'group-' + model.cid }).render(this.groups_container);
});
}
});//end view
//child
var GroupView = Backbone.View.extend({
tagName: 'fieldset'
,className: 'group'
,initialize:function(){
this.template = Handlebars.compile($('#group-container').html());
}
,render:function(box){//box passed by parent
this.$el.html(this.template(this.model.toJSON()));
$(box).append(this.$el);
//now I can set things based on dom parent, if needed
return this;
}
});
I just realized that I have no idea what the heck I am doing when it comes to backbone. I came to this realization when trying to figure out a cogent strategy for removing the view's event listeners on the model. Then I asked "well, where is the model anyways now that the view has been rendered to the DOM?" and then I asked "how is this model object that I created inside a function body, and is therefore out of scope now that I have rendered the view to the DOM, maintaining state?"
AHHHHHHHH!!!!!!!!!
Ex.
View Constructor
Timeclock.Views.JobNewView = Backbone.View.extend({
template: JST['jobs/_form'],
events:{
'blur #job_form :input':'assignValue'
},
initialize: function(options){
this.listenTo(this.model, 'failed-request', this.failedLocationRequest);
this.listenTo(this.model, 'updated-location', this.updatedLocation);
this.listenTo(this.model, 'sync', this.renderJobView);
this.listenTo(this.model, 'invalid', this.displayModelErrors);
this.listenTo($(window), 'hashchange', this.clearListeners);
},
render: function(){
this.$el.html(this.template({attributes: this.model.attributes}));
this.$el.find('#address_fields').listenForAutoFill();
return this;
},
assignValue: function(e){
var $field = $(e.currentTarget)
var attr_name = $field.attr('name');
var value = $field.val();
this.model.set(attr_name, value);
}...
});
Function rendering view to the DOM
renderCollaboratingView: function(e){
var job = this.model;
var $row = $(e.currentTarget);
job.set({customer_id: $row.data('id')});
var model_view = new this.ViewConstructor({model: job});
$container.html(model_view.render().el);
}
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
I understand that backbone views are just a wrapper to declaratively write DOM listeners but how are DOM events acting on the underlying model object in the example above? As soon as the renderCollaboratingView() function has exited how is the model that I passed to the view still being interacted with?
I can think of two ways:
1) The model object is bound to the DOM through a jquery object. All the event listeners that I declare in my view all know where the underlying model object lives on the jquery object(the 'model' attribute?).
2) Backbone is creating some object namespace that the view knows about where it stores models and collections that back the DOM. I have a feeling it's #1 but who knows.
Once again, I got here because I was trying to understand why I need to remove the listeners on the model that I passed into view in the first place. If backbone views are really just jquery objects then aren't jquery listeners removed from DOM elements when the element backing the jquery object is removed from the DOM? Do I only need to remove the listeners if I am going to not destroy the view entirely and save it for later use?
Any help that can be given would be greatly apprecaited. Having an existential crisis.
Thanks
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
Backbone Models and Views are simply Javascript objects that live in-memory in the scope of the page (like any other Javascript). If you were to do ...
var name = 'Peter';
var person = new Backbone.Model({ name: 'Peter' });
var view = new Backbone.View({ model: person } );
... then name, person, and view are all just objects in memory. They have no relation to jQuery; they have no relation to the DOM. The View happens to be able to create DOM elements if you implement render(), but even then those elements don't ever have to ever be attached to the page's live DOM at all.
... how are DOM events acting on the underlying model object in the example above? As soon as the renderCollaboratingView() function has exited how is the model that I passed to the view still being interacted with?
Based on the code you've shown, the model isn't being interacted with directly. Your events hash ...
events:{
'blur #job_form :input':'assignValue'
},
... does say that any time a blur event happens in the job_form element, it will call a method on the view called assignValue. That method may interact with the model (it probably does, right?), but DOM events don't directly cause interaction with the model at all.
If backbone views are really just jquery objects then aren't jquery listeners removed from DOM elements when the element backing the jquery object is removed from the DOM?
Backbone's listeners are wholly different than jQuery listeners. They listen for Backbone-centric events. See here for the list of built-in events that Backbone components fire. A View's events hash is a nice convention that is used to listen for DOM events; it's basically a nice wrapper around the jQuery concept of event delegation.
Do I only need to remove the listeners if I am going to not destroy the view entirely and save it for later use?
If you don't remove listeners, they will continue to run whenever the related event happens, regardless of whether the listening component is changing the page. Suppose you had a Backbone.View that did something like this:
var MyView = Backbone.View.extend({
// ...
events: {
// Don't do this!
'click': '_onClick'
},
// ...
_onClick: function() {
this.$el.append('Clicked!');
}
});
Any time any click DOM event happens on the page, this view will append the text Clicked! to its internal DOM element. When the view is attached to the page's DOM, Clicked! would appear on every click. When the view was removed from the DOM, the function would still run on every click... but since the View's internal root element wasn't attached to anything the function would have no effect.
It's a type of a memory leak, as any instance of MyView will ever be cleared up by the garbage collector. But the particularly nefarious side effect is it also uses CPU time to do something that is completely worthless. Now imagine if the event listener did anything of consequence. Performance of the page will suffer.
JavaScript has garbage collection. Objects do not get destroyed then they go out of scope. An Object X get garbage collected by the runtime system, when it sees that nobody is having a reference (or is pointing) to X.
A Backbone View is also an object. An object can store reference to another object.
In your renderCollaboratingView, you wrote :
var model_view = new this.ViewConstructor({model: job});
this model_view is your view's object. You passed your job which is your model you got from :
renderCollaboratingView: function(e){
var job = this.model;
....
}
You can look at this line in backbone annotated code : BackBone View Options. (I would suggest to look at the link after you have read the answer)
The line is :
var viewOptions = ['model', 'collection', 'el', 'id',
'attributes', 'className', 'tagName', 'events'];
and then Backbone View is defined as : BackBone View
It is :
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
options || (options = {});
_.extend(this, _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
};
Look at line :
_.extend(this, _.pick(options, viewOptions));
and your code :
var model_view = new this.ViewConstructor({model: job});
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
If you merge the dots : You are passing a model to your view. You can also pass other like 'collection', 'el', 'id', ... in viewOptions.
They get pick from your passed object {model: job} and extended in the view object.
This is how your view object has reference to the model that it was given.
Once again, I got here because I was trying to understand why I need to remove the listeners on the model that I passed into view in the first place.
As i said, just removing a view-object from DOM is not going to destroy it. You would have to remove all references of view-object that other objects (here model) have.
When you said :
initialize: function(options){
this.listenTo(this.model, 'failed-request', this.failedLocationRequest);
....
in your view. You told the model to call your view-object's failedLocationRequest on model's event failed-request. This is possible only when your model's object would store reference to view's object. So, your view is not destroyed.
view-object(s) not in dom would continue receiving such events from models and all other places where they registered (except dom) and would do things in the background, that you just never wanted. Definitely not what you wanted..
simple advice, call remove on your view. BackBone View remove
and read stopListening
I am learning Backbone.
I am wondering whether or not a Backbone View always requires a Backbone Model.
For example, let's say I have a panel that contains two child panels. The way I would structure this is with a parent view for the main panel, then two child views for the child panels...
var OuterPanel = Backbone.View.extend({
initialize: function() {
this.innerPanelA = new InnerPanelA(innerPanelAModel);
this.innerPanelB = new InnerPanelB(innerPanelBModel);
},
});
var outerPanel = new OuterPanel();
The parent view is really just a container. It may have some controls in it, but no data that needs to be persisted. Is this the proper way to do it? Or is this bad practice?
Thnx (in advance) for your help
As said in Backbone.View docs
Backbone views are almost more convention than they are code — they
don't determine anything about your HTML or CSS for you, and can be
used with any JavaScript templating library.
In other words, if you don't have a model, don't use a model. On the other hand, I would inject the children models as options to the outer view instance and not rely on global variables, something like this:
var OuterPanel = Backbone.View.extend({
initialize: function(options) {
this.innerPanelA = new InnerPanelA({model: options.modelA});
this.innerPanelB = new InnerPanelB({model: options.modelB});
}
});
var outerPanel = new OuterPanel({
modelA: innerPanelAModel,
modelB: innerPanelBModel
});
I see many tutorials which don't follow the supposedly best practice of making a model, a view and collection for that model then a view for the collection. Which would be the parent view?
How do I make a view for a collection? Also, is it possible for it to keep track of when a model is added or deleted for it to update/re-render?
You must do something like this in your collection view:
var view = Backbone.View.extend({});
var myView = new view({'collection' : new collection});
To handle add/remove event, use this in your initialize function:
this.collection.on("add", this.onAdd, this);
this.collection.on("remove", this.onRemove, this);
and in your model view:
this.model.on("change", this.onUpdate,this);
See it here: http://www.neiker.com.ar/backbone/
(Sorry, I don't speak english)
EDIT: Just use marionette:
https://github.com/marionettejs/backbone.marionette/blob/master/docs/marionette.collectionview.md
I'm going through the process of learning Backbone.js and I've come across a few things that look like they work... but behind the scenes, they might be causing some problems. One such issue is design patterns for swapping views.
I have a menu on the left (and corresponding view) that contains a list of users. Each user, when clicked, will display their corresponding list of movies to the right into another view. I do this by getting the model of the clicked user, building a new Movie collection based on that model, and rendering a new Movie View (to the right). If I click on another user, it does the same thing: it gets the user model, builds a new Movie collection, and renders a new Movie View to the right, replacing the DOM element. This looks fine -- but I'm worried about all of the new objects/bindings that are being created, and potential issues that could arise. So, what are my alternatives?
(1) Should I be trying to have the view redraw when the collection changes? How do I do this if I'm creating new collections?
(2) Or should I be unbinding everything when another user is clicked?
Here is my userlist view:
Window.UsersList = Backbone.View.extend({
el: $("#users"),
initialize: function(){
this.collection.bind('reset', this.render, this);
},
render: function(){
var users = [];
this.collection.each(function(user){
users.push(new UsersListItem({model: user}).render().el);
},this);
$(this.el).html(users);
return this;
}
});
In my UsersListItem view I capture the click event and call show_user_movies in my controller:
show_user_movies: function(usermodel){
// fetchMovies() lazy-loads and returns a new collections of movies
var movie_collection = usermodel.fetchMovies();
movie_list = new MovieList({collection: movie_collection, model: usermodel});
movie_list.render();
},
Thanks for any suggestions.
Just re-use the same MovieList view along with it's associated collection, using reset(models) to update the models in the collection, which should re-render the view. You can use the same pattern you have above with your MovieList view binding to the collection's reset event and re-rendering itself at that time. Note that usermodel.fetchMovies() doesn't follow the backbone asynchronous pattern of taking success/error callbacks, so I don't think the code works as is (maybe you simplified for the purpose of this question), but the point is when the new set of models arrives from the server, pass it to movie_list.collection.reset and you're good to go. This way you don't have to worry about unbinding events and creating new views.
show_user_movies: function(usermodel){
// fetchMovies() lazy-loads and returns a new collections of movies
movie_list.collection.reset(usermodel.fetchMovies().models);
},