I am building a Backbone.js application, I use BackboneJS Radio for messaging.
First I created a channel:
App.actionsChannel = Backbone.Radio.channel('actions');
And when I click an action button, let's say 'next' action button:
App.actionsChannel.trigger('action:triggered', 'next');
And I handle the action:
App.actionsChannel.on('action:triggered', function(actionName){
//do some ajax requests
});
The problem is, when I click the next button for the first time, it triggers the next action one time, and the second time, it triggers twice, the third time it triggers 4 times, and so on...
Every time I trigger the next action, it fires many times, not once. And when I checked the actionsChannel._events, I found it contains all the actions I triggered.
It's because the registering on is done multiple times, somewhere not shown in your question, and it should only be done once.
✘ Don't do this
var view = Backbone.View.extend({
events: {
"click": "onClick"
},
onClick: function(e) {
App.actionsChannel.on('action:triggered', function(actionName) {
//do some ajax requests
});
}
});
✔ Do this
var view = Backbone.View.extend({
events: {
"click": "onClick"
},
initialize: function(){
App.actionsChannel.on('action:triggered', this.onActionTriggered);
},
onClick: function(e) {
// or if you must register it here for example.
// First make sure it's unregistered.
App.actionsChannel.off('action:triggered', this.onActionTriggered);
App.actionsChannel.on('action:triggered', this.onActionTriggered);
},
onActionTriggered: function(actionName) {
//do some ajax requests
},
});
Using the on function multiple times just adds another listener to the list. So, when triggered, the callback is called as much times as it was registered.
The best
It is recommended to use listenTo instead of on whenever possible to avoid memory leaks.
Backbone.js on vs listenTo
var view = Backbone.View.extend({
events: {
"click": "onClick"
},
initialize: function(){
// this will be removed automatically when the view is `remove`d,
// avoiding memory leaks.
this.listenTo(App.actionsChannel, 'action:triggered', this.onActionTriggered);
},
onClick: function(e) {
},
onActionTriggered: function(actionName) {
//do some ajax requests
},
});
The code snippets above are just examples of how to listen to an event. Use trigger where you need it and where it make sense.
Related
I'm attempting to write some Javascript objects to manage dynamic forms on a page.
The forms object stores an array for forms and renders them into a container.
I'd like to have click events for certain fields on each form so decided to make a seperate object and tried to bind an event inside the objects init method.
The init method is clearly fired for every new form that I add. However on change event only ever fires for the last form object in my array.
JS Fiddle Demonstrating Issue
can be found: here
function Form(node) {
this.node = node;
this.init = function() {
$(this.node).find("input:checkbox").change(event => {
console.log('Event fired');
});
};
this.init();
}
// Object to manage addition / removal
var forms = {
init: function() {
this.formsArray = [];
this.cacheDom();
this.bindEvents();
this.render();
}
// Only selector elems from the DOM once on init
cacheDom: function() { ... },
// Set up add / remove buttons to fire events
bindEvents: function() { ... },
render: function() {
for (let form of forms)
this.$formSetContainer.append(form.node)
}
addForm: function() {
// Logic to create newRow var
this.formsArray.push(new Form(newRow));
},
removeForm: function() {
// Logic to check if a form can be removed
this.formsArray.pop();
}
},
What I've Tried Already
I'm actually able to bind events inside render by removing this.init() inside the Form constructor and altering render like so:
for (let form of this.formsArray) {
this.$formSetContainer.append(form.node)
form.init();
}
Then the events will successfully fire for every form
But I'd rather not have this code run every time I call render() which is called every time I add / remove forms.
I have a feeling that this is a scoping issue or that the event is somehow being clobbered. Either that or I'm misunderstanding how events are bound. Any pointers would be appreciated
Looking at the code in the JSFiddle, the problem comes from using this.$formSetContainer.empty() in the render function. .empty() removes all the event handlers from your DOM nodes.
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
If you want to remove elements without destroying their data or event handlers (so they can be re-added later), use .detach() instead.
https://api.jquery.com/empty/
You can replace this with this.$formsetContainer.children().detach() and it will do what you want.
I'm trying to get my head around custom events. I want some entities to be listeners, and then be able to fire an event without knowing who is listening.
I have this afterrender handler in the main controller, confirmed it is being called and a tab control is "on" for the event userTimeOut:
// Register components
registerComponent: function (component) {
console.log('registering\tid\t' + component['id']);
if (component['id'].indexOf('tab-') > -1) {
console.log("tab getting user timeout event handler\t" + component['id']);
component.on('userTimeOut', this.onUserTimeOut);
}
return true;
},
This is the onUserTimeOut handler:
onUserTimeOut: function (caller) {
console.log('user-time-out\tid' + this['id'] + '\tcaller-id\t' + caller['id']);
}
And I have this code in the main controller called by a button click. Confirmed it does get called as well:
fireUserTimeOut: function(button) {
console.log('fireUserTimeOut\t' + button['id']);
Ext.GlobalEvents.fireEvent('userTimeOut', button);
},
Searching the web, I have tried numerous approaches instead of Ext.GlobalEvents, but no luck. Eventually, I want to be able to have any entity: component, data store, application, view, etc. act as a listener and be able to broadcast the event and have it acted upon.
I can get it to work with built-in events like click or focus, but not these custom events.
Any assistance would be appreciated!
Instead of:
component.on('userTimeOut', this.onUserTimeOut);
Use:
Ext.GlobalEvents.on('userTimeOut', this.onUserTimeOut, component);
Then you can call fireEvent as you are doing. In your onUserTimeOut you can access the component acting on the event via this, e.g. this.id.
I want to trigger a render event when my views are being rendered.
function Renderer() {
_.extend(this, Backbone.Events);
};
Renderer.prototype.render = function(view, model) {
this.trigger('render:before');
// Do some checks to see how
// we should render the view
// and then call render
this.trigger('render:after');
};
var renderer = new Renderer();
Now I can register for events on the Renderer, but I must use the full name. I.e. this works:
renderer.on('render:before', function() { console.log("before rendering"); });
renderer.on('render:after', function() { console.log("after rendering"); });
renderer.on('all', function() { console.log("All events from renderer"); });
But this does not:
renderer.on('render', function() { console.log("Any rendering events"); });
I expected the last one to be equivalent to registering on all events for the renderer.
Is there a way to make listening to render equivalent to listening for both render:before and render:after?
Namespacing event names by using the colon is just a convention:
If you have a large number of different events on a page, the
convention is to use colons to namespace them: "poll:start", or
"change:selection".
The source code of Events.trigger shows that the event handler to be called is searched for by the full name of the event, independently of whether it contains a colon or not:
var events = this._events[name];
...
if (events) triggerEvents(events, args);
You can:
define and trigger an 'all' event,
trigger multiple event handlers by calling trigger with a space-delimited list of event names, or
modify the source code of Events.trigger in backbone.js to add this feature.
I would like to update part of my view when the user types into a input field. Initially I bound to the keyup event listener within the View's events field, and that worked well:
window.AppView = Backbone.View.extend({
el: $("#myapp"),
events: {
"keyup #myInput": "updateSpan",
}, ...
updateSpan: function() {
this.span.text(this.input.val());
}, ...
});
But then I realised that keyup updated too often and made the app slow. So I decided to use the typeWatch plugin so the event would only fire the user stopped typing. But now I don't know how to set the custom event listener in Backbone. Currently I have this:
window.AppView = Backbone.View.extend({
initialize: {
var options = {
callback: function(){
alert('event fired');
this.updateSpan;
},
wait:750
}
this.input.typeWatch(options);
}, ...
updateSpan: function() {
this.span.text(this.input.val());
}, ...
});
Two questions:
I see the alert, but updateSpan is not being fired. I think I'm using this incorrectly in the callback, but how should I do it?
Is initialize now the right place to set the typeWatch event listener, or can I continue to use the events field as I did before?
You aren't actually calling updateSpan, and you're right that this wont be the correct thing. Easiest way to solve it is to just capture the view into another variable first:
var v = this;
var options = {
callback: function() {
alert('event fired');
v.updateSpan();
},
wait: 750
};
this.input.typeWatch(options);
As for your second question, usually I will attach functionality like this in initialize if it's on the base element and in render if it's not, so I think in this case you've probably got it right.
I'm building a Backbone app and I came across this weird issue. In the state A (route: ""), I've got a view like that:
var view = Backbone.View.extend({
events : {
"click a.continue" : "next"
},
next : function(e) {
//Some stuff
Backbone.history.navigate("/page2");
}
});
and once I click on the anchor with "continue" class, I am redirected to a state B (route: "/page2"). If I click on the back button of my browser, and then I click on the anchor, debugging I've noticed that the next function is triggered twice. Actually if I keep going back and forth the number of times the event is triggered keeps increasing.
Any clue?
You've got a zombie view hanging around.
The gist of it is that when you are instantiating and displaying the second view ("state B"), you are not disposing of the first view. If you have any events bound to the view's HTML or the view's model, you need to clean those up when you close the form.
I wrote a detailed blog post about this, here: http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Be sure to read the comments as "Johnny O" provides an alternative implementation which I think is quite brilliant.
I Have the same problem, the solution is...
App.Router = Backbone.Router.extend({
routes: {
"fn1": "fn1",
"fn2": "fn2"
},
stopZombies: function(objView){
if(typeof objView === "object"){
objView.undelegateEvents();
$(objView.el).empty();
}
},
fn1: function(){
this.stopZombies(this.lastView);
var view1 = new App.v1();
this.lastView = view1;
},
fn2: function(){
this.stopZombies(this.lastView);
var view2 = new App.v2();
this.lastView = view2;
}
});
Store the last execute view in this.lastView, then stopZoombies() remove the events from this view.