Access all bound values - javascript

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.

Related

How do I get rid of model attached to view after using Backbone's model#destroy?

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.

Render backbone template when myModel.fetch() is done?

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);
});

Add computedObservable to existing javascript view model

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/

Knockout: Can't parse binding

I'm having a problem with Knockout. It is telling me that it can't parse a specific binding. If I don't include the binding in my HTML, the page compiles and I can run the event handler manually (in the console). Here is what the binding looks like:
<!-- ko foreach: problems -->
<li data-bind="css: {active: $root.current.number() == $data.number() }">
<a data-bind="click: $root.setCurrent( $data.number ), text: $data.number">
<!-- /ko -->
The error I get is the very unhelpful
"Unable to parse bindings.
Bindings value: click: $root.setCurrent( $data.number ), text: $data.number
Message: undefined is not a function"
Of course, $data is very clearly not undefined. If I remove the call to setCurrent, the bindings compile file, and I can run setCurrent in the console (and it works).
The setCurrent handler is defined in my view model as:
var ViewModel = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
}
$('document').ready( function () {
$.getJSON("#{PracticalWriteR}", function (data) {
viewModel = new ViewModel(data);
pager.extendWithPage(viewModel);
ko.applyBindings(viewModel);
pager.start();
viewModel.setCurrent = function (num) {
viewModel.problems()[viewModel.current.number() - 1].response(viewModel.current.problem.response());
viewModel.problems()[viewModel.current.number() - 1].marked(viewModel.current.problem.marked());
var neu = viewModel.problems()[num - 1];
viewModel.current.number(num);
viewModel.current.problem.answers(neu.answers());
viewModel.current.problem.response(neu.response());
viewModel.current.problem.practical.key(neu.practical.key());
viewModel.current.problem.practical.value.question(neu.practical.value.question());
viewModel.current.problem.practical.value.solution(neu.practical.value.solution());
viewModel.current.problem.marked(neu.marked());
viewModel.current.problem.number(neu.number());
};
});
});
The idea is that setCurrent takes an int, which is a "question number" and changes first updates the current-current question, and then changes the current question to a new question (whose number is the original argument). Sorry for the noise here.
When you bind a function to an event in knockout you generally call the function without parenthesis (similar to how you would do using addEventListener).
Knockout automatically provides the current model object as the first parameter to the function, and then the actual DOM event that activated the binding as the second parameter.
Your code should work if you just change it to:
<a data-bind="click: $root.setCurrent, text: $data.number">
Then change your setCurrent function to accept the Object wrapping the number as its parameter.
You can modify this behavior by using an anonymous function if you want to alter the order or way in which parameters are passed... for your case you could do something like
<a data-bind="click: function(data, event) { $root.setCurrent($data.number) }, text: $data.number">
There is also yet another method, using the myFunction.bind(param1, param2, ...) syntax if you don't like the anonymous function.
See the knockout docs on click binding for more info: http://knockoutjs.com/documentation/click-binding.html

Replacing an Associative Array Variable

Here is the following problem.
My javascript file contains the following code...
$(function() {
var names;
var names_hash = { };
// When an user types in a letter in the student name input field
$(".container").on("keyup", "#term", function(){
// Here we are submitting the form via AJAX. The form contains necessary
// Rails code to initiate the AJAX
$(this).parents(".student-search-form").submit();
});
$(".container").on("click", ".add_nested_fields", function() {
var term = $("#term").val();
console.log(names_hash);
});
});
Now when I enter a character into the input field with the id term the Rails controller action is called and responds with the following javascript.
FYI, #group_users_hash is just a Ruby hash and I have to call html_safe so it can be properly converted into a Javascript associative array(yes, I know it's really just an object).
names_hash = <%= #group_users_hash.html_safe %>;
console.log(names_hash);
So when AJAX finishes, I see that console.log(names_hash); has produced on my console
Object {1: "Jason"}
But when I click on the .add_nested_fields calling the on event, my console log displays
Object {}
I don't know why assoc_array isn't being updated. What's wrong with my code and how do I fix it?
It looks like your names_hash (which you redefine in your return anyways via var names_hash and then shadow any higher scoped variables) is not in the same scope as the the one in which you are priting it on click. Thanks to your wrapper function. You're going to need some way - via events or callbacks, etcetera - to access the variable within that scope and set it to the value you wish to use.
EDIT
A non-ideal but sample solution to further explain woudl be:
var names_hash = {};
$(function() {
$(".container").on("click", ".add_nested_fields", function() {
console.log(names_hash);
});
});
And then when your response comes in you simply:
names_hash = <%= #group_users_hash.html_safe %>;
And then you should be golden. The reason this is not an ideal is because you have your names_hash floating in global scope. It's best to "namespace" such as:
(function() {
// prevent redefining it if it's already defined via another file
var MyNamespace = window.MyNamespace || {};
MyNamespace.names_hash = {};
window.MyNamespace = MyNamespace;
})();
$(function() {
$(".container").on("click", ".add_nested_fields", function() {
console.log(MyNamespace.names_hash);
});
});
And from there you modify your response to:
MyNamespace.names_hash = <%= #group_users_hash.html_safe %>;
And viola. You've protected global scope and you've also publicized the functions you want to access outside of the wrapper function.

Categories