I am looking for a better way to implement a PubSub type functionality with Backbone. I currently am achieving it this, but am looking for a better way to go about it.
Sample Routing Code
var AppRouter = Backbone.Router.extend({
customEvents: _.extend({}, Backbone.Events),
routes: {
"login": "login"
},
//Injecting custom event
login: function(){
this.before(function () {
app.showView('#Content', new LoginView({customEvents:this.customEvents}), "login", true);
});
},
//Injecting custom event
otherView: function(){
this.before(function () {
app.showView('#Header', new OtherView({customEvents:this.customEvents}), "login", true);
});
},
..... more code
});
Sample View code. Notice that I am using bind to listen for customEvent. This works fine, but looking for alternative method
LoginView = Backbone.View.extend({
initialize: function(options){
this.customEvents = options.customEvents;
this.customEvents.bind('app:loggedin', this.loggedIn);
},
loggedIn: function(){
console.log("LOG CHANGED");
},
...more code
I'd much rather keep my events with the rest of the events that I am using in my View. Not sure how to achieve this. Should I be extending the View class?
What I'd like to do on my Views
LoginView = Backbone.View.extend({
events: {
"app:loggin" : "loggedIn"
},
loggedIn: function(){
console.log("LOG CHANGED");
},
...more code
Gist : https://gist.github.com/vermilion1/5600032
Demo : http://jsfiddle.net/vpetrychuk/3NVG9/1
Code preview :
// -- BASE VIEW ------------------
var BaseView = Backbone.View.extend({
constructor : function (options) {
Backbone.View.prototype.constructor.apply(this, arguments);
this._eventAggregator = options && options.eventAggregator || this;
this._parseTriggers();
},
_parseTriggers : function () {
_.each(this.triggers || {}, function (fn, event) {
var handler = this[fn];
if (_.isFunction(handler)) {
this.listenTo(this._eventAggregator, event, handler);
}
},
this);
}
});
// -- TEST ------------------
var View = BaseView.extend({
triggers : {
'hello' : 'helloHandler',
'bye' : 'byeHandler'
},
helloHandler : function (name) {
console.log('hello, ' + name);
},
byeHandler : function (name) {
console.log('bye, ' + name);
}
});
var view1 = new View();
view1.trigger('hello', 'dude 1'); // -> hello, dude 1
view1.trigger('bye', 'dude 1'); // -> bye, dude 1
var vent = _.extend({}, Backbone.Events);
var view2 = new View({eventAggregator:vent});
vent.trigger('hello', 'dude 2'); // -> hello, dude 2
vent.trigger('bye', 'dude 2'); // -> bye, dude 2
Related
Is it possible to make a set of default events which exist in every view? For example if every view in my application includes a settings button
events: {
"click #settings" : "goSettings"
},
...
goSettings: function() {
// settings show function
});
How can I can package this event to be included in every view in my application?
The problem is that View#extend simply overwrites existing properties so you can't put your 'click #settings' in a base class and subclass that. However, you can easily replace extend with something of your own that merges events. Something like this:
var B = Backbone.View.extend({
events: {
'click #settings': 'goSettings'
}
}, {
extend: function(properties, classProperties) {
properties.events = _({}).extend(
properties.events || { },
this.prototype.events
);
return Backbone.View.extend.call(this, properties, classProperties);
}
});
And then extend B instead of Backbone.View for your views.
Demo: http://jsfiddle.net/ambiguous/Kgh3V/
You can create a base view with the event(s) and functions, then make your other views inherit from it. I like the pattern described here, because it's simple to set up and easy to override as needed: http://www.scottlogic.com/blog/2012/12/14/view-inheritance-in-backbone.html
A base view looks like this:
var BaseSearchView = function(options) {
this.inheritedEvents = [];
Backbone.View.call(this, options);
}
_.extend(BaseView.prototype, Backbone.View.prototype, {
baseEvents: {},
initialize: function(options) {
// generic initialization here
this.addEvents({
"click #settings" : "goSettings"
});
this.initializeInternal(options);
},
render: function() {
// generic render here
this.renderInternal();
return this;
},
events: function() {
var e = _.extend({}, this.baseEvents);
_.each(this.inheritedEvents, function(events) {
e = _.extend(e, events);
});
return e;
},
addEvents: function(eventObj) {
this.inheritedEvents.push(eventObj);
},
goSettings: function() {
// settings show function
}
});
BaseView.extend = Backbone.View.extend;
And your child classes like this:
var MyView = BaseView.extend({
initializeInternal: function(options) {
// do something
// add event just for this child
this.addEvents({
"click #differentSettings" : "goSettings"
});
},
renderInternal: function() {
// do something
}
});
I have the problem that the event "form:selectedForm" is calling the method "showForm" but when sending this to my view I am getting the following error: TypeError: e[t] is not a function.
This is stated in line 128 in the backbone.js script but I have no clue what he is doing there. It looks like that he is looking for a "to" or "on" event on the collection.
What I am doing wrong here?
MyController = Backbone.Marionette.Controller.extend({
initialize: function(options) {
this.options = options;
this.urls = options.urls;
this.mainRegion = options.mainRegion;
this.view = new MyLayout();
this.mainRegion.show(this.view);
this.view.render();
this.showSelectorView(this.view.formHeader);
},
showSelectorView : function(view) {
var forms = new MyForms();
forms = this.urls.loadForms;
var selectorView = new FormSelectorView({
collection: forms
});
forms.fetch();
this.listenTo(selectorView, "form:selectedForm", this.showForm);
view.show(selectorView);
},
showForm : function(models) {
console.log("showForm");
var form = new FormContentView({
collection: models
});
this.view.form.show(form);
}
});
MyLayout = Backbone.Marionette.Layout.extend({
template: Backbone.Marionette.TemplateCache.get('#content'),
regions: {
formHeader: "#selector",
form: "#formContent",
formContent: "#content",
formFooter: "#save",
formTemplates: "#templates"
}
});
FormSelectorView = Backbone.Marionette.ItemView.extend({
template: Backbone.Marionette.TemplateCache.get('form-selector-template'),
events : {
"click option" : "selectForm"
},
initialize : function() {
this.listenTo(this.collection, "sync", this.render, this);
},
selectForm : function(e) {
e.preventDefault();
var id = $(e.currentTarget).attr("name");
var item = this.collection.get(id);
this.trigger("form:selectedForm", item.attributes.fields);
}
});
I think the error is in your showSelector view function, you are overwriting your forms collection in the second line,
i think your intention in that line was to assing the url of the forms collection so my guess is that this will fix it:
showSelectorView : function(view) {
var forms = new MyForms();
forms.url = this.urls.loadForms; /// Im assuming you were trying to pass the url here
var selectorView = new FormSelectorView({
collection: forms
});
forms.fetch();
this.listenTo(selectorView, "form:selectedForm", this.showForm);
view.show(selectorView);
},
I have a page which include two backbone views (views related to two template). I am changing content of one views based on clicking event on different items on another view. For this, Every time I click on any items in one view I just create a instance of another view which include some socket.io events. At the first time It's work well but everytime I click on item on first view it just create the instance of 2nd one so that all the socket.io events is binding. Except first click every time I click on items on first view and call an socket.io events, it fired more than one time based on how many click I have done to different items.
I know that every time I click an items it create an instance of a view with socket.io event bind. But I can not get the way to unbind the previous socket.io events.
I have tried to use this reference:
Backbone.js View removing and unbinding
But it is not working in my case. May be I did not use it in proper way.
Can anyone please give me a solution or way to unbind all the socket.io events binded before?
Here is my Clicking event from where I am creating a new instance of another view where all the socket.io events binds.
LoadQueueDetails: function (e) {
e.preventDefault();
var queues = new Queues();
queues.fetch({
data: $.param({ Code: this.model.get("QueueCode") }),
success: function () {
$("#grid21").html(new SearchResultListView({ collection: queues }).el);
},
error: function (queues) {
alert('error found in fetch queue details');
}
});
}
And here is my actual view where I bind all the socket.io events.
window.SearchResultListView = Backbone.View.extend({
initialize: function () {
this.collection.on('change', this.render, this);
this.render();
},
render: function () {
var Queues = this.collection;
var len = Queues.length;
$(this.el).html(this.template());
for (var i = 0; i < len; i++) {
$('.QueueListItem', this.el).append(new SearchResultListItemView({ model: Queues.models[i]}).render().el);
}
return this;
}
});
window.SearchResultListItemView = MainView.extend({
tagName: "tr",
initialize: function () {
this.__initialize();
var user;
if ($.super_cookie().check("user_cookie")) {
this.user = $.super_cookie().read_JSON("user_cookie");
}
this.model.bind("change", this.render, this);
this.model.on("destroy", this.close, this);
socket.emit('adduser', this.user.UserName, this.model.get("Code"));
},
events: {
"click a": "JoinQueue"
},
onClose: function(){
this.model.unbind("change", this.render);
},
close: function () {
this.remove();
this.unbind();
this.model.unbind("change", this.render);
},
socket_events: {
"updatechat": "updatechat",
"changeroom": "changedroom"
},
changedroom: function (username, data) {
alert(data);
socket.emit('switchRoom', data);
},
updatechat: function (username, data) {
alert(username);
alert(data);
},
JoinQueue: function (e) {
e.preventDefault();
if ($.super_cookie().check("user_cookie")) {
user = $.super_cookie().read_JSON("user_cookie");
}
socket.emit('sendchat', "new user");
},
render: function () {
var data = this.model.toJSON();
_.extend(data, this.attributes);
$(this.el).html(this.template(data));
return this;
}
});
window.Queue = Backbone.Model.extend({
urlRoot: "/queue",
initialize: function () {
},
defaults: {
_id:null,
Code: null,
ServiceEntityId: null,
ServiceEntityName:null,
Name: null,
NoOfWaiting: null,
ExpectedTimeOfService: null,
Status: null,
SmsCode: null
}
});
window.Queues = Backbone.Collection.extend({
model: Queue,
url: "/queue",
initialize: function () {
}
});
Backbone.View.prototype.close = function () {
this.remove();
this.unbind();
if (this.onClose) {
this.onClose();
}
}
And this is my main view to bind socket.io event in searchResultItemview.
var MainView = Backbone.View.extend({
initialize: function () {
this.__initialize();
},
__initialize: function () {
if (this.socket_events && _.size(this.socket_events) > 0) {
this.delegateSocketEvents(this.socket_events);
}
},
delegateSocketEvents: function (events) {
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) {
method = this[events[key]];
}
if (!method) {
throw new Error('Method "' + events[key] + '" does not exist');
}
method = _.bind(method, this);
socket.on(key, method);
};
}
});
For extra information:
1. I am opening socket connection globally. Like this :
var socket = io.connect('http://localhost:3000');
I am waiting for any kind of advice or solution to get out of this problem. Please feel free to ask any kind of inquiries.
Basically you have to do socket.removeListener for every socket.on when you close your View.
You can update your MainView and add a close method.
This is how it looks in my code (CoffeeScript)
close: ->
self = #
_.each #socket_events, (method, key) ->
method = self[self.socket_events[key]]
socket.removeListener key, method
I have the following problem. On a user-event (click on .twitterDefault) I call save event with
twitter : {
handle : handle,
ignore : false
}
Then the success function gets called and I set fields on the model (klout, twitter and tester). All fields are set (logging statements all print out appropiate objects.
However, then I call view.render() and here twitter is not set anymore. I have no idea why, there is no sync happening after the save so twitter does not get overwritten (additionally I made sure twitter is also saved on the server before the success method gets called).
Any help greatly appreciated!
Code as follows (stripped to improve readability)
$(function() {
var ContactModel,
ContactModelCollection,
ContactView,
ContactCollectionView,
contacts,
contactCollectionView;
//base model
ContactModel = Backbone.Model.extend({
defaults : {
},
initialize : function() {
}
});
ContactModelCollection = Backbone.Collection.extend({
model : ContactModel,
url : '/api/contacts',
comparator : function(contact) {
return contact.get('strength_of_relationship');
},
initialize : function() {
}
});
ContactView = Backbone.View.extend({
tagName : 'li', //attempting to create a new element
render: function() {
var compiled_tmpl = _.template($('#contact-template').html());
var html = compiled_tmpl(this.model.toJSON());
console.log('model.get("twitter")=('+JSON.stringify(this.model.get('twitter)'))+')');
console.log('model.get("klout")=('+JSON.stringify(this.model.get('klout'))+')');
console.log('model.get("tester")=('+JSON.stringify(this.model.get('tester'))+')');
this.$el.html(html);
console.log('rendered view successfully)');
return this;
},
initialize: function() {
console.log('contactView initalized');
this.model.bind('change', this.render, this);
this.model.bind('destroy', this.remove, this);
},
events: {
'click .twitterDefault' : 'assignDefaultTwitterHandle',
},
assignDefaultTwitterHandle : function(event) {
var handle = $(event.currentTarget).data('twitter');
this.assignTwitterHandle(handle);
},
assignTwitterHandle : function(handle) {
console.log('model assignTwitterHandle. handle='+handle+')');
var view = this,
model = view.model;
model.save({
twitter : {
handle : handle,
ignore : false
},
id : model.get('id')
}, {
error : function() {
console.log('saving twitter handle failed');
},
success : function(model, response) {
console.log('response=('+JSON.stringify(response)+')');
if(response.error) {
console.log('error on server ='+response.error);
}
if(response.twitter) {
console.log('twitter is set');
var twitter = {
handle : handle,
tweet : response.twitter,
age : new Date()
};
console.log('setting twitter to '+JSON.stringify(twitter));
model.set('twitter', twitter);
model.set('tester', 'tester');
console.log('twitter after setting it = '+JSON.stringify(model.get('twitter')));
console.log('view\'s model twitter after setting it = '+JSON.stringify(view.model.get('twitter')));
}
if(response.klout) {
console.log('klout is set');
var klout = {
topics : response.klout
}
console.log('setting klout to '+JSON.stringify(klout));
model.set('klout', klout);
}
if(response.twitter || response.klout) {
console.log('Rerendering view after setting klout/twitter');
view.render();
}
}
});
}
});
contacts = new ContactModelCollection;
ContactCollectionView = Backbone.View.extend({
el : $('#suggestions-list'),
initialize : function(){
contacts.bind('add', this.addOne, this);
contacts.bind('reset', this.addAll, this);
contacts.bind('all', this.render, this);
},
render : function(){
console.log('contactcollectionview render');
},
addOne : function(contact) {
console.log('addOne');
var view = new ContactView({model: contact});
var el = view.render().el;
console.log('el=('+el+')');
$('#suggestions-list').append(el);
},
addAll : function() {
console.log('addAll');
contacts.each(this.addOne);
}
});
contactCollectionView = new ContactCollectionView;
App.contacts = contacts;
App.contactCollectionView = contactCollectionView; });
I guess the problem is the scope of the render function.
Depending from where is called, this takes a different value.
To warranty that always this is pointing to the View scope, add to your initialize:
_.bindAll(this,"render");
Also, it's not good habit to call view.render manually. You should let the events do their work. Model save already triggers some events. Just listen to them to update your View.
I've been working on a prototype Backbone application using Backbone.LayoutManager and I'm running into something I don't understand.
The scenario is that I have a form for adding "people" {firstname, lastname} to a list view, I save the model fine and the new item shows up in the list. I also have a remove function that works when after the page is refreshed, but if I try to delete the person I just created without a page refresh, the removeUser() function never gets called.
My code is below. Can someone help me out? I'm just trying to learn Backbone and if you have the answer to this question as well as any other criticisms, I'd be grateful. Thanks.
define([
// Global application context.
"app",
// Third-party libraries.
"backbone"
],
function (app, Backbone) {
var User = app.module();
User.Model = Backbone.Model.extend({
defaults : {
firstName: "",
lastName: ""
}
});
User.Collection = Backbone.Collection.extend({
model: User.Model,
cache: true,
url: "/rest/user"
});
User.Views.EmptyList = Backbone.View.extend({
template: "users/empty-list",
className: "table-data-no-content",
render: function (manage) {
return manage(this).render().then(function () {
this
.$el
.insertAfter(".table-data-header")
.hide()
.slideDown();
});
}
});
User.Views.Item = Backbone.View.extend({
template: "users/user",
tagName: "ul",
className: "table-data-row"
events: {
"click .remove": "removeUser"
},
removeUser: function () {
console.log(this.model);
this.model.destroy();
this.collection.remove(this.model);
this.$el.slideUp();
if (this.collection.length === 0) {
this.insertView(new User.Views.EmptyList).render();
}
}
});
User.Views.List = Backbone.View.extend({
initialize: function () {
this.collection.on("change", this.render, this);
},
render: function (manage) {
if (this.collection.length > 0) {
jQuery(".table-data-no-content").slideUp("fast", function() {
$(this).remove();
});
this.collection.each(function(model) {
this.insertView(new User.Views.Item({
model: model,
collection: this.collection,
serialize: model.toJSON()
}));
}, this);
} else {
this.insertView(new User.Views.EmptyList());
}
// You still must return this view to render, works identical to
// existing functionality.
return manage(this).render();
}
});
User.Views.AddUser = Backbone.View.extend({
template: "users/add-user",
events: {
"click input#saveUser": "saveUser"
},
render: function (manage) {
return manage(this).render().then(function () {
$("input[type='text']")
.clearField()
.eq(0)
.focus();
});
},
saveUser: function () {
var user = new User.Model({
firstName: $(".first-name").val(),
lastName: $(".last-name").val()
});
this.collection.create(user);
this
.$("input[type='text']")
.val("")
.clearField("refresh")
.removeAttr("style")
.eq(0)
.focus();
}
});
return User;
});
The problem turned out to be an incorrect response from the server. Once the server sent back the correct JSON object, everything worked correctly.