Simple view (reduced code):
My.View = Backbone.View.extend({
className: 'my-view',
initialize: function() { },
render: function() {
console.log('render');
return this;
}
});
I use it in another view as subview like this:
var myView = new My.View();
this.$el.append(myView.render().$el);
First instead of a className the view had a template and the view was rendering fine.
However when I removed the template and added className instead it is not rendering correctly.
It just renders the div and the correct class name however no logging in the render method is performed. AND when I add some html inside the render method to this.$el it never appears. Any ideas why?
UPDATE:
When I put my custom rendering code inside the afterRender method it works. Why is it not possible to overwrite the render method in my case?
this.$el.append(myView.render().el);
I would render the subview the following way.
var myView = new My.View();
this.$el.append(myView.el);
myView.render();
Related
I have a model's view set up as follows:
var PlayerView = Backbone.View.extend({
el: "div.player",
template: _.template($("#playerTemplate").html()),
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this;
}
});
and the main view is set up as follows:
var AppView = Backbone.View.extend({
el: "#players",
render: function() {
var pView = new PlayerView({model: playerModel});
this.$el.append(pView.render().$el);
}
});
If you notice the render of AppView, I am rendering the PlayerView first, and then appending its $el to AppView's $el. I was expecting an error situation here, as it would display PlayerView 2 times to main View as follows:
First, in the pView.render() where I put content in pView and
Second, in the same line where I append pView's $el to main view.
But it just appends the pView only once. How does this work?
I am not sure I have explained my question clearly; I can add more context if required.
Assuming div.player exists in DOM as you mentioned in comments,
When you do pView.render(), it adds the template inside it.
Then when you append pView's element (div.player) to AppView's element (#players), entire div.player is moved into #players.
Your code is working the way it should work.
If you intent to create multiple players, You shouldn't use el option in player view, Instead you should decorate the element created by backbone and create multiple instances of player view.
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 created a view like this:
var MyView = Marionette.LayoutView.extend({
template: #someTemplate,
regions: function() {
return {
someRegions: '.Regions',
};
},
initialize: function(options) {
if (options) {
do stuff...
}
},
onRender: function() {
var titleBar = new Bar({ options: "options" });
this.barRegion.show(titleBar);
}
});
Which is basically a view that has a top bar with some buttons.
Now, I need to create a whole bunch of different views that have the same top bar, so I want to be able to do something like this:
var SecondView = MyView.extend({
template: #template,
onRender: function() {
create content below top bar here...
}
});
When I add breakpoints I see that MyView's initialize & render functions DO NOT get called, only my SecondView's initialize and render functions get called.
So the top bar does not show up, in fact MyView's template does not appear. Only SecondView shows up.
What am I missing here?
Thanks in advance...
The problem is that your onRender method on your second view is overriding the first one, so only your SecondView.onRender code is executed.
I think you're taking the wrong approach here. Create a Layout that includes the common topbar, and add another region for the views that go below that. If that view is another Layout is OK.
Graphically:
MainLayout
--Topbar Region
--SecondBar Region
You can make your MainLayout be the Application regions, and use your router or whatever mechanism to load the needed view into the SecondBar region.
Hope it makes sense. ;)
If you insist on using inheritance, then call a second method from your MainView onRender, something like:
onRender: function() {
var titleBar = new Bar({ options: "options" });
this.barRegion.show(titleBar);
//hook for child views
this.triggerMethod("onSecondRender");
}
And then move your onRender code to onSecondRender on your child views.
Thanks everyone who tried to help me with this...
So for whoever might be interested in what I was trying to do, this is how I got it to work:
From SecondView's initialize method I had to call MyView's initialize method:
initialize: function(options) {
MyView.prototype.initialize.call(this, options);
}
Then, from SecondView's onRender method I had to call MyView's onRender method:
onRender: function() {
MyView.prototype.onRender(this);
// extra content for my SecondView goes here...
var secondViewContent = new Content();
this.someRegionInMyView.show(secondViewContent);
}
This is a new question but runs on from the last one, Dynamically load my 'IDs' into my Backbone Collection?
I now have all my models getting the data from my database. Now I want to be able to load this data into a view. But for some reason I can not get any views to work at all, this is my current view code,
var MyView= Backbone.View.extend({
el: '.page',
render: function() {
this.$el.html('CONTENT HERE FROM BACKBONE');
return this;
}
});
var testView = new MyView({});
$(document).ready(function(){
$('.page').append(testView);
})
Now '.page' is a div tag with that class set up on my page. But will not output my test text above, so what am I doing wrong?
The main aim is to have this view load the data form the model, which is all working fine, but right now I can not even get this simple test to work?
Have I forgotten to do with starting up the view functions with backbone?
All help most welcome.
Glenn.
you can initialize View inside document ready, View automatically call his render function when in initialize state, but in your case since document dose not load completely in initialize state, it fail to render
var testView;
$(document).ready(function(){
testView = new MyView
});
testView is an instance of MyView. You should not be calling append with testView. You need to invoke render method on testView instance. Since MyView el element is .page, content will be append to .page.
Use this:
$(document).ready(function(){
testView.render();
})
So i'm very new to backbone.js and not so good at JavaScript in general, so I was wondering if someone could explain to me why
I cannot define my EL property, and Template property in my view, and then use this.template in my render. Instead I have to define the template and el in my render function.
var ProductView = Backbone.View.extend({
el: $('#product-list'),
initialize: function() {
this.el.html('<span style="color:white">loading...</span>');
}, // end initialize
render: function(collection) {
// // assign the template
this.template = $('#product_template');
// Where the template will be placed
this.el = $('#product-list');
// Add the collection to the main object
this.collection = collection;
// add tthe data to the html variable
var html = this.template.tmpl(this.collection.toJSON());
// place the html in the element.
this.el.html(html);
// not even sure what the hell this is.
return this;
} // end render
});
The problem isn't in the way you're defining el or template, it's in how you're setting the call back. In Workspace, your router, you're setting the callback for your collection refresh event like this:
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render);
The problem is, you're setting a method as a callback, but you're not providing a context object as the third argument to bind - so the method is called, but this in the method refers to the global object, not the view. So this.el is undefined, because it's not looking at the view instance at all. Try:
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render, this.view);
and see how that goes.
(I made a jsFiddle to demonstrate that the el and template were set properly under normal circumstances, though it doesn't actually include the fix above, which is hard to mock up without the server-side data: http://jsfiddle.net/nrabinowitz/QjgS9/)
You can't do this:
var ProductView = Backbone.View.extend({
el: $('#product-list'),
// ...
and get anything useful in el as #product-list probably isn't even present in the DOM when your ProductView is built; so trying to use $('#product-list') for el is simply the classic "I forgot to use $(document).ready()" problem dressed up in Backbone. Using $('#product-list') for el should work if #product-list is around when you define your ProductView though.
You can do this though:
var ProductView = Backbone.View.extend({
el: '#product-list',
// ...
and then say $(this.el) when you need to do things inside your view methods. Not only is $(this.el) the usual way of using el but it also works and that's sort of important.
The same issues apply to #product_template.
Looking at your code I see this:
// INstantiate the view
this.view = new ProductView();
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render);
Presumably the render is being triggered by the reset event. But, and this is a big but, the render method isn't bound to the right this anywhere so this won't be the ProductView when render is called and this won't have anything that you expected it to; hence your bizarre "undefined" error.
You could use _.bindAll in your initialize:
initialize: function() {
_.bindAll(this, 'render');
// ...
but usually you'd want to give the view a collection when you create it and the view would bind itself to the events so your structure will still be a bit odd.
You can also supply a context (AKA this) when you call bind:
collection.bind('reset', this.render, this);