I'm kind of new to backbone and have a question. It seems like when an event in my view is fired, I lose the context to "this". How can I preserve this or get the original "this" for the model. Here is an example:
var MyListView = MyDBView.extend({
initialize: function(options){
},
render: function() {
//stuff here. I can access this.options here
},
dialogResponseYes: function(e){
//try to get this.options and options is undefined as this has changed to another element (a button)
}
});
So, how do I get the original context of this?
if you are using events object to bind events to your view like here http://backbonejs.org/#View-delegateEvents everything should be ok, otherwise u can hardly bind them to your view using _.bindAll
initialize: function(options){
_.bindAll(this, "dialogResponseYes");
},
or
var MyListView = MyDBView.extend({
initialize: function(options){
},
events: {
'click div': 'dialogResponseYes' //example
},
render: function() {
},
dialogResponseYes: function(e){
}
});
Related
I have a code like this in JavaScript:
var addModalView = Backbone.View.extend({
tagName:"div",
el:$("#addemployee"),
showbutton:$("#addemployee_button"),
showbutton_click:function(e) {
this.$el.modal("show"); // this.$el is undefined and not working
},
initialize:function() {
this.showbutton.on("click", this.showbutton_click);
this.$el.modal("show"); // this.$el is defined and working
}
});
myaddModalView = new addModalView();
Why is this.$el defined and working on initialize but not on other key index (showbutton_click)?
The proper implementation should be like this using the events hash.
var addModalView = Backbone.View.extend({
el: $("#addemployee"), // should be avoided if possible
events: {
'click #addemployee_button': 'showbutton_click'
},
initialize: function() {
this.$el.modal("show");
},
showbutton_click: function(e) {
this.$el.modal("show");
},
});
myaddModalView = new addModalView();
If for some reason #addemployee_button is not inside "#addemployee", then the event binding should happen in whichever view actually contains it.
I already solved the problem all i need is to bind the backbone object on initialize.
var addModalView = Backbone.View.extend({
tagName:"div",
el:$("#addemployee"),
showbutton:$("#addemployee_button"),
showbutton_click:function(e) {
this.$el.modal("show"); // this.$el is now defined and working
},
initialize:function() {
this.showbutton.on("click", this.showbutton_click);
_.bindAll(this, "showbutton_click"); // bind it first on showbutton_click
this.$el.modal("show"); // this.$el is defined and working
}
});
myaddModalView = new addModalView();
This bind code is the solution and should be added on initialize: _.bindAll(this, "showbutton_click"); so you can call the backbone object inside your custom function variables using the this keyword.
I have the following structure:
var PopupView = Backbone.View.extend({
el:'#shade',
events:{
'click .popup-cancel': 'hide',
'click .popup-commit': 'commit',
},
show: function(){
...
},
hide: function(){
...
},
initialize: function(){
_.bindAll(this, 'show','hide','commit');
}
});
var Popup1 = PopupView.extend({
render: function(){
...
},
commit: function(){
console.log('1');
...
},
});
var Popup2 = PopupView.extend({
render: function(){
...
},
commit: function(){
console.log('2');
...
},
});
The problem is that when I click .popup-commit from one of the popups, it actually triggers the methods of both of them. I've tried moving the declaration of events and initialize() up into the child classes, but that doesn't work.
What's going on, and how can I fix it (so that the commit method of only the view I'm triggering it on gets fired)?
Your problem is right here:
el:'#shade'
in your PopupView definition. That means that every single instance of PopupView or its subclasses (except of course those that provide their own el) will be bound to the same DOM node and they will all be listening to events on on id="shade" element.
You need to give each view its own el. I'd recommend against ever setting el in a view definition like that. I think you'll have a better time if you let each view create (and destroy) its own el. If you do something like:
var PopupView = Backbone.View.extend({
className: 'whatever-css-class-you-need',
tagName: 'div', // or whatever you're using to hold your popups.
attributes: { /* whatever extra attributes you need on your el */ },
//...
});
then your views will each get their own el. See the Backbone.View documentation for more information on these properties.
I'm working on my first app using bbjs, after 10 tutorials and endless sources I am trying to come up with my code design.
I ask what is the best practice with views and templates. Also there is an events problem I am struggling with.
As I understand, the view is to be responsible for one element and its contents (and other sub-views).
For the code to be manageable, testable, etc.. the element/template is to be passed to the view on creation.
In my app Imho the view should hold the templates, because the visible element has many "states" and a different template for each state.
When the state changes, I guess its best to create a new view, but, is it possible for the view to update itself with new element?
App.Box = Backbone.Model.extend({
defaults: function() {
return {
media: "http://placehold.it/200x100",
text: "empty...",
type: "type1"
};
}
});
App.BoxView = Backbone.View.extend({
template: {},
templates: {
"type1": template('appboxtype1'),
"type2": template('appboxtype2')
},
events: {
'click .button': 'delete'
},
initialize: function(options) {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
this.render();
},
render: function() {
this.template = this.templates[ this.model.get("type") ];
// first method
this.$el.replaceWith( $($.parseHTML(this.template(this))) );
this.$el.attr("id", this.model.cid);
// second method
var $t_el = this.$el;
this.setElement( $($.parseHTML(this.template(this))) );
this.$el.attr("id", this.model.cid);
$t_el.replaceWith( this.$el );
this.delegateEvents();
//$('#'+this.model.cid).replaceWith( $(g.test.trim()) );
//! on the second render the events are no longer bind, deligateEvents doesn't help
return this;
},
// get values
text: function() { return this.model.get('text'); },
media: function() { return this.model.get('media'); },
delete: function() {
this.model.destroy();
}
});
Thanx! :)
Instead of trying to replace the view's root element ($el), just replace its content.
this.$el.html(this.template(this));
Events should still work then.
try this
render: function() {
html = '<div>your new html</div>';
var el = $(html);
this.$el.replaceWith(el);
this.setElement(el);
return this;
}
$.replaceWith will only replace the element in the DOM. But the this.$el still holds a reference to the now displaced old element. You need to call this.setElement(..) to update the this.$el field. Calling setElement will also undelegateEvents and delegateEvents events for you.
I came up with this solution: http://jsfiddle.net/Antonimo/vrQzF/4/
if anyone has a better idea its always welcome!
basically, in view:
var t_$el = this.$el;
this.$el = $($.parseHTML(this.template(this)));
this.$el.attr("id", this.cid);
if (t_$el.parent().length !== 0) { // if in dom
this.undelegateEvents();
t_$el.each(function(index, el){ // clean up
if( index !== 0 ){ $(el).remove(); }
});
t_$el.first().replaceWith(this.$el);
this.delegateEvents();
}
I am working on a nested backbone view, in which if you click on, it will create a new instance of the same view. I want to disable only a specific event, not all of them; in this case, the click. I tried using undelegateEvents(), but this will disable all the functions. Any ideas on how can this be done?
Here is a piece of the code I am working on:
var View = Backbone.View.extend({
events: {
"mousedown": "start",
"mouseup": "over"
},
start: function() {
var model = this.model;
var v = new View({
model: model,
});
v.undelegateEvents(); //I just want to disable mousedown
v.render();
},
over: function() {
/*
some code here
*/
},
render: function() {
/*
some code here
*/
}
});
The idea is to ban clicking in the second instantiated view while keeping the other events. The first one will have all of its events.
Thanks
You can specify the events you want to use when you call delegateEvents:
delegateEvents delegateEvents([events])
Uses jQuery's delegate function to provide declarative callbacks for DOM events within a view. If an events hash is not passed directly, uses this.events as the source.
So you could do something like this:
var v = new View({
model: model,
});
v.undelegateEvents();
var e = _.clone(v.events);
delete e.mousedown;
v.delegateEvents(e);
v.render();
You might want to push that logic into a method on View though:
detach_mousedown: function() {
this.undelegateEvents();
this.events = _.clone(this.events);
delete this.events.mousedown;
this.delegateEvents();
}
//...
v.detach_mousedown();
You need the this.events = _.clone(this.events) trickery to avoid accidentally altering the "class's" events (i.e. this.constructor.prototype.events) when you only want to change it for just one object. You could also have a flag for the View constructor that would do similar things inside its initialize:
initialize: function() {
if(this.options.no_mousedown)
this.detach_mousedown()
//...
}
Another option would be to have a base view without the mousedown handler and then extend that to a view that does have the mousedown handler:
var B = Backbone.View.extend({
events: {
"mouseup": "over"
},
//...
});
var V = B.extend({
events: {
"mousedown": "start",
"mouseup": "over"
},
start: function() { /* ... */ }
//...
});
You'd have to duplicate the B.events inside V or mess around with a manual extend on the events as _.extend won't merge the properties, it just replaces things wholesale.
Here is a simple example that shows how to delegate or undelegate events within a Backbone view
Backbone.View.extend({
el: $("#some_element"),
// delete or attach these as necessary
events: {
mousedown: "mouse_down",
mousemove: "mouse_move",
mouseup: "mouse_up",
},
// see call below
detach_event: function(e_name) {
delete this.events[e_name]
this.delegateEvents()
},
initialize: function() {
this.detach_event("mousemove")
},
mouse_down: function(e) {
this.events.mousemove = "mouse_move"
this.delegateEvents()
},
mouse_move: function(e) {},
mouse_up: function(e) {}
})
I have a view which doesn't seem to want to render as the model's change event is not firing.
here's my model:
var LanguagePanelModel = Backbone.Model.extend({
name: "langpanel",
url: "/setlocale",
initialize: function(){
console.log("langselect initing")
}
})
here's my view:
var LanguagePanelView = Backbone.View.extend({
tagName: "div",
className: "langselect",
render: function(){
this.el.innerHTML = this.model.get("content");
console.log("render",this.model.get(0))
return this;
},
initialize : function(options) {
console.log("initializing",this.model)
_.bindAll(this, "render");
this.model.bind('change', this.render);
this.model.fetch(this.model.url);
}
});
here's how I instantiate them:
if(some stuff here)
{
lsm = new LanguagePanelModel();
lsv = new LanguagePanelView({model:lsm});
}
I get logs for the init but not for the render of the view?
Any ideas?
I guess it's about setting the attributes of the model - name is not a standard attribute and the way you've defined it, it seems to be accessible directly by using model.name and backbone doesn't allow that AFAIK. Here are the changes that work :) You can see the associated fiddle with it too :)
$(document).ready(function(){
var LanguagePanelModel = Backbone.Model.extend({
//adding custom attributes to defaults (with default values)
defaults: {
name: "langpanel",
content: "Some test content" //just 'cause there wasn't anything to fetch from the server
},
url: "/setlocale",
initialize: function(){
console.log("langselect initing"); //does get logged
}
});
var LanguagePanelView = Backbone.View.extend({
el: $('#somediv'), //added here directly so that content can be seen in the actual div
initialize : function(options) {
console.log("initializing",this.model);
_.bindAll(this, "render");
this.render(); //calling directly since 'change' won't be triggered
this.model.bind('change', this.render);
//this.model.fetch(this.model.url);
},
render: function(){
var c = this.model.get("content");
alert(c);
$(this.el).html(c); //for UI visibility
console.log("render",this.model.get(0)); //does get logged :)
return this;
}
});
var lpm = new LanguagePanelModel();
var lpv = new LanguagePanelView({model:lpm});
}); //end ready
UPDATE:
You don't need to manually trigger the change event - think of it as bad practice. Here's what the backbone documentation says (note: fetch also triggers change!)
Fetch
model.fetch([options])
Resets the model's state from the server.
Useful if the model has never been populated with data, or if you'd
like to ensure that you have the latest server state. A "change" event
will be triggered if the server's state differs from the current
attributes. Accepts success and error callbacks in the options hash,
which are passed (model, response) as arguments.
So, if the value fetched from the server is different from the defaults the change event will be fired so you needn't do it yourself. If you really wish to have such an event then you can use the trigger approach but custom name it since it's specific to your application. You are basically trying to overload the event so to speak. Totally fine, but just a thought.
Change
model.change()
Manually trigger the "change" event. If you've been
passing {silent: true} to the set function in order to aggregate rapid
changes to a model, you'll want to call model.change() when you're all
finished.
The change event is to be manually triggered only if you've been suppressing the event by passing silent:true as an argument to the set method of the model.
You may also want to look at 'has changed' and other events from the backbone doc.
EDIT Forgot to add the updated fiddle for the above example - you can see that the alert box pops up twice when the model is changed by explicitly calling set - the same would happen on fetching too. And hence the comment on the fact that you "may not" need to trigger 'change' manually as you are doing :)
The issue was resolved my adding
var LanguagePanelModel = Backbone.Model.extend({
//adding custom attributes to defaults (with default values)
defaults: {
name: "langpanel",
content: "no content",
rawdata: "no data"
},
events:{
//"refresh" : "parse"
},
url: "/setlocale",
initialize: function(){
log("langselect initing");
//this.fetch()
},
parse: function(response) {
this.rawdata = response;
// ... do some stuff
this.trigger('change',this) //<-- this is what was necessary
}
})
You don't need attributes to be predefined unlike PhD suggested. You need to pass the context to 'bind' - this.model.bind('change', this.render, this);
See working fiddle at http://jsfiddle.net/7LzTt/ or code below:
$(document).ready(function(){
var LanguagePanelModel = Backbone.Model.extend({
url: "/setlocale",
initialize: function(){
console.log("langselect initing");
}
});
var LanguagePanelView = Backbone.View.extend({
el: $('#somediv'),
initialize : function(options) {
console.log("initializing",this.model);
// _.bindAll(this, "render");
//this.render();
this.model.bind('change', this.render, this);
//this.model.fetch(this.model.url);
},
render: function(){
var c = this.model.get("content");
alert(c);
$(this.el).html(c);
console.log("render",this.model.get(0));
return this;
}
});
var lpm = new LanguagePanelModel();
var lpv = new LanguagePanelView({model:lpm});
lpm.set({content:"hello"});
}); //end ready