Is it technically possible to nest views, using templating, something like that:
<%= new PhotoCollectionView({model:new PhotoCollection(model.similarPhotos)}).render().el) %>
I can put all the stuff in the render method as well, but templating gives much more room for flexibility and layout.
I tried the aforementioned variant, but all I get as a result on the screen is [HTMLDivElement].
If I try to extract just the HTML out ouf it, using jQuery's HTML, I get it rendered, but it turns out that the DOM nodes that get printed out are different from the ones that the views hold a reference to, because no interaction whatsoever with those DOM nodes is possible using the view instance. For instance if within the view I say $(this.el).hide(), nothing will happen.
What is the proper way, if any?
I typically render the parent view first. I then use the this.$('selector') method to find a child element that I can use as the el of the child view.
Here is a full example:
var ChildView = Backbone.View.extend({
//..
})
var ParentView = Backbone.View.extend({
template: _.template($('#parent-template').html()),
initialize: function() {
_.bindAll(this, 'render');
}
render: function() {
var child_view = new ChildView({ el: this.$('#child-el') }); //This refers to ParentView.
return this;
}
});
var v = new ParentView();
v.render();
The accepted answer has a major flaw, which is the fact that the ChildView is going to be re-initialised everytime it's rendered. This means you will lose state and potentially have to re-initialised complicated views on each render.
I wrote a blog about this here: http://codehustler.org/blog/rendering-nested-views-backbone-js/
To summarise though, I would suggest using something like this:
var BaseView = Backbone.View.extend({
// Other code here...
renderNested: function( view, selector ) {
var $element = ( selector instanceof $ ) ? selector : this.$el.find( selector );
view.setElement( $element ).render();
}
});
var CustomView = BaseView.extend({
// Other code here...
render: function() {
this.$el.html( this.template() );
this.renderNested( this.nestedView, ".selector" );
return this;
}
});
You do not need to extend the Backbone view if you don't want to, the renderNested method could be put anywhere you like.
With the code above, you can now initialise the ChildView in the initialisation method and then simply render it when render() is called.
Check out the Backbone.Subviews mixin. It is a minimalist mixin built for managing nested views and does not re-initialize the child views every time the parent is rendered.
I don't know about within a template itself, but I've done it with tables and lists before. In the outer template, just have the stub:
<script type="text/template" id="table-template">
<table>
<thead>
<th>Column 1</th>
</thead>
<tbody>
</tbody>
</table>
</script>
and for the individual items:
<%= field1 %>
then in your render method, just render the individual items and append them to the tbody element...
The decision to initialize a new object every time you render seems to me very inefficient.
Particularly this:
render: function() {
var child_view = new ChildView({ el: this.$('#child-el') }); //This refers to ParentView.
return this;
}
Ideally the Rendering of the parent should be something like
render: function() {
this.$el.html(this.template());
this.childView1.render();
this.childView2.render();
}
And the children creation should happen only when initializing the parent:
initialize: function() {
this.childView1 = new ChildView1(selector1);
this.childView2 = new ChildView2(selector2);
}
the problem is that we do not have selector1 and selector2 before rendering the parent template. This is where I am stuck at right now :)
Related
I think I am missing something very trivial here. I have created a Backbone View as follows(without extending Backbone.View):
var PlayersView = new Backbone.View({
initialize: function() {
this.render();
},
render: function() {
console.log("hello World");
}
});
But it doesn't log anything when I run this code. It doesn't work when I explicitly do: PlayersView.render(); as well.
But the following code works :
var PlayersView = Backbone.View.extend({
initialize: function() {
this.render();
},
render: function() {
console.log("hello World");
}
});
var playersview = new PlayersView();
The View constructor does not accept properties to add to the constructed object. It accepts a few special options like 'model', 'tagName', and so on. But the initialize(...) and render(...) properties in your first code snippet are effectively ignored.
The proper way to provide initialize, render, is to use Backbone.View.extend({...}).
From http://backbonejs.org/#View-extend
extend
Backbone.View.extend(properties, [classProperties]) Get started with
views by creating a custom view class. You'll want to override the
render function, specify your declarative events, and perhaps the
tagName, className, or id of the View's root element.
In other words, your first view's render method was not overridden by your custom render/initialize function.
When using extend you actually let Backbone understand you wish to use your own methods instead of the "default" ones.
I want to render marionette ItemView and append result html() to my div.
My code:
var ProjectItemView = Backbone.Marionette.ItemView.extend({
template: "#ProjectItem",
tagName: 'div',
initialize: function () {
this.model.on('change', this.life, this);
this.model.on('change', this.render, this);
},
life: function () {
alert(JSON.stringify(this.model));
}
});
var tmp = new Project({project_id: 1});
tmp.fetch();
$('#container').append(new ProjectItemView({model: tmp}).$el);
alert in life: function shows model right. It means fetch works fine.
The question is - how to get html as result of view.
I also tried $('#container').append(new ProjectItemView({model: tmp}).render().el);
The problem was with the REST service that I use to populate collections/models. It returns array that contains one element - not plain object directly.
You have to react to render event from marionette.
...
onRender : function() {
$('#container').append(this.$el);
}
...
new ProjectItemView({model: tmp});
tmp.fetch();
If you want to get uncoupled, fire a distinct app event from within your view handler to the outside world. Radio might be worth considering if you're not already.
I think your problem is only the order of operations. Try this:
$('#container').append((new ProjectItemView({model: tmp})).render().el);
The way you had it before, you were invoking .render() on the constructor. With the extra parenthesis above, .render() is called on the instance.
pass element to view:
new ProjectItemView({model: tmp, el:'#container'}).render();
I am trying to use templating to render a view initially, but to update the view on subsequent calls when it is part of the document.
My app's page looks a little something like
<body>
<!-- ... -->
<div id="view_placeholder"/>
<!-- ... -->
</body>
And in pseudo-code I want to do something like so
Backbone.View.extend({
// ...
render: function() {
if (this.el *IS NOT A CHILD OF document*) {
// render the contents from the template
} else {
// update the content visibility based on the model
}
},
// ...
});
The reason for this is that the template contains quite a lot of nodes and regenerating it for every change is not practicable.
I have explored some of the data-binding libraries, e.g. rivets.js but they are a poor fit to the template:model relation.
One thing I noticed is that this.el.parentNode==null before I add it to the document, but I am not sure that this is a definitive test, and in any case if I wrap this view within another, then that test becomes less useful (or maybe I am being overly cautious as once within another view's el I have rendered my sub-template anyway.
Another option I can see is to use a field to track the rendered status, e.g.
Backbone.View.extend({
//
templateRendered:false,
// ...
render: function() {
if (!this.templateRendered) {
// render the contents from the template
this.templateRendered = true;
} else {
// update the content visibility based on the model
}
},
// ...
});
but that feels hacky to me.
So my question is:
What is the best way to track the fact that I have rendered the template fully and therefore only need to tweak the rendered template rather than re-render it (and re-insert all the sub-views)?
I think the idiomatic backbone approach is to only call a full render() on your view when you want a full render, and use model change event bindings to call sub-render functions that render smaller portions of the view.
var AddressView = Backbone.View.extend({
initialize: function (options) {
Backbone.view.prototype.initialize.call(this, options);
_.bindAll(this)
options.model.on('change:name', this.renderName);
options.model.on('change:street', this.renderStreet);
options.model.on('change:zipCode', this.renderZipCode);
},
renderName: function (model) {
this.$el.find("#name").text(model.get("name"));
},
renderZipCode: function (model) {
this.$el.find("#zipcode").text(model.get("zipCode"));
},
renderStreet: function (model) {
this.$el.find("#stree").text(model.get("street"));
},
render: function () {
//Populate this.el with initial template, subviews, etc
//assume this.template is a template function that can render the main HTML
this.$el.html(this.template(model));
this.renderName(this.model);
this.renderZipCode(this.model);
this.renderStreet(this.model);
return this;
}
});
Code as above is undoubtedly tedious. I would reconsider knockback.js or rivets.js, personally, but I believe the pattern above is the canonical vanilla backbone.js approach.
I would avoid rendering a view until its element is about to be inserted. In any case, you can find out by checking the parent of the view's element, i.e.
this.$el.parent()
should be empty if the this.$el is not part of your document.
I have this backbone that is working fine, i just want to render the collection is been fetched!
code:
var SearchView = Backbone.View.extend({
events: {
"keyup": "handleChange"
},
initialize: function(){
this.collection.bind("reset", this.updateView, this);
},
render: function(){
// this is where i need help
this.$el.next('#suggestions').append(// iterate over this.collection and apppend here) //
},
handleChange: function(){
var term = this.$el.val();
this.collection.search(term);
},
updateView: function() {
this.render();
}
});
I just want to iterate over this.collection and display the "text" attribute thats inside each collection and append it to the ('#suggestions') el. thanks
Focusing on your reset, render over collections issue, this is how I would do it. This way assumes that when you create your view, the $el is already present and you're passing it in through the View constructor so it's ready to go.
var SearchView = Backbone.View.extend({
template: _.template('<span><%= term %></span>');
initialize: function(){
this.collection.bind("reset", this.render, this);
},
render: function(){
this.addAllTerms();
return this;
},
addAllTerms: function() {
var self = this;
this.collection.each(function(model) {
self.addTerm(model);
});
},
addTerm: function(someModel) {
this.$el.next('#suggestions').append(this.template(someModel.toJSON()));
}
});
It's a bit different from your approach in a few ways. First, we utilize Underscore's template function. This could be anything from span to li to div whatever. We use the <%= %> to indicate that we're going to interpolate some value (which will come from our model.term attribute).
Instead of going to the handler then render, I just bind the collection to render.
The render() assumes we're always going to refresh the whole thing, build from scratch. addAllTerms() simply cycles through the collection. You can use forEach() or just each() which is the same thing except forEach() is 3 characters too long for my lazy bum. ;-)
Finally, the addTerm() function takes a model and uses it for the value that it will append to the #suggestions. Basically, we're saying "append the template with interpolated value". We defined the template function up above as this View object property to clearly separate the template from data. Although you could have skipped this part and just append('<span>' + someModel.get('term') + '</span>') or what not. The _.template() uses our template, and also takes any sort of object with the property that lines up with the one in our template. In this case, 'term'.
It's just a different way to do what you're doing. I think this code is a little more manageable. For example, maybe you want to add a new term without refreshing the whole collection. The addTerm() function can stand on its own.
EDIT:
Not that important but something I utilize with templates that I found useful and I didn't see it when I first started out. When you're passing the model.toJSON() into the template function, we're essentially passing all the model attributes in. So if the model is like this:
defaults: {
term: 'someTerm',
timestamp: '12345'
}
In our previous example, the attribute timestamp is also passed in. It isn't used, only <%= term %> is used. But we could easily interpolate it as well by adding it to the template. What I want to get at is that you don't have to limit yourself to data from one model. A complex template might have data from several models.
I don't know if it's the best way, but what I do is have something like this:
makeHash: function() {
var hash = {};
hash.term = this.model.get('term');
hash.category = anotherModel.get('category');
var date = new Date();
hash.dateAccessed = date.getTime();
return hash;
}
So you can easily build your own custom hash to throw into a template, aggregating all the data you want to interpolate into a single object to be passed into a template.
// Instead of .toJSON() we just pass in the `makeHash()` function that returns
// a customized data object
this.$el.next('#suggestions').append(this.template(this.makeHash()));
You can also easily pass in whole objects.
makeHash: function() {
var hash = {};
hash.term = this.model.get('term');
var animal = {
name: 'aardvark',
numLegs: 4
};
hash.animal = animal;
return hash;
}
And pass this into our template that looks like this:
template: _.template('<span><%= term %></span>
<span><%= animal.name %></span>
<span><%= animal.numLegs %></span>')
I'm not sure if this is the best way but it helped me understand exactly what data is going into my templates. Maybe obvious but it wasn't for me when I was starting out.
I found the solution to the problem, im gonna put it here for people that might want to know:
render: function(){
this.collection.forEach(function(item){
alert(item.get("text"));
});
}
I am implementing my first actual non-tutorial Backbone app, and have 2-ish questions about an aspect of using using backbone.js that isn't settling very well with me, which relates to injecting a view's rendered el into the DOM vs. using an existing element for el. I suspect I will provide you all with a few "teachable moments" here, and appreciate the help.
Most Backbone View examples I see in the web specify tagName, id and/or className, when creating a View and thereby create an el that is unattached from the DOM. They typically look something like:
App.MyView = Backbone.View.extend({
tagName: 'li',
initialize: function () {
...
},
render: function () {
$(this.el).html(<render an html template>);
return this;
}
});
But the tutorials don't always get around to explaining how they recommend getting the rendered el into the DOM. I've seen it a few different ways. So, my first question is: where is the appropriate place to call a view's render method and insert its el into the DOM? (not neccessarily one and the same place). I've seen it done in a router, in the view's initialize or render functions, or just in a root level document ready function. ( $(function () ) . I can imagine that any of these work, but is there a right way to do it?
Second, I am starting with some HTML markup/wireframe, and converting html portions to js templates corresponding to backbone views. Rather than let the view render an unattached element and providing an anchor point in the html to stick it in, I feel like its more natural, when there is only going to be one element for a view and it won't be going away, to use an existing, emptied wrapper element (often a div or span) as the el itself. That way I don't have to worry about finding the place in the document to insert my unattached el, which would potentially end up looking like this (note the extra layering):
<div id="insert_the_el_in_here"> <!-- this is all that's in the original HTML doc -->
<div id="the_el"> <!-- i used to be a backbone generated, unattached el but have been rendered and inserted -->
<!-- we're finally getting to some useful stuff in here -->
</div>
</div>
So part of my second question is, for a basically static view, is there anything wrong with using an existing element from the page's HTML directly as my view's el? This way I know its already in the DOM, in the right place, and that calling render will immediately render the view on the page. I would acheive this by passing the already exixting element to my view's constsructor as 'el'. That way, it seems to me, i don't have to worry about sticking it into the DOM (making question 1 sort of moot), and calling render will immediately update the DOM. E.g.
<form>
<div someOtherStuff>
</div>
<span id="myView">
</span>
</form>
<script type="text/template" id = "myViewContents"> . . . </script>
<script type="application/javascript">
window.MyView = Backbone.View.extend( {
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
$(this.el).html(this.template());
return this;
}
});
$(function () {
window.myView = new MyView({ el: $('#myView').get(0) });
});
</script>
Is this an OK way to do it for static views on the page? i.e., there is only one of these views, and it will not go away in any circumstance. Or is there a better way? I realize that there may be different ways to do things (i.e., in a router, in a parent view, on page load, etc.) based on how I am using a view, but right now I am looking at the initial page load use case.
Thanks
There's absolutely nothing wrong with the idea of attaching a view to an existing DOM node.
You can even just put the el as a property on your view.
window.MyView = Backbone.View.extend( {
el: '#myView',
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
this.$el.html(this.template()); // this.$el is a jQuery wrapped el var
return this;
}
});
$(function () {
window.myView = new MyView();
});
What I recommend is, do what works... The beauty of Backbone is that it is flexible and will meet your needs.
As far as common patterns are concerned, generally I find myself having a main view to keep track of over views, then maybe a list view and individual item views.
Another common pattern as far as initialization is concerned is to have some sort of App object to manage stuff...
var App = (function ($, Backbone, global) {
var init = function () {
global.myView = new myView();
};
return {
init: init
};
}(jQuery, Backbone, window));
$(function () {
App.init();
});
Like I said before though, there's really no WRONG way of doing things, just do what works.
:)
Feel free to hit me up on twitter #jcreamer898 if you need any more help, also check out #derickbailey, he's kind of a BB guru.
Have fun!
You can also send an HTML DOM Element object into the view as the 'el' property of the options.
window.MyView = Backbone.View.extend( {
initialize: function () {
this.template = _.template($('#myViewContents').html());
this.render();
},
render: function () {
this.$el.html(this.template()); // this.$el is a jQuery wrapped el var
return this;
}
});
$(function () {
window.myView = new MyView({
el: document.getElementById('myView')
});
});
Use delegate events method:
initialize: function() {
this.delegateEvents();
}
To understand why: http://backbonejs.org/docs/backbone.html#section-138 near "Set callbacks, where"
Looks also like these days you can youse setElement.