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
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 have a number of different "control elements" on my application: dropdowns, tabs, menus, etc. On same pages, there are many of the same control. When writing JavaScript to handle the different events associated with each of these controls, I'm trying to make my code as DRY as possible. One of the challenges is modularizing my JQuery code so that events that occur within a specific control only effect that control.
Take this initial code for example, all it does is open a dropdown menu when it is clicked. I'm used to writing just a ton of different anonymous functions triggered by different events so this type of JQuery is really new to me.
var dropdown = {
init: function() {
$(".dropdown").click(".dropdown", dropdown.openDropdown);
},
openDropdown: function() {
$(this).children(".dropdown-menu").show();
$(this).addClass("open");
}
}
$(document).ready(dropdown.init);
My question is, within this dropdown variable, I want to be able to save/track different pieces of the dropdown control currently being acted upon. For example, I might want to write:
var menu = $(this).children(".dropdown-menu");
somewhere in this chunk so that I could refer back to this menu while calling different functions. I just cannot figure out syntactically how to do this. Any help/guidance is welcomed! Thanks.
Something I like about coffeescript is how it allows you to easily create classes. Classes in coffee are just a simplified way of generating "modules" using javascript's prototypal inheritance. More on that here: http://coffeescript.org/#classes
But how YOU could implement more modular jQuery code is by doing something like this:
http://jsfiddle.net/x858q/2/
var DropDown = (function(){
// constructor
function DropDown(el){
this.el = $(el);
this.link = this.el.find("a");
this.menu = this.el.find(".dropdown-menu");
this.bindClick();
}
// method binding click event listener
DropDown.prototype.bindClick = function(){
var _this = this;
this.link.click(function(e){
_this.openDropDown();
e.preventDefault();
});
};
// click event handler
DropDown.prototype.openDropDown = function(){
this.menu.show();
this.link.addClass("open");
};
return DropDown;
})();
$(function(){
// init each .dropdown element as a new DropDown
$(".dropdown").each(function(){
new DropDown(this);
});
});
You've touched on a pattern I've been leaning towards more and more. Basically, create a JavaScript object that acts as a controller given a root element on the page. Since this "dropdown" is pretty generic, it could probably have access to the whole page and be perfectly happy. I would also recommend making these "modules" instantiable objects, as this allows you to write unit tests easier:
function DropdownModule() {
this.handleClick = this.handleClick.bind(this);
}
DropdownModule.prototype = {
element: null,
$element: null
constructor: DropdownModule,
init: function(element) {
this.setElement(element);
this.$element.on("click", ".dropdown", this.handleClick);
},
handleClick: function(event) {
var $dropdown = $(event.currentTarget);
$dropdown.children(".dropdown-menu").show();
$dropdown.addClass("open");
this.someOtherFunction($dropdown);
},
someOtherFunction($dropdown) {
// do something with $dropdown
},
setElement: function(element) {
this.element = element;
this.$element = $(element);
}
}
Then to use it, just throw this anywhere after the definition for Dropdown:
var dropdown = new Dropdown()
.init(document.documentElement);
The document.documentElement property refers to the <html> tag and is available the moment JavaScript begins executing.
As a side note, I've built a whole framework around this approach: Foundry. Other frameworks, like Angular, take a similar approach as well.
What you want sounds like exactly what jQuery UI has already implemented in their Widget Factory.
I'd highly recommend you check it out since what you'd end up with it something like
$.widget( 'dropdown', {
_create: function() {
this.element.addClass( 'dropdown' );
this._on({
'click': '_clicked'
});
},
_clicked: function( event ) {
// `this` is an instance of dropdown here, not the element
this.clicked = !this.clicked;
this.element.toggleClass( 'clicked', this.clicked );
},
_destroy: function() {
this.element.removeClass( 'dropdown' );
}
});
Then you would use it like any other jQuery UI Widget
$( '#some-element' ).dropdown();
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 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 have a jQuery plugin that needs to register a click event handler:
$.fn.myPlugin = function (options) {
var settings = {
// snipped
};
$.extend(settings, options || {});
$("body").click(function () {
// Do Something
});
// Rest of the plugin
});
The problem is that multiple invocations register the function more than once. Since the function needs to stay attached, I can't use .one().
Is there a way if a function is already attached? Can I give it a name or so? Or do I have to set some boolean flag using closure magic?
Namespace your events.
$('body').unbind('click.myPlugin').bind('click.myPlugin', function() {
..code...
});
More on Namespaced Events.
A very easy method with good performance would be to set a data element on the body element:
if (!$.data(document.body, 'myPluginRegistered') {
$.data(document.body, 'myPluginRegistered', true);
// do your plugin code here
}
Easiest might be the boolean plus closure magic you suggested. Alternatively, you could get the list of all functions bound to "click" object, and see if the function you're attaching is there already.
This question shows how to get the list.
List all javascript events wired up on a page using jquery
Though, the namespace suggestion that came in after I first responded is probably simpler.