Backbone listenTo and 'this' context - backbone.js

I have a Backbone view where I listen for an event 'mouse:down' triggered by the view's canvas variable, which holds a fabric Canvas object. The event triggers the function 'myFunction', and inside the function I need to use 'this' to reference the view instance. See code below:
define([
'underscore',
'backbone',
'mustache',
'fabric'
], function(_, Backbone, Mustache, fabric) {
var MyView = Backbone.View.extend({
template: '<canvas id="my-canvas"></canvas>',
tagName: 'div',
id: 'my-id',
initialize: function () {
Mustache.parse(this.template);
},
render: function () {
this.$el.html(Mustache.render(this.template));
this.initCanvas('my-canvas');
return this;
},
initCanvas: function (canvasId) {
var canvas = new fabric.Canvas(canvasId, {
hoverCursor: 'pointer',
selection: false,
allowTouchScrolling: true,
width: 1170,
height: 658
});
fabric.Image.fromURL('myimage.jpg', function (img) {
canvas.setBackgroundImage(img);
canvas.renderAll();
});
this.canvas = canvas;
this.initCanvasListeners();
},
initCanvasListeners: function () {
this.listenTo(this.canvas, 'mouse:down', this.myFunction);
},
myFunction: function (options) {
console.log(this); // Outputs the canvas object, not MyView
}
});
return MyView;
});
'myFunction' is triggered, but now 'this' references the canvas object, not the view instance. How can I fix this? I need to call other functions of the view from 'myFunction', but I am quite stuck right now...
I have also unsuccessfully tried to change my event listener to look like this:
this.canvas.on('mouse:down', this.myFunction, this);

When setting up the event, bind it like this:
object.on(event, callback, [context])
Set context to the value object you want to be the "this" on the callback.
See: http://backbonejs.org/#Events-on

Thank you, nimgrg, that worked with a tiny modification:
this.canvas.on('mouse:down', this.myFunction.bind(this));

Related

How can I reference the backbone view from a kendo ui treeview event handler?

I'm using the Kendo UI treeview with Backbone.js along with Marionette.js The treeview is inside a view component and works well execpt for one area. To start, I initialize the treeview when I call the view render method
View.myPanel = Marionette.ItemView.extend(
render: function () {
this.treeview = this.$el.find("#treeview").kendoTreeView({
dataSource: this.hierarchicalDataSource,
dataTextField: ["item"],
dragAndDrop: true,
loadOnDemand: false,
drop: this.onDrop
}).data("kendoTreeView"),
this.treeview.expand(".k-item");
},
onDrop: function (e) {
...
code to create model goes here ......
...
this.saveItem(localModel, false);
}
}
The problem I have is that when I try and call this.saveItem, I have no reference to "this". . Normally "this" would be the view itself.
Instead "this" refers to the treeview object. I see the event object inside the drop handler but no reference to the view.
I tried to extend the treeview with BackBone.Events but that causes me to lose the drag and drop functionality. I also tried passing the view object as a parameter to the drop handler but that replaces the event parameter in the onDrop function.
Underscore's bind and bindAll methods may hep you.
View.myPanel = Marionette.ItemView.extend(
initialize: function() {
_.bindAll(this,'onDrop');
},
render: function () {
this.treeview = this.$el.find("#treeview").kendoTreeView({
dataSource: this.hierarchicalDataSource,
dataTextField: ["item"],
dragAndDrop: true,
loadOnDemand: false,
drop: this.onDrop
}).data("kendoTreeView"),
this.treeview.expand(".k-item");
},
onDrop: function (e) {
...
code to create model goes here ......
...
this.saveItem(localModel, false);
}
}
To know more see _.bind
Here is sample EXAMPLE posted it there on underscore site
var buttonView = {
label : 'underscore',
onClick: function(){ alert('clicked: ' + this.label); },
onHover: function(){ console.log('hovering: ' + this.label); }
};
_.bindAll(buttonView, 'onClick', 'onHover');
// When the button is clicked, this.label will have the correct value.
jQuery('#underscore_button').bind('click', buttonView.onClick);
Here this in callback function buttonView.onClick points correctly to buttonViewand not to the bound DOM element (which is usual).
I know very little about backbone or marionette, but it seems like you can use the ItemView variable you created:
View.myPanel.saveItem(localModel, false);
Update
Have you tried a self-executing function that passes the View object in as a local variable and returns the ItemView object:
View.myPanel = (function(view) {
return Marionette.ItemView.extend(
render: function () {
this.treeview = this.$el.find("#treeview").kendoTreeView({
dataSource: this.hierarchicalDataSource,
dataTextField: ["item"],
dragAndDrop: true,
loadOnDemand: false,
drop: this.onDrop
}).data("kendoTreeView"),
this.treeview.expand(".k-item");
},
onDrop: function (e) {
...
code to create model goes here ......
...
view.myPanel.saveItem(localModel, false);
}
);
})(View);
Kendo UI explicitly sets the context for event handlers to the widget that triggers the event; use a closure to keep access to your view:
render: function () {
var that = this;
this.treeview = this.$el.find("#treeview").kendoTreeView({
dataSource: this.hierarchicalDataSource,
dataTextField: ["item"],
dragAndDrop: true,
loadOnDemand: false,
drop: function (e) {
that.onDrop(e)
}
}).data("kendoTreeView"),
this.treeview.expand(".k-item");
}

