Using backbone.js I'm trying to fetch a model from my server and based on that, render an underscore template. I first tried it without using the result of the api call using the following render function:
render: function(options) {
this.ticketId = options.ticketId;
var that = this;
var ticketModel = new TicketModel({'id': that.ticketId});
$.when(ticketModel.fetch()).done(function(){
console.log(ticketModel); // this outputs the correct model
});
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
this.$el.html(template);
},
This works perfectly well. So I tried using the result of the api call to render the template:
render: function(options) {
this.ticketId = options.ticketId;
var that = this;
var ticketModel = new TicketModel({'id': this.ticketId});
$.when(ticketModel.fetch()).done(function(){
console.log(ticketModel);
console.log($('#tab-content-template').html());
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
this.$el.html(template);
});
},
but unfortunately, this results in an error saying
Uncaugt TypeError: Cannot read property 'html' of undefined.
The weird thing is that it outputs the html in the console correctly resulting from console.log($('#tab-content-template').html());. The error I get is on the line which reads this.$el.html(template);
How can it be that it first is able to get the html(), and after that it says it can't find the property html? I'm totally stuck here.. :S
All tips are welcome!
Your issue is this no longer refers to what you think it refers to. in your code you have placed
var that = this;
this is a common pattern/trick to allow the closure to retain the context of "this" when being executed. Inside your closure "that" would now refer to what you think "this" should refer to.
Try changing "this" to "that"
$.when(ticketModel.fetch()).done(function(){
console.log(ticketModel);
console.log($('#tab-content-template').html());
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
that.$el.html(template);
});
I usally make use of jQuery's proxy function that ensures when a function is executed you can be confident of the context in which it is running
$.when(ticketModel.fetch()).done($.proxy(function(){
console.log(ticketModel);
console.log($('#tab-content-template').html());
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
this.$el.html(template);
},this));
oh your other question about why is $('#tab-content-template').html() working, this is because you are using JQuery directly which is in the global namespace so there for accessible where as $el is a property of your view so if you can't access your view you can't access the property.
See http://backbonejs.org/#Model-fetch - in options argument it accepts success and error callbacks:
ticketModel.fetch({
success: function(model, response, options) {
// render the template
}
});
Also if you need to use a context of current view object in this callback you can use Underscore/Lo-Dash _.bind() function to pass the context:
ticketModel.fetch({
success: _.bind(function(model, response, options) {
// Render the template using some method of the view:
this.renderMyTemplate(model);
}, this)
});
or just pass the method itself:
ticketModel.fetch({
success: this.renderMyTemplate,
error: this.handleFetchError
});
You don't need a $.when here, backbone now returns a promise for fetch call. Below code should work for you. Also consider compiling template outside render function. Compiling template is bit heavy task, should be cached once done.
var _this = this;
ticketModel.fetch().done(function () {
var template = _.template($('#tab-content-template').html(), {ticketId: that.ticketId});
_this.$el.html(template);
});
Related
I'm seeing the success callback but in the debugger Chrome Dev Tools, I'm still seeing the model when I type in this.model. I know that it's destroyed on the server side, but can you explain why it's still attached to the view? How do I get rid of it?
delete: function () {
this.model.destroy({success: function () {console.log("success in destroy");}});
debugger;
}
What you are seeing is correct. Looking at the documentation on model.destroy (or looking to the code) we can see it basically does two things:
HTTP DELETEs the server representation of the model
Removes the model from any containing collections
Note that nothing happens to the model itself or to any objects the model may be attached to.
We can see this behavior with a simple example:
var foo = new Backbone.Model({foo: 'bar'});
var foos = new Backbone.Collection([foo]);
var fooView = new Backbone.View();
fooView.model = foo;
foo.destroy({success: function () {
console.log('success');
}});
console.log(foo, foos, fooView);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone.js"></script>
Note that nothing happens to foo or fooView.model after executing this code, although foos no longer contains an instance of foo.
Removing view.model
If you want to remove the model from the view you can leverage the success callback. Just change your view's delete method (from your question) to something like the following:
delete: function () {
this.model.destroy({success: function () {
delete this.model;
}.bind(this)});
}
Alternatively, since we know from the docs that the model will fire a "destroy" event, we can also listen for that event and fire a callback that deletes our model.
I have a big project written in ASP.NET I need to modify. It uses knockout.js to bind values:
$(document).ready(function () {
var model = new MyViewModel();
ko.applyBindings(model);
});
Afterwards, the bindig is done:
<span data-bind="text: myText"></span>
I now want to access all values that were bound. Unfortunately, I'm not very experienced with those frameworks.
With
console.log(model);
I get a huge output (extract):
MyViewModel{settings: [...]}
TypeSeriesSubscription: c()
barClick: (e, bar)
cancelSetting: ()
chartClick: (value)
chartUser: c()
charts: c()
[...]
With
model.myText
I get just undifined.
How can I access myText in order to just put its text out in a console?
You're scoping model to only exist inside of the ready function. Try this:
var model = new MyViewModel();
$(document).ready(function () {
ko.applyBindings(model);
});
Then you should be able to:
console.log(model.myText());
Since KO relies on an observable pattern, you need to use () after any property to see its current value.
I have the following ViewModel.
function PageSectionVM(pageSection) {
var self = this;
self.SectionName = ko.observable();
self.Markup = ko.observable();
self.update(pageSection);
}
and I have also created the update method as called in the above constructor function.
PageSectionVM.prototype.update = function (pageSection) {
var self = this;
pageSection = pageSection || {};
self.SectionName(pageSection.SectionName);
self.Markup(pageSection.Markup);
};
This is bundled off in its own file and I would like to reuse this VM in several pages. On one particular page I would like to 'extend' this viewmodel to include a new function. I have tried do this by adding a new function to PageSectionVM's prototype, like so.
PageSectionVM.prototype.tabName = function () {
var self = this;
return "#tab-" + self.SectionName();
};
If I then add this as a knockout binding statement, it returns the text of the function as opposed to the function result. I get the feeling I am missing something. If I add tabName as a computedObservable in the original viewmodel it works, but that means I am putting specific code for a single purpose in my 'general' viewmodel's code (something I'd like to avoid).
The knockout binding statement I am using is
<a data-bind="attr:{href: tabName}, text:SectionName"></a>
This sits inside a foreach binding on an observableArray of PageSectionVMs. The text property is fine, but the href ends up containing the literal text of the function, not its result.
Any help greatly appreciated.
If I then add this as a knockout binding statement, it returns the text of the function as opposed to the function result.
Of course it does. Knockout bindings work like this:
check if the bound value is an observable
if yes, unwrap it (i.e. "execute it")
convert to string what remains and use it in your view
NB: All observables are functions, but not all functions are observables.
That means you will get the function text if your binding looks like text: SectionName when SectionName() is a plain old function.
Because of the way observables work (w/r/t dependency tracking & this handling) you cannot use them in a prototype, they have to live in the instance.
That means:
either you put them in the instance (like #WayneEllery suggests)
or you call them yourself in the view (text: SectionName())
or you use ko.utils.extend() to extend pre-existing PageSectionVM instances with extra observables/computeds.
I don't understand why you have a problem using a computed. If you want to use prototype you can do it as follows:
I've also added url encoding and initilized the model.
function PageSectionVM(pageSection) {
var self = this;
self.SectionName = ko.observable(pageSection.SectionName);
self.Markup = ko.observable(pageSection.Markup);
self.TabName = ko.computed(this.getTabName, self);
};
PageSectionVM.prototype.getTabName = function () {
var self = this;
return "#tab-" + encodeURIComponent(self.SectionName());
};
http://jsfiddle.net/5vUhe/
We have a JS framework that lets us set up "modules". Each module is added by calling the addModule method and passing a literal object that contains required properties about the module as well as optional methods. Example:
framework.addModule({
id: "test-module",
init: function () {
//stuff to do when initializing like set up jQuery bindings
$("div").click(function () {
// need access to literal object so I can call:
something.utility1();
});
},
utility1: function () {
something.utility2();
},
utility2: function () {
// need access to literal object so I can call:
}
});
I'm trying to figure out the easiest way to make the object itself available to any code, at any level, inside the object (in place of "something").
The best I've been able to do is to add a this: this property to the object and then inside of methods I can put var module = this, which works but requires that variable to be added to each module. I'd like to see if there's another way that wouldn't require adding a variable to each method. Thanks.
Thanks for the comments and, zzzzBov, thanks for your suggestions.
However, it looks like the below code will work best for my needs. The devs on my team are writing a lot of these modules and I need the solution to be clear to them. Having to call $.proxy could make it less clear. I was hoping to avoid having to put var module = this in each method, so it would be cleaner, but it seems that it's not possible without it.
framework.addModule({
id: "test-module",
init: function () {
var module = this;
$("div").click(function () {
module.utility1();
});
},
utility1: function () {
var module = this;
module.utility2();
},
utility2: function () {
}
});
If anyone has a cleaner solution, let me know.
jQuery has a proxy method which will bind the function to a specific context. This would turn your event binding into:
$('div').click($.proxy(this, 'utility1'));
Alternatively, instead of using an object literal to instantiate the module object, you could instantiate an anonymous function:
framework.addModule(new function () {
this.id = 'test-module';
this.init = function () {
$('div').click($.proxy(this, 'utility1'));
};
this.utility1 = function () {
...more code...
};
this.utility2 = this.utility1;
});
I'm not a newbie to JavaScript, but often I find its flexible ways (like defining anonymous function as callbacks and their scope) quite confusing. One thing I'm still struggling with are closures and scopes.
Take this example (from a Backbone model):
'handleRemove': function() {
var thisModel = this;
this.view.$el.slideUp(400, function() { thisModel.view.remove(); });
},
After the model is removed/deleted, this will animate its view and finally remove it from the DOM. This works just fine - but initially I tried the following code:
'handleRemove': function() {
var thisModel = this;
this.view.$el.slideUp(400, thisModel.view.remove );
},
Which is basically the same, but without the function() {} wrapper for the remove() call.
Can someone explain why the latter code does not work? I get the following exception/backtrace:
Uncaught TypeError: Cannot call method 'remove' of undefined backbone-min.js:1272
_.extend.remove backbone-min.js:1272
jQuery.speed.opt.complete jquery-1.8.3.js:9154
jQuery.Callbacks.fire jquery-1.8.3.js:974
jQuery.Callbacks.self.fireWith jquery-1.8.3.js:1084
tick jquery-1.8.3.js:8653
jQuery.fx.tick
Thank you!
That's because the context (this) of the callback function changes to the element being animated by jQuery.
var obj = { fn: function() { // Used as below, the following will print:
alert(this === obj); // false
alert(this.tagName); // "DIV"
}};
$('<div>').slideUp(400, obj.fn);
Furthermore, Backbone's view.remove function looks like this (source code):
remove: function() {
this.$el.remove();
this.stopListening();
return this;
},
Because this is not the Backbone view object any more, $el is not defined. Hence you get the "Cannot call method 'remove' of undefined" error.
Another way to avoid the error is to use Underscore's _.bind method:
this.view.$el.slideUp(400, _.bind(thisModel.view.remove, this) );