how to access member variables from a callback? - javascript

I have a simple Backbone view which uses jQuery UI's buttons.
For example I have a delete button, which should delete the model (and view). I managed to add the button to the view in the render method using the following:
var self = this;
$(this.el).find("#deleteButton").button({text : false});
$(this.el).find("#deleteButton").bind( "click", self.deleteView);
and then I have the corresponding method in the view:
deleteView: function(){
console.log(this.model);
}
The "deleteView" method gets called, but it will print "undefined" to the console, as "this" is referring to the button and not the view. Replacing "this" with "self" doesn't work either. Also, passing the model or the view as an argument to the method doesn't seem to work as the argument will be the click event.
What is the correct way to handle such callbacks with backbone?

You would want to use the backbone.viewevents property instead. This is the suggested method rather than manually using jQuery to bind events to child elements during render. http://documentcloud.github.com/backbone/#View-delegateEvents
Would be as simple as adding this to your view:
events: {
"click #deleteButton": "deleteView",
},

The context of a function changes when it's bound as a jQuery event.
Use another function, in which you use self, or jQuery.proxy.
An example of maintaining the context through $.proxy: http://jsfiddle.net/bAeQZ/
var self = {
    deleteView: function(){console.log(this.model)},
    model: '...some model...'
};
$(document).click($.proxy(self.deleteView, self));​
// Alternative without $.proxy:
$(document).click(function(){self.deleteView();});​

Related

Knockout "with" binding removes my jquery DOM events

I have several jquery dom events that are created on DOM load or document ready. These are mostly default behaviors that should be applied to all forms in my application. Example:
$('input:text').focus(function ()
{
$(this).select();
});
Right before applying knockout binding, I can check my dom elements and all events are there:
But when I run the applyBindings method to bind the viewmodel to my DOM, the "with" binding removes all events that are not related to knockout:
I have tried overwriting the cleanExternalData as explained on the documentation and on this answer. But that did not help with this, the function is replaced, but the events are still removed from the DOM when the templating is applied on the binding process.
For the record, this is not an exclusive behavior of the with function, but all anonymous templating functions also do that, foreach, if, ifnot. Using template, as expected, also behaves the same way. The DOM element is completely destroyed, stored as a template, then added again on my document when the condition is satisfied, but now without any jquery event handlers.
How to avoid that knockout removes the events from my DOM elements?
Instead of binding elements to a specific node, you can use a databinding to use the jquery on() functionality to handle events. Here's a binding I use:
define(['knockout'], function (ko) {
ko.bindingHandlers.eventListener = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var params = ko.utils.unwrapObservable(valueAccessor());
if (!(params instanceof Array)) {
params = [params];
}
params.forEach(function (param) {
$(element).on(param.event, param.selector, function (event) {
param.callback(ko.dataFor(this), ko.contextFor(this), event);
});
});
}
}
});
Usage:
<div data-bind="eventListener: [
{ event: 'click', selector: '.copyInclusionRule', callback: copyInclusionRule},
{ event: 'click', selector: '.deleteInclusionRule', callback: deleteInclusionRule}]">
... other knockout template stuff here ...
</div>
The above will listen for click events on either an element with the specified class and perform the callback when the event is received for anything within the div's 'scope'. The value of 'event' param can be anything that on() uses.
I think the reason why you can't leverage the cleanNode overrides is that your dom is being completely destroyed and re-created..at least that's my theory, if there was a way to get some kind of memory ID of the pre-applyBindings() dom elements and then after the applyBindings is called, are those new nodes? If they are new nodes, it's not something you can't fix with cleaning, those nodes are gone.
Alright, here is how I fixed my problem and I hope this can clarify things to others that don't want to destroy their DOM as well. If you don't want that knockout to destroy your DOM, that is possible since version 2.2. And thus, destroying the DOM when that is not necessary is not intended behavior and can be avoided.
I had tried several bidings created by Michael Best before, like his using binding that will come in knockout 3.5, and let or withLight (which became using now). None really worked. These simplified bidings would load the initial object, but not update the dom when this object properties had changed.
But this helped me to figure out what I am doing wrong. When I wanted to update my observable object, I was using myViewModel.observableObject(NewObject), like the documentation told me to do:
To write a new value to the observable, call the observable and pass the new value as a parameter. For example, calling myViewModel.personName('Mary') will change the name value to 'Mary'.
But I wasn't passing a single property's value, I was passing a new object that had the same structure (same properties). And this triggered knockout that the old object was destroyed (and thus, falsy for a second) and a new object took its place, even though all properties are there, they just got different values. Unlike the documentation told me, it didn't simply changed the value, but changed the entire object.
To go around this, instead of doing that, First, I had to initiate my viewModel with this object already created, using dummy data, this makes knockout not destroy the DOM when applyBindings is called. Then, when I want my object to update, I replaced the value of each property of the observable object to have the value of the new object. This didn't destroy the object and knockout updated my binding properly.
myViewModel.setSelectedItem = function setSelectedItem (newObject)
{
for (var prop in myViewModel.myObservableObject())
myViewModel.myObservableObject()[prop](newObject[prop]);
}
The with binding still killed some of my events (my angular ng-change for one of my components, for instance), but it kept all jquery events in there (which is great). And the using binding didn't kill any of my events at all (which is even better).

