I have a view:
App.Views.Rebill = App.Views.baseView.extend({
view: 'requests._rebill_line',
tag: 'div',
className: 'row row-spaced',
render: function() {
var self = this;
App.get_view(self.view).done(function(data){
var view = Mustache.render(data, {
text: self.model.get('text'),
cost: self.model.get('cost')
});
self.$el.append(view);
var $selectVat = self.$el.find('select[name="vat"]');
var vatPicker = new App.Views.VatPicker({
model: self.model,
el: $selectVat
});
vatPicker.render();
});
return self;
}
});
this view is created in the parent:
addRebillLine: function(rebillModel){
var self = this;
var rebillView = new App.Views.Rebill({
model: rebillModel
});
self.$el.after(rebillView.render().$el);
},
I have linked this to a button, which creates an empty model, and calls the addRebillLine function. This works fine, the new Rebill view appears in the DOM when I click the button.
However, on rendering the parent view, I run through a json array, create a model with each line, and call the addRebillLine function with that model. This runs, but the Rebill views are not added to the DOM.
Further up, the parent view is itself a child view, and is attached to its parent like so:
this.$el.find('[data-role="repair-items"]').append(itemView.render().$el);
The parent and grandparent views are rendered synchronously, and the child view asynchronously (App.get_view() is basically a call to $.ajax, hence the .done())
The wierd thing is that I do the same thing with the App.Views.VatPicker view, in several other places, and that works just fine. The only difference is that I pass an element to attach to into the VatPicker view. But if I pass the parent $el element in, and run this.$parent.after(self.$el) in my done() callback, that doesn't work either.
When you call this line:
this.$el.find('[data-role="repair-items"]').append(itemView.render().$el);
it assumes your render code is synchronous (as it should be) , but you render code is not.
When render return the $el is not set yet, this is a problem in your design.
You should solve this design issue by making your templates available when the view needs them. Don't invent the wheel here, take a look at the TodoMVC backbone example.
If you want your templates to be loaded aysnc, use requirejs.
Related
I am relatively new to Backbone and I am running into this problem.
I am using Backbone with DustJS
My template looks something like this - index.dust
<div id="parentView">
<div class="section">
{>"app/inc/responseMessage" /}
<div class="userDetails">
{! A button to get user details !}
</div>
</div>
</div>
This is my partial below - responseMessage.dust
<div id="responseMessage">
{#eq key="{data.success}" value="true"}
<div class="alert alert-success success" role="alert">success</div>
{/eq}
</div>
My JS looks like this
initialize: function() {
this.responseMessageView = this.responseMessageView ||
new ResponseMessageView({
model: new Backbone.Model()
}); // View is for the partial
this.model = this.model || new Backbone.Model(); //View for the whole page
},
Below function is called when an event occurs and it does a POST and returns successfully.
primaryViewEventTrigger: function(event){
//Button click on `index.dust` triggers this event and does a POST event to the backend
this.listenToOnce(this.model, 'sync', this.primaryViewSuccess);//this will render the whole view.
this.listenToOnce(this.model, 'error', this.primaryViewError);
this.model.save({data: {x:'123'}});
}
responseViewEventTrigger: function(event){
//Button click on `responseMessage.dust` triggers this event and does a POST event to the backend
this.listenToOnce(this.responseMessageView.model, 'sync', this.responseViewSuccess);//it should only render the partial view - responseMessage.dust
this.listenToOnce(this.responseMessageView.model, 'error', this.primaryViewError);
this.responseMessageView.model.save({data: {x:'123'}});
}
primaryViewSuccess: function(model,response){
this.model.set('data', response.data);
this.render();
}
responseViewSuccess: function(model,response){
this.responseMessageView.model.set('data', response.data);
console.log(this.responseMessageView.model);
this.responseMessageView.render(); // Does not work in some cases
}
My implementations of the callback function
exports.sendEmail = function sendEmail(req, res){
req.model.data.success = true;
responseRender.handleResponse(null, req, res);
};
this.model belongs to the model of the whole page. Whereas this.responseMessageView.model is the model for the partial.
Question: This works perfectly fine in most of the cases. There is one case where it does not render the partial with the latest model values. When I click on the button on index.dust and primaryViewSuccess is executed. After which I click on another button and trigger responseViewEventTrigger. It does the POST successfully and it comes to responseViewSuccess and stores it in the model too. But it does not show it on the frontend. data.success is still not true whereas console.log(this.responseMessageView.model) show that attributes->data->success = true
But the same behavior when I refresh the page it all works perfect. Its just that when primaryViewSuccess is called and then responseViewSuccess its not taking the latest model changes. In other words model is being updated but the DOM remains the same.
What am I missing here? Thanks for your time!
You're hitting the classic Backbone render-with-subviews gotcha: When you render the index view, you are replacing all of the DOM nodes. That includes the DOM node that your responseMessageView instance is tied to. If you inspect the responseMessageView.el you'll actually see that it has been updated with the new model data, however the element isn't attached to the index's DOM tree anymore.
var Parent = Backbone.View.extend({
template: _.template(''),
render: function() {
this.$el.html(this.template());
},
initialize: function() {
this.render();
this.child = new Child();
this.$el.append(child.el);
}
});
Here when you manually call render, child.el will no longer be in the
parent.el (you can check using the inspector).
The simplest fix here is to call child.setElement after the parent renders with the newly rendered div#responseMessage element. The better fix is to detach the responseMessageView's element beforehand, and reattach after:
var Parent = Backbone.View.extend({
render: function() {
this.responseMessageView.$el.detach();
this.$el.html(this.template(this.model.toJSON()));
this.$el.find('#responseMessage').replaceWith(this.responseMessageView.el);
}
});
Try to use success callback and let me know I think your problem may come from here:
this.model.save( {att1 : "value"}, {success :handler1, error: handler2});
Also are you sure you want to use listenToOnce ? instead of listenTo ??
Try doing this.delegateEvents() after your render (http://backbonejs.org/#View-delegateEvents).
I've been trying to learn Backbone, and I'm developing an app now. But I have a problem with a view's events: App.views.ChannelView should have a click event, but it is not firing.
Here's the code:
http://pastebin.com/GgvVHvtj
Everything get rendered fine, but events won't fire. Setting the el property in the view will work, but I can't use it, and I've seen on Backbone's todo tutorial that it is possible.
How do I make events fire without a defined el property?
You must define the el element to be an existing element in your DOM. If you do not define it, fine, it will default to a div, but when you render the view, the html generated must be appended/prepended whatever, you get the point, to an existing DOM element.
Events are scoped to the view, so something's wrong with your scope. From the code you provided I can't reproduce the problem, so if you might, please provide a live example on jsfiddle/jsbin etc in order to fully understand the issue.
Demo ( in order to demonstrate the view render )
var App = {
collections: {},
models: {},
views: {},
};
App.models.Channel = Backbone.Model.extend({
defaults: {
name: '#jucaSaoBoizinhos'
}
});
App.views.ChannelView = Backbone.View.extend({
el:$('#PlaceHolder'),
events: {
"click .channel": "myhandler"
},
render: function() {
this.$el.html('<div class="channel"><button>' + this.model.get('name') + '</button></div>');
return this;
},
myhandler: function(e) {
alert(e);
console.log(this.model.get('name'));
},
});
var chView = new App.views.ChannelView({model: new App.models.Channel()});
//console.log(chView.render().el) //prints div#PlaceHolder
//without the el specified in the view it would print a div container
//but i would have to render the view into an existing DOM element
//like this
//$('#PlaceHolder').html(chView.render().el)
chView.render()
Can you try doing a
events: {
"all": "log"
}
log: function(e) {
console.log e;
}
That should log out every event that's getting fired. I find it super helpful when troubleshooting.
backbone view events can work without dom element specified. If you can't use any element at the view createion (initialization) moment, then you can use it's 'setElement' method, to attach your view to specified dom element. Here is description.
Be the way your view render method will not work also without specified 'el'.
So I have two templates, one is the parent template, and the other is a template that has the subviews in it. My first parent view extends backbone's view and its render method looks like
render: function () {
this.$el.append(templates["template-parent-test"](this.model));
return this;
}
This parent view has a button on it. The button right now I use to populate my view. It basically does this:
populateView: function () {
// create some dummy test data to match the web service
_.each(myModel, function (theModel) {
var testView = new childView({ model: theModel });
this.$('div-in-parent-view').append(testView.render().$el);
});
}
This backbone view extends the Backbone.View and its render method looks like:
render: function () {
console.log("rendering child view");
this.$el.html(this.template({ data: this.model.toJSON() }));
return this;
}
So this works. But I don't want to populate the parent view on a button press. I want to populate it when I show it for the first time and have the button do what its actually supposed to do. I would think that I could just call this.populateView() from my render function in the parent view, but nothing actually gets rendered. But if I do this.populateView() in my button event, it gets rendered. Why is there a difference here? Thanks.
Try adding this as a third parameter to each. That sets the context. When you don't specify the context, this will be window. And since your html is not yet on the page during the parent view's render, it wont find this.$('#div-in-parent-view') in order to add the html to it.
I need to run a layout script as soon as my views are inserted into the DOM. So...
$(".widgets").append(widgets.render().el)
$(".widgets .dashboard").isotope # <-- This needs to be called whenever new widgets are inserted
The problem is I have to insert new widgets a few different views and re-call this script a few different places, which is not DRY. I am wondering how I can define the isotope in the View class.
Would it be a good idea to define an event listener to watch for append into the ".widgets" and to run the script? Is there a built in way of building views that are smart about when they are added to the DOM?
(For that matter, it would be also useful to define a callback for when a View is removed from the DOM.)
How about calling the isotope each time the view is rendered? You'll need to be careful to call render() only after the widget is injected, but this ought to take care of your problem:
//in Backbone.view.extend({
initialize: function() {
// fix context for `this`
_.bindAll(this);
},
render: function() {
// .. do rendering..
this.isotope();
return this;
}
// }) // end .extend
use:
var self = this;
this.$el.on('DOMNodeInserted', function(evt){
self.isotope();
$(evt.target ).stopPropagation();
})
In my view I don't declare this.el because I create it dinamically, but in this way the events don't fire.
This is the code:
View 1:
App.Views_1 = Backbone.View.extend({
el: '#content',
initialize: function() {
_.bindAll(this, 'render', 'renderSingle');
},
render: function() {
this.model.each(this.renderSingle);
},
renderSingle: function(model) {
this.tmpView = new App.Views_2({model: model});
$(this.el).append( this.tmpView.render().el );
}
});
View 2:
App.Views_2 = Backbone.View.extend({
initialize: function() {
_.bindAll(this, 'render');
},
render: function() {
this.el = $('#template').tmpl(this.model.attributes); // jQuery template
return this;
},
events: {
'click .button' : 'test'
},
test: function() {
alert('Fire');
}
});
});
When I click on ".button" nothing happens.
Thanks;
At the end of your render() method, you can tell backbone to rebind events using delegateEvents(). If you don't pass in any arguments, it will use your events hash.
render: function() {
this.el = $('#template').tmpl(this.model.attributes); // jQuery template
this.delegateEvents();
return this;
}
As of Backbone.js v0.9.0 (Jan. 30, 2012), there is the setElement method to switching a views element and manages the event delegation.
render: function() {
this.setElement($('#template').tmpl(this.model.attributes));
return this;
}
Backbone.View setElement: http://backbonejs.org/#View-setElement
setElementview.setElement(element)
If you'd like to apply a Backbone view to a different DOM element, use
setElement, which will also create the cached $el reference and move
the view's delegated events from the old element to the new one.
Dynamically creating your views in this fashion has it's pros and cons, though:
Pros:
All of your application's HTML markup would be generated in templates, because the Views root elements are all replaced by the markup returned from the rendering. This is actually kind of nice... no more looking for HTML inside of JS.
Nice separation of concerns. Templates generate 100% of HTML markup. Views only display states of that markup and respond to various events.
Having render be responsible for the creation of the entire view (including it's root element) is in line with the way that ReactJS renders components, so this could be a beneficial step in the process of migrating from Backbone.Views to ReactJS components.
Cons: - these are mostly negligible
This wouldn't be a painless transition to make on an existing code base. Views would need to be updated and all templates would need to have the View's root elements included in the markup.
Templates used by multiple views could get a little hairy - Would the root element be identical in all use cases?
Prior to render being called, the view's root element is useless. Any modifications to it will be thrown away.
This would include parent views setting classes/data on child view elements prior to rendering. It is also bad practice to do this, but it happens -- those modifications will be lost once render overrides the element.
Unless you override the Backbone.View constructor, every view will unnecessarily delegate it's events and set attributes to a root element that is replaced during rendering.
If one of your templates resolves to a list of elements rather than a single parent element containing children, you're going have a bad time. setElement will grab the first element from that list and throw away the rest.
Here's an example of that problem, though: http://jsfiddle.net/CoryDanielson/Lj3r85ew/
This problem could be mitigated via a build task that parses the templates and ensures they resolve to a single element, or by overriding setElement and ensuring that the incoming element.length === 1.