I'm creating a Backbone.js plugin that offers a basic grid layout given supplied JSON data. My problem is that I'm not sure how to deal with binding events to a View class without altering the plugin itself. And I'd rather not do this -- I'd rather have the user of the plugin be able to extend the view, or alter its prototype to bind custom events.
The View in the plugin is a basic view without any events binded. It also contains some other functions which I've omitted for simplicity.
FlipCard.CardView = Backbone.View.extend({
tagName: 'div',
className: 'card',
// and more
});
I've attempted to use the prototype attribute in my separate app.js file to bind events, but they don't seem to be triggered.
FlipCard.CardView.prototype.events = {
'click .card' : 'alert'
};
FlipCard.CardView.prototype.alert = function(){
alert("hello!");
};
And I'm familiar with the .extend({}) function, but that won't work unless I can somehow inform the plugin to use the extended version of the view... which I'd rather not do.
Any ideas on what I should be doing here?
EDIT: Turns out it was a silly error. Because the view has the class '.card' and I was trying to bind a click event to '.card', it's unnecessary to put in 'click .card'. Instead the event should be:
FlipCard.CardView.prototype.events = {
'click' : 'alert'
};
If someone were to use their plugin, they would extend your FlipCard.CardView in the same way you are extending Backbone.Model
myApp.Views.myCardView = FlipCard.CardView.extend({
events: {
'click .card' : 'alert'
},
alert: function() {
alert("hello!");
}
}
This creates an extended version of your plugin view with events bound to it, and it does not alter the plugin in any way. The user would then instantiate it as normal:
var someView = new myApp.Views.myCardView();
Related
I'm new to backbone so forgive me if this is obvious. I wrote a function to change the value associated with a toggle switch to either true or false based on its position. I used this code in two different views and want to refactor it out.
I created a Utils object and attached the function as a method to that object. I then imported Utils into both views. Here is a bit of the code as I have it properly functioning now:
var AddView = AbstractView.extend({
template: "path/to/template.html",
events: {
"change .toggleBoolean" : "temp"
},
temp: function(e){
Utils.toggleValue.call(this, e);
}, ...
This works in both places as expected. However, I was hoping to replace "temp" in the events hash with the temp method. Any direction on how to properly do this would be greatly appreciated.
You can simply do this:
var AddView = AbstractView.extend({
template: "path/to/template.html",
events: {
"change .toggleBoolean" : Utils.toggleValue
},
Utils.toggleValue will be invoked in view's context.
Here is the part of backbone code that deals with the event object:
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[method];
if (!method) continue;
var match = key.match(delegateEventSplitter);
this.delegate(match[1], match[2], _.bind(method, this));
//-------------------------------------^ context is handled here
}
it checks whether value is a function, and binds the handler function context to view
You could just use a reference instead of a string
var Myview = Backbone.View.extend({
events: {
'click p': Utils.action
},
});
plnkr
Or if you want to mix the util methods into the view class instead of relying on delegation you can do
var Myview = Backbone.View.extend({
events: {
'click p': 'action'
},
});
Myview.prototype.action = Utils.action;
plnkr
The above will preserve the context as you can see in the plunker.
My preferred method if you plan to start pulling more logic out this way would be to look at a plugin that offers mixin functionality. Marionette has (amongst many other useful things) functionality for sharing behaviors between views. There are several other approaches as well that you can research by googling for "Backbone mixin".
I'm attempting to add a bit of processing functionality to Backbone.View in the initialize function that I want to be carried over to all my Backbone Views. The problem is, I'm using Marionette so I can't do something like this:
var BaseView = Backbone.View.extend({})
because Marionette extends Backbone.View itself. Here's what I would like to do:
// Add processoring logic to an extended version of Backbone.
Backbone.View.extend({
initialize: function(options){
if(options.hasOwnProperty("vents") {
// process vents
}
// native code. Calling the library's actual original function to maintain original functionality.
Backbone.View.initialize(this, aurguments);
}
})
var CollectionView = new Marionette.CollectionView({
vents: {VentCallName: function(){}}
// When initialize is called, it'll see the vents and deal with them automatically.
});
I'm just not sure how to add the functionality to Backbone.View while maintaining whatever function logic is already in there.
EDIT
How do I actually get the initial extended functionality into Backbone.View.initialize without making a new extended instance and basing all my views off that? I can't get Marionette to use that extended view, so the extra processing has to go into Backbone.View's initialize function.
If I do this, it loops back on itself:
Backbone.View.prototype.initialize = function(){
console.log("moooo");
// custom logic then run Backbone.View.initialize native code.
Backbone.View.prototype.initialize.apply(this, arguments);
}
Backbone.View.prototype.initialize.apply(this, arguments);
Edit
Okay, well that's a slightly different question.
var oldInitialize = Backbone.View.prototype.initialize;
Backbone.View.prototype.initialize = function(){
console.log("moooo");
// custom logic then run Backbone.View.initialize native code.
oldInitialize.apply(this, arguments);
}
Lets say I have the following Backbone view which loads two links, one with the anchor text "test1" and the other with the anchor text "test2".
I bind a click event and I get the HTML of the link that was clicked and store it inside the clickedHtml variable.
Now, this view is loaded by a Backbone router.
When the user clicks either one of the two links (test1 or test2) another view called "main" will be loaded by the router.
Now, how can I pass the "clickedHtml" variable to that view?
Should I use LocalStorage?
Should I declare it globally like window.clickedHtml?
Is there a better way?
Ty!
// file: views/test.js
define([
'jquery',
'underscore',
'backbone'
], function($, _, Backbone) {
var Test = Backbone.View.extend({
el : '.test',
initialize : function () {
var that = this;
that.$el.html('test1<br />test2');
},
events : {
'click .test a' : 'click'
},
click : function (e) {
var clickedHtml = $(e.target).html();
}
return Test;
});
Here is my router:
// file: router.js
define([
'jquery',
'underscore',
'backbone',
'views/test',
'views/main'
], function ($, _, Backbone, Test, Main) {
var Router = Backbone.Router.extend({
routes: {
'' : 'home',
'test' : 'test'
}
});
var initialize = function () {
var router = new Router();
router.on('route:home', function () {
var main = new Main();
});
router.on('route:test', function () {
var test = new Test();
});
Backbone.history.start();
}
return {
initialize : initialize
}
});
Basicly you should use Backbone.Event:(Or it's equivalents in Marionette)
//Declaration
var notificationService = {};
_.extend(notificationService, Backbone.Events);
//Used by listener
notificationService.on("alert", function(o) {
alert(o);
});
//Used by publisher
notificationService.trigger("alert", {foo:"bar"});
The real question is how does it get passed from one view to another?
The way I see it, you have 2 options:
Bubble notificationService from one view to another in initialization
Wrap the notificationService with a requirejs model that returns it (creates a 'almost global' notificationService that can be passed by requirejs).
Although I don't like singletons a bit, this case a of a singleton notificationService object that can easily get injected by requirejs in every model will come in handy.
EDIT:
Another option, the quick and dirty one, just use jquery to trigger event on the DOM (specifically the body element) and listen to body in the other view
//on Listening view, after DOM is ready
$( "body" ).on( "alert", function( event, param1, param2 ) {
alert( param1 + "\n" + param2 );
});
//on Triggering view, after DOM is ready
$( "body").trigger( "alert", [ "Custom", "Event" ] );
NOTE:
notice that once a listening view is closed, it must removes itself from listening to events (unbind/off), so you wont have memory leak
Architecturally speaking, your aim should be to keep your code generic & reusable.
One of the main things you don't want to do in a situation like this is to pass direct references from one object to another - if you end up changing the setup of one of the objects, or you need to pass data from another object as well, this can get messy really fast.
One design pattern that's widely used in situations like this is a mediator. Also known as "pub/sub" you can have a centralized standalone object that mediates information between objects. Certain objects will publish information and other objects can subscribe to them. The mediator acts as an intermediary so that the objects never have to communicate directly with each other. This makes a much more generic, reusable and maintainable solution.
More info here:
http://addyosmani.com/largescalejavascript/#mediatorpattern,
Javascript Patterns
On the Backbone side of things... If you've used Marionette, you may have come across a complimentary mini-library (also implemented by Derick Bailey) called wreqr. You can use this to create a simple mediator with low-overhead in your Backbone applications.
https://github.com/marionettejs/backbone.wreqr
It basically allows you to use backbone style events across objects. Example below:
First, you need to create a globally accessible mediator object, or add it to your app namespace or use require.js:
var mediator = new Wreqr.EventAggregator();
inside View #1
events : {
'click .test a' : 'click'
},
click : function (e) {
var clickedHtml = $(e.target).html();
// trigger an 'element:click' event, which can be listened to from other
// places in your application. pass var clickedHtml with the event
// (passed to the arguments in your eventhandler function).
mediator.trigger('element:click', clickedHtml);
}
Inside View #2
initialize: function(){
//...
this.listenTo(mediator, 'element:click', this.myEventHandler, this);
}
myEventHandler: function(elem){
// elem = clickedHtml, passed through the event aggregator
// do something with elem...
}
Backbone events are the way to go here.
When you capture the event in the view, I would bubble it up using:
click : function (e) {
var clickedHtml = $(e.target).html();
Backbone.Events.trigger("eventname",clickedHtml);
}
Then, you should be able to capture this in your router initialise function, using:
Backbone.Events.on("eventname", responseFunction); // listen out for this event
And then in the router declare a separate function:
responseFunction : function(clickedHtml)
{
//Do whatever you want here
}
I'm writing this from memory, so hopefully it make sense. I've also not tested catching an event like this i the router, but it should work.
HTH.
In the exact case you outline I would create a temp storage object on your global namespace and use that to transfer the data between your views, its a bit "hacky" but its better than using local storage, or the window object directly, at least with a temp object on your own global namespace the intent of the objects usage is known.
I find it better to use the http://backbonejs.org/#Events for a similar purpose of passing data between two views, though it does depend on how you structure your pages, if you have two views on the page representing a "control" or "component" this approach works really well.
If you post a link to your site or something I can have a look and give you some more help.
Russ
You could perhaps store it as a property on the view:
click : function (e) {
this.clickedHtml = $(e.target).html();
}
If your router can access both views, it can then simply pass the firstView.clickedHtml property to a function in the secondView (or to the initializer)
I've a question regarding View Deletion / Removal. I'm aware that you can call the remove method on a view object which will remove the DOM element, and any event listeners that have been bound via listenTo. My question is if you need to do more than that. I normally bind some extra variables within these views and I'm wanting to know if I need to nullify those as well.
An example view:
var myView = Backbone.View.extend({
el: '#exampleContainer',
events: {
'click': 'onClick'
},
initialize: function() {
this.exampleString = 'Hello World';
this.$exampleSelector = this.$('#exampleChild');
},
onClick: function(event) {
console.log('Hello World');
}
});
Also, would I be right in assuming that it's not enough to call remove, but I'd also need to nullify the variable pointing at the view?
myView.remove();
myView = null;
AFAIK you should set the variable to null, since JavaScript's garbage collector will only throw away objects that are no longer referenced (or objects that have no route to the root object to be exact). Calling .remove() on the object will not destroy the reference, so it will probably stay in memory.
This post on HTML5Rocks explains what the "Object Graph" is and how JavaScript's garbage collection works. (I think the GC workflow differs from engine to engine, but that's basically how it works)
I understand how custom events work in Backbone and how to trigger them, but I'm having trouble understanding when exactly to use them and what purpose they serve over just calling the function directly.
e.g.
var MyView = Backbone.View.extend({
tagName: 'div',
className: 'myview',
initialize: function() {
this.model.on("mycustomevent", this.doSomething, this);
},
doSomething: function() {
console.log('you triggered a custom event');
}
});
If I am not mistaken, The doSomething method can be called by using this.model.trigger("mycustomevent") within other methods, but can be also called directly with this.doSomething()
Outside the view, it can be called similarly with
var myview = new MyView({model:somemodel});
myview.model.trigger("customevent");
myview.doSomething();
What I am confused about is why not forgo the the custom event and just call the method directly when you need it? Any example uses would be greatly appreciated!
You might want to add multiple handlers in different places in the code, f.ex:
this.model.on("mycustomevent", this.doSomething, this);
// ... and somewhere else you add an anonymous function
this.model.on("mycustomevent", function() {
console.log('do something');
});
Then when you trigger the event, it will execute all handlers. You can also use off to unbind/manage individual or multiple handlers.
If you are asking about a generic explanation of the event pattern (also called observer pattern, publish/subscribe, etc...), you should probably look for a more in-depth article or book.
With backbone, the perfect example is changing a property of a model. Using a function, you would have to do something like this...
$( '#someUI' ).click( function {
// update the model property
myModel.someProperty = 'somethingDifferent';
// update any ui that depends on this properties value
$( '#uiThatDisplaysModelData' ).find( 'someSelector' ).html( 'somethingDifferent' );
// save the model change to the server
$.ajax( {
url: 'somesaveurl',
data: { someProperty: 'somethingDifferent' }
success: callback
} );
} );
And then repeat those steps all over your code for each property change.
With backbone and a little setup the same things can be accomplished with:
myModel.set( 'property', 'somethingDifferent' );
This is because we have attached handlers to the change and change:property events of this model. These are custom events that are created automatically for models by backbone. So whenever any part of your code manipulates the model, the DOM updates and saving can be done automatically. We can also bind input validation or whatever we want to these custom events.
It's basically just applying the observer pattern to your application, where events belong to an object that is observable, and the handlers belong to its observers.
http://en.wikipedia.org/wiki/Observer_pattern