My question is around the best way to sub view a rendered view with Backbone.
Whilst there are a lot of blogs around the topic, I haven't been able to find any practical ideas that I can apply to this use case.
Backstory
On page load, backbone fetches and renders the remote model and remote page template.
The page template is made up of 3 fieldsets, each containing lots of readonly data and images, with an [edit] button.
+-----------------------+ +-----------------------+
| ID: ViewA | | ID: ViewB |
| | | |
| | | |
| EDIT | | EDIT |
+-----------------------+ +-----------------------+
+-----------------------+
| ID: ViewC |
| |
| |
| EDIT |
+-----------------------+
When [edit] is clicked by the user, I'd like a sub view created, the partial (underscore template) fetched, the existing model applied to it, and finally, it to replace the fieldset's innerHTML.
This will make the previously readonly content in the fieldset, editable and saveable by the user. Once saved (or cancelled) by the user, it should push back to the server, and re-render the readonly view.
For argument sake, let's say templates are hosted at /templates/edit/<fieldsetid>.html
Here is the existing code:
Model and Collection
// Model
window.Page = Backbone.Model.extend();
// Collection
window.PageCollection = Backbone.Collection.extend({
model: Page,
url: '/page/view.json'
});
The View
window.PageView = Backbone.View.extend({
initialize:function () {
this.model.bind("reset", this.render, this);
},
events:{
'click fieldset a[role=edit-fieldset]' : 'edit'
},
edit: function(e){
e.preventDefault();
// #TODO Do some subview awesomeness here?
},
render:function (eventName) {
_.each(this.model.models, function (data) {
$(this.el).append(this.template({data: data.attributes}));
}, this);
return this;
}
});
Router
var AppRouter = Backbone.Router.extend({
routes:{
"page/view":"pageView"
},
pageView:function (action) {
this.page = new PageCollection();
this.pageView = new PageView({model:this.page});
this.page.fetch();
$('#content').html(this.pageView.render().el);
}
});
Template Loader
// Borrowed from:
// http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/
// Should probably move to RequireJS and AMDs
utils.loadTemplate(['PageView'], function() {
var app = new AppRouter();
Backbone.history.start();
});
So with that in mind, what are your thoughts? Am I on the right track? Is there another pattern I should look at?
Thanks in advance.
The way I usually approach these situations is as follows (using the existing 'loadTemplate', Page and PageCollection architecture above):
// Model
window.Page = Backbone.Model.extend();
// Collection
window.PageCollection = Backbone.Collection.extend({
model: Page,
url: '/page/view.json'
});
window.PageView = Backbone.View.extend({
template: templates.pageViewTemplate,
render: function() {
var html = this.map(function(model) {
// Important! We don't need to store a reference to the FieldSetViews.
// Simply render them and by passing in the model, we can update the view
// as needed when the model changes.
//
// This is also good to reduce the potential for memory-leaks.
return new FieldSetView({
model: model
}).render();
});
// Do one DOM update.
this.$el.html(html);
return this.$el;
}
});
window.FieldSetView = Backbone.View.extend({
events: {
"click .edit": "_onEdit"
},
template: templates.fieldSetTemplate,
render: function() {
// Replace the top-level div created by backbone with that of your
// template (i.e fieldset). This gets around having to use
// the tagName property which should really be in your template.
this.setElement(this.template.render(this.model.toJSON()));
return this.$el;
},
_onEdit: function(e) {
// Handle edit mode here by passing the model to another view
// or by enhancing this view to handle both edit and view states.
}
});
Related
I wonder if possible to pass multiple collections into a view once fetched, something like:
Collection1 = new Collections.Collection();
Collection1.fetch({
success: function (collection) {
//save it somewhere
}
});
Collection2 = new Collections.Collection();
Collection2.fetch({
success: function (collection) {
//save it somewhere
}
});
So once they are loaded... render my view (which also, Idk how to wait until both are fetched, so any help would be appreciated).
var template = _.template( $('#myTemplate').html(), { col1: collection1, col2 : collection2 } ) ;
Is this posible?, if not, what could I do if I need to access to different collections values into a template?
Thanks a ton in advanced!
Backbone .fetch return a promise object, you can use $.when
$.when(firstCollection.fetch(), secondCollection.fetch()).then(function () {
//both are fetched
});
Yes you can, if your template is ready to work with col1 and col2, load both collections in the view, and setup event listeners so you re-render when any of them is ready:
var MyDoubleColView = Backbone.View.extend({
initialize: function(options){
this.firstCollection = new Collections.Collection();
this.secondCollection = new Collections.Collection();
this.listenTo(this.firstCollection, "reset", this.render);
this.listenTo(this.secondCollection, "reset", this.render);
this.firstCollection.fetch({reset:true});
this.secondCollection.fetch({reset:true});
},
render: function(){
var template = _.template( $('#myTemplate').html(),
{ col1: this.firstCollection.toJSON(), col2 : this.secondCollection.toJSON() }) ;
}
});
This will cause your view to re-render when any of the collections is hydrated from the server. I've used the reset event but you can use any other of the collection server-related events, like sync.
Have your template be ready for having an empty collection and you'll be good to go. :)
Background:
I am making changes to an application that uses backbone.js with Handlebars as the templating engine. After a change event fires I need to create html that is appended to the current DOM structure which is basically just a spit-out of information that is contained in the model. This change needed to fit in the already established application structure.
Issue:
I have created a new view that uses a Handlebars template and the model to create the html. I then instantiate that view and call the render function and append the output using JQuery. What I am noticing is that when the html is rendered the model that is passed in because attributes on the $el instead of filling in the template (like I think it should).
View I'm altering:
$.hart.TestView = Backbone.View.extend({
tagName: "li",
template: Handlebars.compile($('#templateOne').html()),
initialize: function () {
this.model.on('change', function () {
this.createMoreInfoHtml();
}, this);
},
selectSomething: function () {
this.$el.removeClass('policies');
this.createMoreInfoHtml(); //function created for new view stuff
},
createMoreInfoHtml: function () {
var id = this.$el.attr('data-id', this.model.get("ID"));
$('.info').each(function () {
if ($(this).parent().attr('data-id') == id
$(this).remove();
});
var view = new $.hart.NewView(this.model, Handlebars.compile($("#NewTemplate").html()));
$('h1', this.$el).after(view.render().el);
},
render: function () {
... //render logic
}
});
View I Created:
$.hart.NewView = Backbone.View.extend({
initialize: function (model, template) {
this.model = model;
this.template = template;
},
render: function () {
this.$el.html(this.template({ info: this.model }));
this.$el.addClass('.info');
return this;
}
});
Json the is the model:
{
"PetName":"Asdfasdf",
"DateOfBirth":"3/11/2011 12:00:00 AM",
"IsSpayNeutered":false,
"Sex":"F",
"SpeciesID":2,
"ID":"ac8a42d2-7fa7-e211-8ef8-000c2964b571"
}
The template
<script id="NewTemplate" type="text/html">
<span>Pet Name: </span>
<span>{{this.PetName}}</span>
</script>
So now to the question: What am I doing wrong? Why are the properties of the model being created as attributes on the $el instead of filling in the template? Can someone please direct me as to how to get the results I am looking for?
Let's skip the problem Jack noticed.
The way you're creating your view is just wrong. It may work as you get the expected arguments in the initialize function, but it has unexpected behaviors you don't see. See the View's constructor:
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
Now let's have a look at this _configure method:
_configure: function(options) {
if (this.options) options = _.extend({}, _.result(this, 'options'), options);
_.extend(this, _.pick(options, viewOptions));
And of course...
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
Ok here we are... Basically when passing the model as the options argument, you're passing an object with an attributes key (the attributes of your model). But this attributes key is also used in the View to bind attributes to its element! Therefore the behavior your noticed.
Now, other wrong thing. You're compiling your template each time you create a new function, but not using it as a singleton either. Put your template in the view:
$.hart.NewView = Backbone.View.extend({
template: Handlebars.compile($("#NewTemplate").html(),
And change your view's creation to make the whole thing work:
new $.hart.NewView({model: this.model});
Oh, and get rid of this useless initialize method. You're just doing things Backbone already does.
I have a two views:
1 LeftView (maximized when RightView is minimized & vice versa)
2 RightView (containing)
- collection of
- RightItemView (rendering RightItemModel)
When RightView is maximized and the user clicks a RightItemView, I want to maximize LeftView and display something according to the data from the clicked RightItemView.
What's the proper way to wire them?
I would recommend using the Backbone.Events module:
http://backbonejs.org/#Events
Basically, this line is all it takes to create your event dispatcher:
var dispatcher = _.clone(Backbone.Events);
Then all of your views can trigger/listen for events using the global dispatcher.
So, in RightItemView you would do something like this in the click event:
dispatcher.trigger('rightItemClick', data); // data is whatever you need the LeftView to know
Then, in LeftView's initialize function, you can listen for the event and call your relevant function:
dispatcher.on('rightItemClick', this.maximizeAndDisplayData);
Assuming your LeftView would have a function like so:
maximizeAndDisplayData: function(data) {
// do whatever you need to here
// data is what you passed with the event
}
The solution #jordanj77 mentioned is definitely one of the correct ways to achieve your requirement. Just out of curiosity, I thought of another way to achieve the same effect. Instead of using a separate EventDispatcher to communicate between the two views, why shouldn't we use the underlying model as our EventDispatcher? Let's try to think in those lines.
To start with, add a new boolean attribute to the RightItem model called current and default it to false. Whenever, the user selects the RightItemView, set the model's current attribute to true. This will trigger a change:current event on the model.
var RightItem = Backbone.Model.extend({
defaults: {
current: false,
}
});
var RightItemView = Backbone.View.extend({
events: {
'click li': 'changeCurrent'
}
changeCurrent: function() {
this.model.set('current', true);
}
});
On the other side, the LeftView will be handed a Backbone.Collection of RightItem models during creation time. You would anyways have this instance to supply the RightView isn't it? In its initialize method, the LeftView will listen for change:current event. When the event occurs, LeftView will change the current attribute of the model it is currently displaying to false and start displaying the new model that triggered this event.
var LeftView = Backbone.View.extend({
initialize: function() {
this.collection.on('change:current', this.render, this);
},
render: function(model) {
// Avoid events triggered when resetting model to false
if(model.get('current') === true) {
// Reset the currently displayed model
if (this.model) {
this.model.set('current') = false;
}
// Set the currently selected model to the view
this.model = model;
// Display the view for the current model
}
}
});
var leftView = new LeftView({
// Use the collection that you may have given the RightView anyways
collection: rightItemCollection
});
This way, we get to use the underlying model as the means of communication between the Left and Right Views instead of using an EventDispatcher to broker for us.
The solution given by #Ganeshji inspired me to make a live example
I've created 2 views for this.
var RightView = Backbone.View.extend({
el: $('.right_view'),
template: _.template('<p>Right View</p>'),
renderTemplate: function () {
this.$el.html('');
this.$el.append(this.template());
this.$link = this.$el.append('Item to view').children('#left_view_max');
},
events: {
'click #left_view_max' : 'maxLeftView'
},
maxLeftView: function () {
//triggering the event for the leftView
lView.trigger('displayDataInLeftView', this.$link.attr('title'));
},
initialize: function (options) {
this.renderTemplate();
}
});
var LeftView = Backbone.View.extend({
el: $('.left_view'),
template: _.template('<p>Left View</p>'),
renderTemplate: function () {
this.$el.html('');
this.$el.append(this.template());
},
displayDataInLeftView: function (data) {
this.$el.append('<p>' + data + '</p>');
},
initialize: function (options) {
//set the trigger callback
this.on('displayDataInLeftView', this.displayDataInLeftView, this);
this.renderTemplate();
}
});
var lView = new LeftView();
var rView = new RightView();
Hope this helps.
Quick summary of my problem: first rendering of the view contains the elements in the collection, and the second rendering doesn't. Read on for details...
Also of note: I do realize that the following code represents the happy path and that there is a lot of potential error handling code missing, etc, etc...that is intentional for the purpose of brevity.
I'm pretty new with backbone.js and am having some trouble figuring out the right way to implement a solution for the following scenario:
I have a page shelled out with two main regions to contain/display rendered content that will be the main layout for the application. In general, it looks something like this:
-------------------------------------------
| | |
| | |
| Content | Actions |
| Panel | Panel |
| | |
| | |
| | |
| | |
| | |
| | |
-------------------------------------------
I have an HTTP API that I'm getting data from, that actually provides the resource information for various modules in the form of JSON results from an OPTIONS call to the base URL for each module. For example, an OPTIONS request to http://my/api/donor-corps returns:
[
{
"Id":0,
"Name":"GetDonorCorps",
"Label":"Get Donor Corps",
"HelpText":"Returns a list of Donor Corps",
"HttpMethod":"GET",
"Url":"https://my/api/donor-corps",
"MimeType":"application/json",
"Resources":null
}
]
The application has a backbone collection that I'm calling DonorCorpsCollection that will be a read-only collection of DonorCorpModel objects that could potentially be rendered by multiple different backbone views and displayed in different ways on different pages of the application (eventually...later...that is not the case at the moment). The url property of the DonorCorpsCollection will need to be the Url property of the object with the "GetDonorCorps" Name property of the results of the initial OPTIONS call to get the API module resources (shown above).
The application has a menubar across the top that has links which, when clicked, will render the various pages of the app. For now, there are only two pages in the app, the "Home" page, and the "Pre-sort" page. In the "Home" view, both panels just have placeholder information in them, indicating that the user should click on a link on the menu bar to choose an action to take. When the user clicks on the "Pre-sort" page link, I just want to display a backbone view that renders the DonorCorpsCollection as an unordered list in the Actions Panel.
I'm currently using the JavaScript module pattern for organizing and encapsulating my application code, and my module currently looks something like this:
var myModule = (function () {
// Models
var DonorCorpModel = Backbone.Model.extend({ });
// Collections
var DonorCorpsCollection = Backbone.Collection.extend({ model : DonorCorpModel });
// Views
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.template = _.template($('#pre-sort-actions-template').html());
this.collection.bind('reset', this.render); // the collection is read-only and should never change, is this even necessary??
},
render : function () {
$(this.el).html(this.template({})); // not even exactly sure why, but this doesn't feel quite right
this.collection.each(function (donorCorp) {
var donorCorpBinView = new DonorCorpBinView({
model : donorCorp,
list : this.collection
});
this.$('.donor-corp-bins').append(donorCorpBinView.render().el);
});
return this;
}
});
var DonorCorpListItemView = Backbone.View.extend({
tagName : 'li',
className : 'donor-corp-bin',
initialize : function () {
_.bindAll(this, 'render');
this.template = _.template($('#pre-sort-actions-donor-corp-bin-view-template').html());
this.collection.bind('reset', this.render);
},
render : function () {
var content = this.template(this.model.toJSON());
$(this.el).html(content);
return this;
}
});
// Routing
var App = Backbone.Router.extend({
routes : {
'' : 'home',
'pre-sort', 'preSort'
},
initialize : function () { },
home : function () {
// ...
},
preSort : function () {
// what should this look like??
// I currently have something like...
if (donorCorps.length < 1) {
donorCorps.url = _.find(donorCorpResources, function (dc) { return dc.Name === "GetDonorCorps"; }).Url;
donorCorps.fetch();
}
$('#action-panel').empty().append(donorCorpsList.render().el);
}
})
// Private members
var donorCorpResources;
var donorCorps = new DonorCorpsCollection();
var donorCorpsList = new DonorCorpsListView({ collection : donorCorps });
// Public operations
return {
Init: function () { return init(); }
};
// Private operations
function init () {
getAppResources();
}
function getAppResources () {
$.ajax({
url: apiUrl + '/donor-corps',
type: 'OPTIONS',
contentType: 'application/json; charset=utf-8',
success: function (results) {
donorCorpResources = results;
}
});
}
}(myModule || {}));
Aaannnd finally, this is all using the following HTML:
...
<div class="row-fluid">
<div id="viewer" class="span8">
</div>
<div id="action-panel" class="span4">
</div>
</div>
...
<script id="pre-sort-actions-template" type="text/template">
<h2>Donor Corps</h2>
<ul class="donor-corp-bins"></ul>
</script>
<script id="pre-sort-actions-donor-corp-bin-view-template" type="text/template">
<div class="name"><%= Name %></div>
</script>
<script>
$(function () {
myModule.Init();
});
</script>
...
So far, I've been able to get this to work the first time I click on the "Pre-sort" menu link. When I click it, it renders the list of Donor Corps as expected. But, if I then click on the "Home" link, and then on the "Pre-sort" link again, this time I see the header, but the <ul class="donor-corp-bins"></ul> is empty with no list items in it. ... and I have no idea why or what I need to be doing differently. I feel like I understand backbone.js Views and Routers in general, but in practice, I'm apparently missing something.
In general, this seems like a fairly straight-forward scenario, and I don't feel like I'm trying to do anything exceptional here, but I can't get it working correctly, and can't seem to figure out why. An assist, or at least a nudge in the right direction, would be hugely appreciated.
So, I've figured out a solution here. Not sure if its the best or the right solution, but its one that works at least for now.
There seems to have been a couple of issues. Based on Rob Conery's feedback, I started looking at different options for rendering the collection with my view template, and this is what I came up with:
As I mentioned in the comment, we're using Handlebars for view templating, so this code uses that.
I ended up changing my view template to look like this:
<script id="pre-sort-actions-template" type="text/x-handlebars-template">
<h2>Donor Corps</h2>
<ul class="donor-corp-bins">
{{#each list-items}}
<li class="donor-corp-bin">{{this.Name}}</li>
{{/each}}
</ul>
</script>
Then, I removed DonorCorpListItemView altogether, and changed DonorCorpsListView to now look like this:
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.collection.bind('reset', this.render);
},
render : function () {
var data = { 'list-items' : this.collection.toJSON() };
var template = Handlebars.compile($('#pre-sort-actions-template').html());
var content = template(data);
$(this.el).html(content);
return this;
}
});
So, that seems to be working and doing what I need it to do now.
I clearly still have a lot to learn about how JS and Backbone.js work though, because the following DOES NOT WORK (throws an error)...and I have no idea why:
var DonorCorpsListView = Backbone.View.extend({
initialize : function () {
_.bindAll(this, 'render');
this.template = Handlebars.compile($('#pre-sort-actions-template').html());
this.collection.bind('reset', this.render);
},
render : function () {
var data = { 'list-items' : this.collection.toJSON() };
var content = this.template(data);
$(this.el).html(content);
return this;
}
});
Hopefully this will be helpful to someone.
Your rendering code looks odd to me for the DonorCorpListView - it looks like you're templating before you have any data. You need to compile that template with the data to get the HTML output.
So, the rendering logic should be somthing like:
var template = $([get the template).html();
template.compile(this.collection.toJSON());
this.el.append(template.render());
I used some pseudo code here as I'm not sure what your templating mechanism is (I like Handlebars myself).
I'm doing my first application in backbone and i get a strange thing happening trying to attach an event.
I got this code so far:
//View for #girl, EDIT action
GirlEditView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
console.log(attr);
this.render();
},
render: function() {
var template = _.template( $("#girl_edit").html(), this.variables );
$(this.el).html( template );
$("#edit_girl").modal('show');
}
});
//View for #girl
GirlView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
this.render();
},
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
},
events: {
"click p.modify": "modify"
},
modify: function() {
//calls to modify view
new GirlEditView({el : $("#edit_girl")}, this.variables);
}
});
//One girl from the list
Girl = Backbone.Model.extend({
initialize: function() {
this.view = new GirlView({el : $("#content")}, this.attributes );
}
});
//all the girls
Girls = Backbone.Collection.extend({
model: Girl,
});
//do magic!
$(document).ready(function() {
//Underscore template modification
_.templateSettings = {
escape : /\{\[([\s\S]+?)\]\}/g,
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
}
//get initial data and fill the index
var list = [];
$.getJSON('girls.json', function(data) {
list = [];
$.each(data, function(key, val) {
list.push( new Girl(val) );
});
var myGirls = new Girls(list);
console.log( myGirls.models);
});
});
As you can see.
I'm using a collection to store all the girls and the data comes from a REST api in ruby.
Each girls create a new model instance and inside i attached a view instance.
I don't know if it's a good practice but i can't think a better way to do it.
Each view makes a content with a unique id. girl-1 girl-2 and go on.
Now, the template have a edit button.
My original idea is to attack the onclick event and trigger the edit view to get rendered.
That is working as expected.
The proble so far is:
When the events triggers, all the collection (girls) fire the edit view, not the one that "owns" the rendered view.
My question is what i'm doing wrong?
Thanks a lot
All the edit-views come up because all the GirlViews are using the same el:
this.view = new GirlView({el : $("#content")}, this.attributes );
and then you render be appending more HTML:
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
}
Backbone events are bound using delegate on the view's el. So, if multiple views share the same el, you'll have multiple delegates attached to the same DOM element and your events will be a mess of infighting.
You have things a little backwards: models do not own views, views watch models and collections and respond to their events. You'll see this right in the documentation:
constructor / initialize new View([options])
[...] There are several special options that, if passed, will be attached directly to the view: model, collection, [...]
Generally, you create a collection, c, and then create the view by handing it that collection:
var v = new View({ collection: c })
or you create a model, m, and then create a view wrapped around that model:
var v = new View({ model: m })
Then the view binds to events on the collection or model so that it can update its display as the underlying data changes. The view also acts as a controller in Backbone and forwards user actions to the model or collection.
Your initialization should look more like this:
$.getJSON('girls.json', function(data) {
$.each(data, function(key, val) {
list.push(new Girl(val));
});
var myGirls = new Girls(list);
var v = new GirlsView({ collection: myGirls });
});
and then GirlsView would spin through the collection and create separate GirlViews for each model:
var _this = this;
this.collection.each(function(girl) {
var v = new GirlView({ model: girl });
_this.$el.append(v.render().el);
});
Then, GirlView would render like this:
// This could go in initialize() if you're not certain that the
// DOM will be ready when the view is created.
template: _.template($('#girl_template').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON());
return this;
}
The result is that each per-model view will have its own distinct el to localize the events. This also makes adding and removing a GirlView quite easy as everything is nicely wrapped up in its own el.