Backbone.js model trigger change only if model.on called again

It's my first post here so please be nice ;) I'm trying to create a Backbone+requireJS sample app and I have stumbled upon a strange problem. In my views initialize function I bind to models change event by
this.model.on("change", this.change());
Event is triggered correctly when data is loaded and all is fine. In my view I also bind to a click event. On click I try to change one of the properties and I was hoping for a change event, but it never came.
I was trying some stuff recommended here on stackoverflow (here, here, and there) but with no luck.
In my guts I feel it has to do with event binding. When I tried to bind again to models on change event inside click callback it started to work. So now I sit here and I'm, hm, confused. Could someone shed some light on this issue?
My View:
define(
[
'jquery',
'underscore',
'backbone',
'text!templates/news/newsListItem.html'
],
function($, _, Backbone, newsListItemTemplate)
{
var NewsListItemViewModel = Backbone.View.extend({
tagName: 'li',
events: {
"click a" : "onLinkClick"
},
initialize: function(){
this.model.on("change", this.changed());
},
render: function()
{
this.$el.html(_.template(newsListItemTemplate, this.model.toJSON()));
},
changed: function()
{
this.render();
console.log("changed");
},
//GUI functions
onLinkClick: function(e)
{
console.log("click!");
this.model.toggle();
this.model.on("change", this.changed());
}
});
var init = function(config){
return new NewsListItemViewModel(config);
}
return {
init : init
}
}
);
My Model:
define(
['jquery', 'underscore', 'backbone'],
function($, _, Backbone){
var NewsListItemModel = Backbone.Model.extend({
toggle: function() {
this.set('done', !this.get('done'));
this.trigger('change', this);
}
});
var init = function(data)
{
return new NewsListItemModel(data);
}
return {
init: init,
getClass: NewsListItemModel
};
}
);
Thanks for any input :)
First, you should use a function as event handler - not its result.
Hence, change the line into this:
this.model.on("change", this.changed.bind(this));
As it stands now, you actually fire this.changed() function just once - and assign its result (which is undefined, as the function doesn't have return statement) to be the model's change event handler.
Second, you shouldn't rebind this handler in onLinkClick: once bound, it'll stay here. It looks like it's more appropriate to trigger this event instead:
onLinkClick: function(e)
{
console.log("click!");
this.$el.toggle();
this.model.trigger('change');
}

How to properly use Marionette layouts?

my current code looks like this:
define([
'jquery',
'underscore',
'backbone',
'marionette',
'templates',
'gridView',
'detailView',
'detailModel'
], function ($, _, Backbone, Marionette, JST, GridView, DetailView, DetailModel) {
'use strict';
return Marionette.Layout.extend({
el: '#main',
template: JST['app/scripts/templates/main.ejs'],
initialize: function() {
this.render();
},
onRender: function () {
var Layout = Marionette.Layout.extend({
el: 'div',
template: _.template(""),
regions: {
grid: '#grid',
detail: '#detail'
}
});
this.layout = new Layout();
this.layout.render();
},
showGrid: function () {
var detailModel = new DetailModel();
var g = new GridView(detailModel);
var d = new DetailView(detailModel);
this.layout.grid.show(g);
this.layout.detail.show(d);
}
});
});
What I do not understand is why I need an extra layout in my onRender method to make this work. The '#grid' and '#detail' divs are part of the main.ejs template, but the following does not work:
return Marionette.Layout.extend({
el: '#main',
template: JST['app/scripts/templates/main.ejs'],
regions: {
grid: '#grid',
detail: '#detail'
},
initialize: function() {
this.render();
},
onRender: function () {
var detailModel = new DetailModel();
var g = new GridView(detailModel);
var d = new DetailView(detailModel);
this.grid.show(g);
this.detail.show(d);
}
});
It seems that the layout only works if the elements specified in the region object already exist when the layout is created. But the documentation says that this is not the case.
I'm probably doing something wrong. But what ?
Regards
Roger
In your second code example, try using onShow instead of onRender.
In addition, in Marionette you usually don't call render yourself, since the framework will call that method when you pass view/layouts to the show method.
You can see a different take on what you're trying to accomplish here :
https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/apps/contacts/list/list_controller.js (particularly lines 43-46)
As an additional warning, calling .show() in the onRender method can negatively impact anything nested below that layout, especially if you are trying to use onShow later down the line to ensure that a view's DOM subtree is jQuery accessible.
.show() triggers a "show" event across any subviews of that layout and can mean that onShow() is called in those subviews (which listen for the "show" event) before those subviews have rendered and inserted their content.

Event handling between views

Ok I have a layout like the one in this pic:
The table in the upper part of the screen is made by:
MessageListView
define(['backbone','collections/messages','views/message'], function(Backbone, MessageCollection, MessageView) {
var MessageListView = Backbone.View.extend({
el: '#messagesContainer',
initialize: function() {
this.collection = new MessageCollection();
this.collection.fetch({reset:true});
this.listenTo( this.collection, 'reset', this.render );
this.table = this.$el.find("table tbody");
this.render();
},
render: function() {
this.collection.each( function(message, index) {
this.renderMessage(message, index);
}, this);
},
renderMessage: function(message, index) {
var view = new MessageView({
model:message,
className: (index % 2 == 0) ? "even" : "odd"
});
this.table.append( view.render().el );
}
});
return MessageListView;
});
MessageView
define(['backbone','models/message'], function(Backbone, MessageCollection, MessageView) {
var MessageView = Backbone.View.extend({
template: _.template( $("#messageTemplate").html() ),
render: function() {
this.setElement( this.template(this.model.toJSON()) );
return this;
},
events:{
'click':'select'
},
select: function() {
// WHAT TO DO HERE?
}
});
return MessageView;
});
AppView
define(['backbone','views/messages'], function(Backbone, MessageList) {
var App = Backbone.View.extend({
initialize: function() {
new MessageList();
}
});
return App;
});
I will soon add a new view (maybe "PreviewView") in the lower part of the screen.
I want to make something happen inside the "PreviewView" when user clicks a row.
For example, it could be interesting to display other model's attributes (details, e.g.) inside the PreviewView.
What is the best practice?
holding a reference to PreviewView inside each MessageView ?
triggering events inside select method, and listening to them using on() inside the preview view.
using a transient "selected" attribute in my model, and make PreviewView listen to collection "change" events?
Thank you, if you need more details tell me please, I'll edit the question.
Not sure about the best practice but I found this solution trivial to implement. I created a global messaging object, bus, whatever:
window.App = {};
window.App.vent = _.extend({}, Backbone.Events);
You have to register the "triggerable" functions of PreviewView on the previously created event bus (according to your example, this should be in the PreviewView):
initialize: function () {
App.vent.on('PreviewView.show', this.show, this);
}
Now you should be able to trigger any of registered events from anywhere within your application by calling: App.vent.trigger. For example when the user click on a row you will have something similar:
App.vent.trigger('PreviewView.show');
in case if you have to send and object along with the triggered event use:
App.vent.trigger('PreviewView.show', data);

Backbone.js event after view.render() is finished

Does anyone know which event is fired after a view is rendered in backbone.js?
I ran into this post which seems interesting
var myView = Backbone.View.extend({
initialize: function(options) {
_.bindAll(this, 'beforeRender', 'render', 'afterRender');
var _this = this;
this.render = _.wrap(this.render, function(render) {
_this.beforeRender();
render();
_this.afterRender();
return _this;
});
},
beforeRender: function() {
console.log('beforeRender');
},
render: function() {
return this;
},
afterRender: function() {
console.log('afterRender');
}
});
Or you can do the following, which is what Backbone code is supposed to look like (Observer pattern, aka pub/sub). This is the way to go:
var myView = Backbone.View.extend({
initialize: function() {
this.on('render', this.afterRender);
this.render();
},
render: function () {
this.trigger('render');
},
afterRender: function () {
}
});
Edit: this.on('render', 'afterRender'); will not work - because Backbone.Events.on accepts only functions. The .on('event', 'methodName'); magic is made possible by Backbone.View.delegateEvents and as such is only available with DOM events.
As far as I know - none is fired. Render function is empty in source code.
The default implementation of render is a no-op
I would recommend just triggering it manually when necessary.
If you happen to be using Marionette, Marionette adds show and render events on views. See this StackOverflow question for an example.
On a side note, Marionette adds a lot of other useful features that you might be interested in.
I realise this question is fairly old but I wanted a solution that allowed the same custom function to be called after every call to render, so came up with the following...
First, override the default Backbone render function:
var render = Backbone.View.prototype.render;
Backbone.View.prototype.render = function() {
this.customRender();
afterPageRender();
render();
};
The above code calls customRender on the view, then a generic custom function (afterPageRender), then the original Backbone render function.
Then in my views, I replaced all instances of render functions with customRender:
initialize: function() {
this.listenTo(this.model, 'sync', this.render);
this.model.fetch();
},
customRender: function() {
// ... do what you usually do in render()
}
Instead of adding the eventhandler manually to render on intialization you can also add the event to the 'events' section of your view. See http://backbonejs.org/#View-delegateEvents
e.g.
events: {
'render': 'afterRender'
}
afterRender: function(e){
alert("render complete")
},
constructor: function(){
Backbone.View.call(this, arguments);
var oldRender = this.render
this.render = function(){
oldRender.call(this)
// this.model.trigger('xxxxxxxxx')
}
}
like this http://jsfiddle.net/8hQyB/

Resources