'this' inside View.render() being set to the model

I am new to Backbone.js
I must be doing something wrong here. I'm trying to make a little demo to see what I can do with Backbone and basing it off some sample code.
I an get "Uncaught TypeError: Cannot call method toJSON of undefined".
I see why it is doing this, because the .bind("change", taskView.render) call is setting the context to the model (which the alert confirms) but it seems like there should at least be an argument to the render function to get access to the view. Maybe I am just going about it the wrong way? (see the sample code below).
task.bind("change", _.bind(taskView.render, taskView));
On Backbone Views and Models, the 'this' context for 'bind' is the calling object, so for model.bind('change', ...) this with be the model. For view.bind(... this will be the view.
You are getting the error because task.bind("change", _.bind(taskView.render, taskView)); sets this to be task when render runs, but this actually needs to be taskView.
Since you want to bind the model event to the view, as irvani suggests, the best way to do this is to use the .listenTo method (see: http://backbonejs.org/#Events-listenTo)
taskView.listenTo(task, 'change', taskView.render);
Depending on how you want the view lifecycle to work, you could also put the code in the initialize method of the view, which executes when the view is created. Then use stopListening in the remove method, to clear up the listener when the view is no longer in use.
As the task model is passed into the view, you get a fairly neat:
AskView = Backbone.Model.extend({
initialize: function(){
this.listenTo(this.model, 'change', this.render);
},
...
remove: function(){
this.stopListening(this.model);
...
}
});
var askView = new AskView({ model: task });
However, the one line solution to your problem is:
task.on("change", taskView.render, taskView);
bind is just an alias of on (see: http://backbonejs.org/#Events-on). The first argument is the event you are listening to, the second is the method to fire, and the third argument is the context to bind to, in this case, your taskView.
task.listenTo(model, 'change', taskView.render);
This says that listen to the model change and render the taskView on every change.
As irvani suggested, use listenTo instead.
object.listenTo(other, event, callback) Tell an object to listen to a particular event on an other object. The advantage of using this
form, instead of other.on(event, callback, object), is that listenTo
allows the object to keep track of the events, and they can be removed
all at once later on. The callback will always be called with object
as context.
In your case,
taskView.listenTo(task,"change",taskView.render);
assuming taskView is Backbone.View and task is a Backbone.Model.
The chances of memory leaks will be less when you use listenTo compared to using on.
If you must use on, you can specify the context as a third argument as and mu is too short suggested:
To supply a context value for this when the callback is invoked, pass the optional last argument: model.on('change', this.render, this) or model.on({change: this.render}, this).
In your case:
task.on("change", taskView.render, taskView);

How to use jQuery in a Knockout.js Template?

I'm trying to target an element using jQuery which is embedded in one of my knockout templates:
<script type="text/html" id="video-file-template">
<div class="video" data-bind="attr: { 'data-index': $index }">
</div>
</script>
Yet, when I attempt to select $('.video') using jQuery, wrapped in a document ready function, I get an object with a length of 0 returned:
$(document).ready(function() {
console.log($('.video')); // Returns an object with a length of 0
});
Why is this? Is it because the element is not part of the DOM when my jQuery script is evaluated? If so, how can I target the element when it is loaded into the DOM via Knockout.js?
It's true that the document is ready before ko.applyBindings finishes, so that's why you're not seeing the element. However, you should not be using jQuery to violate the boundary between your view model and the DOM like that. In knockout, the way to accomplish what you need is with custom bindings.
Basically, you define a new knockout binding (like text, value, foreach, etc) and you have access to an init function, which fires when the element is first rendered, and an update function, which fires when the value you pass to the binding is updated. In your case, you would only need to define init:
ko.bindingHandlers.customVideo = {
init: function (element) {
console.log(element, $(element)); // notice you can use jquery here
}
};
And then you use the binding like this:
<div data-bind="customVideo"></div>
Perhaps it's better to add the "video" class and do other initialization right in the init callback:
ko.bindingHandlers.customVideo = {
init: function (element) {
$(element).addClass('video');
}
};
If this feels a little wonky at first, remember there's a very good reason for the indirection. It keeps your view model separate from the DOM it applies to. So you can change the DOM more freely and you can test the view model more independently. If you waited for ko.applyBindings to finish and called some jQuery stuff after that, you'd have a harder time testing that code. Notice that knockout custom bindings are not "special" in any way, and you can see that the built in bindings are defined exactly the same: https://github.com/knockout/knockout/tree/master/src/binding/defaultBindings
As the previous comments have suggested, it's because your $(document).ready fires before your knockout templates have been rendered.
Whenever I need to do this sort of thing I tend to have an 'init' (or whatever) function on my ko view model that I call after applyBindings has completed;
So:
var ViewModel = function(){
var self=this;
//blah
self.init = function(){
//jquery targeting template elements
}
}
var vm = new ViewModel();
ko.applyBindings(vm);
vm.init();

How do you call a function from a jQuery controller?

I'm using JavascriptMVC and have a controller of the form
$.Controller.extend('AppName.Controllers.ControllerName',
{
onDocument: true
}
{
initControllerName: function() {
...
},
testFucntion1() {
alert('yeah!!');
}
});
and I'd like to be able to call the function testFunction1() from the page generated by my view.
I found this question which seems to be asking the same thing, but I wasn't able to figure it out with the answer provided there.
I've tried
$('#controllername').testFunction1();
$('#ppame_controllername').testFunction1();
$('#ppame_controllers.controllername').testFunction1();
without success.
Thanks for your help!!
Martin Owen's answer is accurate except I found app_name_controller_name confusing at first.
A real example would be:
if your controller is defined like
$.Controller.extend('Layout.Controllers.Page',
...
then use
$(document).layout_page("testFunction1");
Layout = app_name
Page = controller_name
You can call your function with:
$(document).app_name_controller_name("testFunction1");
If you want to pass arguments to your function specify them after the function name:
$(document).app_name_controller_name("testFunction1", "Hello World");
The onDocument: true in the static section of your controller definition means that it is automatically attached to the document element, so that is how you get an instance of it. If you want to bind it to something else remove onDocument: true and use something like:
$('#main').app_name_controller_name();
That will create an instance of your controller and attach it to the $('#main') element. That element is then available in the controller's methods via this.element.
I don't know your situtation but you shouldn't really need to call controller methods very often - the controller should bind to events that are triggered by DOM elements and published by models. JMVC makes it very easy to bind controller methods to events: Listening To Events

Backbone.js binding a change event to a collection inside a model

I'm pretty new to Backbone so excuse me if this question is a little obvious.
I am having problems with a collection inside of a model. When the collection changes it doesn't register as a change in the model (and doesn't save).
I have set up my model like so:
var Article = Backbone.Model.extend({
defaults: {
"emsID" : $('html').attr('id')
},
initialize: function() {
this.tags = new App.Collections.Tags();
},
url: '/editorial_dev.php/api/1/article/persist.json'
});
This works fine if I update the tags collection and manually save the model:
this.model.tags.add({p : "a"});
this.model.save();
But if the model is not saved the view doesn't notice the change. Can anyone see what I am doing wrong?
initialize: function() {
this.tags = new App.Collections.Tags();
var model = this;
this.tags.bind("change", function() {
model.save();
});
},
Bind to the change event on the inner collection and just manually call .save on your outer model.
This is actually an addendum to #Raynos answer, but it's long enough that I need answer-formatting instead of comment-formatting.
Clearly OP wants to bind to change and add here, but other people may want to bind to destroy as well. There may be other events (I'm not 100% familiar with all of them yet), so binding to all would cover all your bases.
remove also works instead of destroy. Note that both remove and destroy fire when a model is deleted--first destroy, then remove. This propagates up to the collection and reverses order: remove first, then destroy. E.g.
model event : destroy
model event : remove
collection event : destroy
collection event : remove
There's a gotcha with custom event handlers described in this blog post.
Summary: Model-level events should propagate up to their collection, but can be prevented if something handles the event first and calls event.stopPropagation. If the event handler returns false, this is a jQuery shortcut for event.stopPropagation();
event.preventDefault();
Calling this.bind in a model or collection refers to Underscore.js's bind, NOT jQuery/Zepto's. What's the difference? The biggest one I noticed is that you cannot specify multiple events in a single string with space-separation. E.g.
this.bind('event1 event2 ...', ...)
This code looks for the event called event1 event2 .... In jQuery, this would bind the callback to event1, event2, ... If you want to bind a function to multiple events, bind it to all or call bind once for each event. There is an issue filed on github for this, so this one will hopefully change. For now (11/17/2011), be wary of this.

Categories