I'm trying to manually trigger a click event right after the html has been rendered but it's not working.
To simplify and verify that it's not working I tried this code:
var _testView = Backbone.View.extend({
events : {
'click a' : 'sayHi'
},
initialize : function() {
this.render();
this.$el.find('a').trigger('click');
},
render : function() {
$(document.body).html(
this.$el.html('alert hi')
);
},
sayHi : function() {
alert('Hi');
return false;
}
});
var y = new _testView;
I'm trying to manually trigger the click event but it's not being triggered. If I'm going to put the trigger in a setTimeout with a delay of 500 it will work. I don't know why.... thx
I found the answer. I looked at the Backbone core and I see that initialize method is being called first before attaching the events to the view.
View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this._configure(options || {});
this._ensureElement();
this.initialize.apply(this, arguments);
this.delegateEvents();
};
You are calling the click event on the element which haven't been yet created. You should call the function when the render is finished or you can just call this.sayHi() instead of triggering the click.
hjuster is correct. the $(document.body) wait for the 'document ready' event, and you are calling new _testView before the document is ready. You can change your intialize to this - it queues up the trigger to execute after the document is ready.
initialize : function() {
this.render();
var self = this;
$(function(){
self.$el.find('a').trigger('click');
});
},
I added the var 'self' since you can't reference 'this' to get your _testView object in the function.
It works in this fiddle
Related
The "change" event is not firing in the following code.
var PageView = Backbone.View.extend({
el: $("body"),
initialize: function(){
this.model.on("change:loading", this.loader, this);
},
loader: function(){
if(this.model.get("loading")){
this.$el.find('.loader').fadeIn(700);
}
else
this.$el.find('.loader').fadeOut(700);
},
});
var PageModel = Backbone.Model.extend({
defaults: {
loading: null,
},
initialize: function(){
this.set({loading:false});
},
});
$(function(){
var pageModel = new PageModel({});
var pageView = new PageView({model: pageModel});
})
It works if I'm adding this in the model's initialize function:
setTimeout(function() {
this.set({'loading': 'false'});
}, 0);
I can leave it this way, but this is a bug.
The situation explained
Here's the order the code runs:
the model is created,
model's initialize function is called, setting the loading attribute to false,
then the model is passed to the view,
then a listener is registered for the "change:loading"
The event handler is never called because the event never occurs after it was registered.
Quick fix
First remove the set from the model.
var PageModel = Backbone.Model.extend({
defaults: {
loading: null
}
});
Then, after creating the view, set the loading attribute.
var pageModel = new PageModel();
var pageView = new PageView({ model: pageModel });
pageModel.set('loading', false); // now the event should trigger
Since the listener is now registered before the model's loading attribute is changed, the event handler will be called.
Optimized solution
Use Backbone's best practices:
Favor .listenTo over .on to avoid memory leaks
Cache jQuery objects
Try to avoid setting the el property on the view
A view is an atomic component that should only care about itself and its sub-views.
While in your case, it wouldn't matter much that you use the el property on the view, it still goes beyond the responsibilities of the view. Let the calling code deal with passing the element to use for this view.
var PageView = Backbone.View.extend({
initialize: function() {
this.model = new PageModel();
this.$loader = this.$('.loader');
this.listenTo(this.model, "change:loading", this.loader);
},
loader: function() {
this.$loader[this.model.get("loading")? 'fadeIn': 'fadeOut'](700);
},
render: function() {
this.loader();
return this;
}
});
Put the defaults where they belong.
var PageModel = Backbone.Model.extend({
defaults: {
loading: false
}
});
Here we choose the body as the element to use for the view, using the el option, and then call render when ready.
$(function() {
var pageView = new PageView({ el: 'body' }).render();
});
The event won't be triggered by the listener right away, instead, we use the render function to put the view in its default state. Then, any subsequent changes of the loading attribute will trigger the callback.
I have listed the most useful answers I've written about Backbone on my profile page. You should take a look, it goes from the beginning to advanced and even provides some clever Backbone components that solves common problems (like detecting a click outside a view).
I'm having an issue when I am trying to switch the view after a model save on a click event.
The flow I am trying to create is a reorder process, the user will have a confirmation page to reorder. On clicking submit an api call will execute and the invoice page will load on success.
Currently when I click the submit button the first time nothing happens and when i click again I can get an invoice page. no such issue for the cancel button.
var confirmView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template( $("#confirmReorder_template").html());
this.$el.html(template);
},
events: {
"click #submitButton": "submitReorder",
"click #cancelButton": "cancelReorder"
},
submitReorder: function(event){
var URI='<config property="api.url.itemReorder"/>';
var ItemReorderModel = new itemReorderModel({url:URI});
$("#submitButton").click(function(event) {
event.preventDefault();
ItemReorderModel.set('id','1');
ItemReorderModel.save( {}, {
success : function() {
var response = ItemReorderModel.toJSON();
var InvoiceView = new invoiceView({el: $("#itemData")});
},
error : function(model, xhr, options) {
}
});
});
},
cancelReorder: function(event){
document.location.href = "items_list.ctl";
}
});
second view
var invoiceView = Backbone.View.extend({
initialize: function(){
this.render();
},
render: function(){
var template = _.template( $("#reorderInvoice_template").html());
this.$el.html(template);
},
events: {
"click #returnButton": "itemlist",
"click #printButton": "print"
},
itemlist: function(event){
document.location.href = "items_list.ctl";
},
print: function(event){
}
});
loading of first view
$(document).ready(function() {
var ConfirmView = new confirmView({el:$('#itemData')});
});
I'm new to backbone so not sure if I should be using a route, I also have read something about binding, but still trying to get my head around how it all works.
any advice is much appreciated.
You are binding a new event handler in submitReorder method, and your actual functionality is inside that event handler.
So the fist time you click the button, the event handler delegated toview via backbone event hash will trigger submitReorder, which binds a new event handler with actual functionality directly to the button element.
Next time when you click it, this new direct handler will also trigger and fire the functionality you expect.
Each time you click the button you're adding a new event handler.
Your code should be simply:
submitReorder: function(event){
event.preventDefault();
var URI='<config property="api.url.itemReorder"/>';
//-----^------ if this is hardcoded, why not specify this in the model itself..?
var ItemReorderModel = new itemReorderModel({url:URI});
//-------------^----------- why not do this just once while initializing view..?
ItemReorderModel.set('id','1');
//-------------^----------- if this is hardcoded, why not set specify it in model..?
ItemReorderModel.save( {}, {
success : function() {
var response = ItemReorderModel.toJSON();
var InvoiceView = new invoiceView({el: $("#itemData")});
},
error : function(model, xhr, options) {
}
});
},
I also suggest initializing the model in the view's initialize method and caching it as it's property rather than initializing a new model on every click.
I have a simple backbone view as follows:
/**
* Renders a form view for an event object.
*/
APP.EventFormView = Backbone.View.extend({
tagName: 'form',
events: {
'keydown': 'keyPressed',
'focus input': 'inputChanged',
'change select': 'selectChanged',
'change textarea': 'textareaChanged'
},
initialize: function() {
this.template = _.template($('#newevent-form').html());
this.listenTo(this.model, 'change', this.render);
this.listenTo(APP.eventTypes, 'update', this.render);
this.listenTo(APP.selectedEvent, 'update', this.render);
},
render: function() {
var modelJSON = this.model.toJSON();
if ('id' in modelJSON && modelJSON.id !== "") {
this.loadForm();
} else if (!('id' in modelJSON) || modelJSON.id === "") {
this.loadForm();
} else {
this.$el.html('');
}
return this;
},
loadForm: function() {
var templateData = $.extend(this.model.toJSON(),
{"event_types":APP.eventTypes.toJSON()});
this.$el.html('');
this.$el.html(this.template($.extend(this.model.toJSON(),
{event_types: APP.eventTypes.toJSON()})));
$('.ev-main-container').html('').html(this.el);
},
inputChanged: function(e) {
console.log('inputChanged');
},
selectChanged: function(e) {
console.log('selectChanged');
},
textareaChanged: function(e) {
console.log('textareaChanged');
},
keyPressed: function(e) {
console.log('key pressed');
}
});
I initialize this view as follows under document.ready:
// Initialize the form view
APP.selectedEvent = APP.selectedEvent || new APP.Event();
APP.eventFormView = new APP.EventFormView({model: APP.selectedEvent});
APP.eventFormView.render();
But none of the events I have defined are firing for some reason, What is it that I am doing wrong here ?
Update:
Ok, I fugred out if i remove $('.ev-main-container').html('').html(this.el); from the loadForm method and instead intialize the view as follows, it works:
APP.eventFormView = new APP.EventFormView({
model: APP.selectedEvent,
el: $('.ev-main-container'),
});
I was able to resolve it but I still don't understand why this happens, could anyone throw a little light on what's going on and how this works.
jQuery's html function has a side effect that many people seem to forget about, from the fine manual:
jQuery removes other constructs such as data and event handlers from child elements before replacing those elements with the new content.
Consider what that means when you do something like this:
container.html(view.el);
container.html(view.el);
Everything will be fine after the first container.html() call. But the second will "remove ... event handlers from child elements" (such as view.el) before adding the new content. So after the second container.html() call, all the events on view.el are gone. Sound familiar?
You have lots of things that will call render on your view and render will eventually do this:
$('.ev-main-container').html('').html(this.el);
Your events will silently disappear the second time that gets called but the HTML will look just fine.
Consider this simplified example (http://jsfiddle.net/ambiguous/otnyv93e/):
var V = Backbone.View.extend({
tagName: 'form',
events: {
'click button': 'clicked'
},
initialize: function() {
this.template = _.template($('#t').html());
},
render: function() {
this.$el.html('');
this.$el.html(this.template());
$('.ev-main-container').html('').html(this.el);
return this;
},
clicked: function() {
console.log('clicked');
}
});
var v = new V;
v.render();
$('#re-render').click(function() {
v.render();
console.log('Re-rendered');
});
and you'll see exactly your problem.
If you make the view's el the .ev-main-container then you'll be using html() to alter the contents of el rather than altering the contents of the element that contains el. Once you're working entirely inside the el you're no longer accidentally re-using an element and no longer accidentally removing the event bindings from that element.
My rules of thumb for preventing event problems with Backbone:
Never attach views to existing DOM nodes, always let views create and own their own el and let the caller put that el in a container.
Call remove on views to dispose of them when they're no longer needed.
Don't try to re-use views, create them when you need them and remove them when you don't need them.
No view references anything outside its el.
There are exceptions (of course) and this approach won't solve everything but it is a good starting point and avoids most of the common problems.
I am using backbone.js to create a View which contains a Like button. The model of this View contains the attribute is_liked, and if its value is 1, then the function setStateLike called will change the style of the Like button.
Problem: I am not able to select the button using this.setStateLike() in the initialize function. Doing so just returns a []. However, when I define this.setStateLike as a click event handler, selecting the button works! The stranger thing is that this.setStateLike() called within initialize is able to select $(this.el), but not $(this.el).find()!
Any idea what has happened here and how can it be fixed? Thanks!
PhotoListItemView = Backbone.View.extend({
tagName: 'div',
className: 'photo_box',
events: {
'click': 'setStateLike'
},
initialize: function() {
this.setStateLike();
},
render: function() {
$(this.el).html( this.template( this.model.toJSON() ) );
return this;
},
setStateLike: function() {
console.log( $(this.el).find('#like') ); // returns []
if(this.model.get('is_liked')) {
console.log( $(this.el) ); // returns correctly
console.log( $(this.el).find('#like') ); // returns []
// Change icon to Active state
$(this.el).find('#like.photo_btn').addClass('photo_btn_active').attr('id', 'unlike');
}
}
});
If your script comes before the bulk of the body HTML and if the initialize function is being called immediately, that's your issue: the DOM isn't actually built yet, so no elements can be selected. Either run the script at the end of the </body>, or use jQuery's DOM-ready handler.
The 2nd answer to this question nicely explains how event declarations in Backbone.js views are scoped to the view's el element.
It seems like a reasonable use case to want to bind an event to an element outside the scope of el, e.g. a button on a different part of the page.
What is the best way of achieving this?
there is not really a reason you would want to bind to an element outside the view,
there are other methods for that.
that element is most likely in it's own view, (if not, think about giving it a view!)
since it is in it's own view, why don't you just do the binding there, and in the callback Function,
use .trigger(); to trigger an event.
subscribe to that event in your current view, and fire the right code when the event is triggered.
take a look at this example in JSFiddle, http://jsfiddle.net/xsvUJ/2/
this is the code used:
var app = {views: {}};
app.user = Backbone.Model.extend({
defaults: { name: 'Sander' },
promptName: function(){
var newname = prompt("Please may i have your name?:");
this.set({name: newname});
}
});
app.views.user = Backbone.View.extend({
el: '#user',
initialize: function(){
_.bindAll(this, "render", "myEventCatcher", "updateName");
this.model.bind("myEvent", this.myEventCatcher);
this.model.bind("change:name", this.updateName);
this.el = $(this.el);
},
render: function () {
$('h1',this.el).html('Welcome,<span class="name"> </span>');
return this;
},
updateName: function() {
var newname = this.model.get('name');
console.log(this.el, newname);
$('span.name', this.el).text(newname);
},
myEventCatcher: function(e) {
// event is caught, now do something... lets ask the user for it's name and add it in the view...
var color = this.el.hasClass('eventHappened') ? 'black' : 'red';
alert('directly subscribed to a custom event ... changing background color to ' + color);
this.el.toggleClass('eventHappened');
}
});
app.views.sidebar = Backbone.View.extend({
el: '#sidebar',
events: {
"click #fireEvent" : "myClickHandler"
},
initialize: function(){
_.bindAll(this, "myClickHandler");
},
myClickHandler: function(e) {
window.user.trigger("myEvent");
window.user.promptName();
}
});
$(function(){
window.user = new app.user({name: "sander houttekier"});
var userView = new app.views.user({model: window.user}).render();
var sidebarView = new app.views.sidebar({});
});
Update: This answer is no longer valid/right. Please see other answers below!
Why do you want to do this?
Apart from that, you could always just bind it using regular jQuery handlers. E.g.
$("#outside-element").click(this.myViewFunction);
IIRC, Backbone.js just uses the regular jQuery handlers, so you're essentially doing the same thing, but breaking the scope :)