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.
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 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.
I'm doing this inside one of my Views:
render: function($options) {
...
this.collection.on('reset', _(function() {
this.render($options);
}).bind(this));
....
}
The problem is, whenever reset as well as the re-rendering has been triggered, a new reset binding will be created, resulting 2x, 4x, 8x, etc. times of re-rendering as it goes on.
It's a bit tricky to move the binding into the initialize section (which should solve this issue), however since it's not an option, is there any other solution available, like having Backbone checking if this event has been bound before, or something?
Moving your binding to initialize would be best but assuming that you have good reasons not to, you could just set a flag:
initialize: function() {
var _this = this;
this._finish_initializing = _.once(function($options) {
_this.collection.on('reset', function() {
_this.render($options);
});
});
//...
},
render: function($options) {
this._finish_initializing($options);
//...
}
There are lots of different ways to implement the flag, _.once just nicely hides the flag checking. You could also trigger an event in render have a listener that unbinds itself:
initialize: function() {
var finish_initializing = function($options) {
/* your binding goes here ... */
this.off('render', finish_initializing);
};
this.on('render', finish_initializing, this);
},
render: function($options) {
this.trigger('render', $options);
//...
}
That's the same logic really, just dressed up in different clothes. You could also use an explicit flag and an if in render or assign a function to this._finish in initialize and that function would delete this._finish.
like having Backbone checking if this event has been bound before, or something?
Sure..
!!this.collection._events["render"]
Backbone doesn't expose most of the API required to make it useful. That's alright, use it anyway.
First, define your event handler function as a named function
var self = this;
var onReset = function() {
self.render($options);
}
Then, defensively unbind the function each time render is called
this.collection.off('reset', onReset);
this.collection.on('reset', onReset);
I recently accomplished this using a javascript variable.
Outside of any functions, I declared:
var boundalready =0
Then, inside the function:
if (boundalready == 0){
boundalready = 1;
bind(this);
};
This worked for me pretty well.
I have a sample single-page-application in Backbone that I am messing around with. The idea is that I have a button that will trigger a refresh on a view. I am trying to get the event handler to remember 'this' as the backbone view, not the element that it was called on. No matter how much docs I read, I cant seem to make it past this mental hump.
in my view, I have
initialize: function() {'
//i am trying to say, call render on this view when the button is clicked. I have tried all of these calls below.
//this.$("#add-tweet-button").click(this.render);
//this.$("#add-button").bind("click", this.render);
}
When the render function is called, the 'this' element is the button. I know what im missing is pretty easy, can someone help me out with it? Also, is this sound as coding conventions go?
If you use the View's 'delegateEvents' functionality, the scoping is taken care of for you:
var yourView = Backbone.View.extend({
events: {
"click #add-tweet-button" : "render"
},
render: function() {
// your render function
return this;
}
});
This only works with elements that are 'under' the View's El. But, your example shows this.$(...), so I'm assuming this is the case.
#Edward M Smith is right, although if you need to handle a jquery event of element outside the scope of your View you might write it that way :
var yourView = Backbone.View.extend({
initialize: function() {
var self = this;
$("button").click(function () {
self.render.apply(self, arguments);
});
},
render: function(event) {
// this is your View here...
}
});
So, I've started using Backbone.js to structure my javascript code and have modular applications, and I came across a problem reggarding events.
I want to make a simple View that handles forms and validates them. In the future I would like to add all the javascript functionallity like live validation, hover effects, etc.
This is the simplified code I have right now:
var Form = Backbone.View.extend({
attributes: {
att1 = 'att1',
att2 = 'att2'
},
events: {
'submit': 'validateFields'
},
initialize: function(element) {
this.el = $(element);
},
validateFields: function() {
alert(this.attributes.att1); //do something
return false;
}
});
var f = new Form('#formid');
The problem I had is that the validateFields function is not called when I submit the form. I also tried using this on the constructor:
this.el.bind('submit', this.validateFields);
Now, that last code works but the "this" inside the validation function would be the $('#formid') object, instead of my Form object.
Backbone uses jQuery's delegate method to bind the events to the callbacks in the events hash.
Unfortunately the delegate method does not work for the submit event in IE See comment in Backbone source
An easy fix is to add this line of code to your render method.
render: function() {
//render the view
$(this.el).submit(this.validateFields);
}
You will also need to bind the context for validateFields in the initialize method
initialize: function() {
_.bindAll(this, 'render', 'validateFields');
}
Try setting your el in other way:
var f = new Form({el: '#formid'});
In this case you can even remove initialize method (or change it):
var Form = Backbone.View.extend({
// ...
initialize: function() {
// Some code
},
// ...
});
As far as this code is concerned: this.el.bind('submit', this.validateFields);. If you want to preserve Form object context you should use binding:
this.el.bind('submit', _.bind(this.validateFields, this)); // using Underscore method
this.el.bind('submit', $.proxy(this.validateFields, this)); // using jQuery method