I think the way i put the question was misleading you, so i've made a major edit.
I will use code from knockoutjs tutorial
"Loading and saving data", step 3
I've made one change to show what i mean.
function Task(data) {
this.title = ko.observable(data.title);
this.isDone = ko.observable(data.isDone);
}
function TaskListViewModel() {
// Data
var self = this;
self.tasks = ko.observableArray([]);
self.newTaskText = ko.observable();
self.incompleteTasks = ko.computed(function() {
return ko.utils.arrayFilter(self.tasks(), function(task) { return !task.isDone() });
});
// Operations
self.addTask = function() {
self.tasks.push(new Task({ title: this.newTaskText() }));
self.newTaskText("");
};
self.removeTask = function(task) { self.tasks.remove(task) };
// ** Read this!!
// This below is the original code in the tutorial, used to fetch remote data.
// I commented this out, and i will use the below load() method instead.
// **
//$.getJSON("/tasks", function(allData) {
// var mappedTasks = $.map(allData, function(item) { return new Task(item) });
// self.tasks(mappedTasks);
//});
// This is the load method to emulate the above commented
// $.get. Please, DO NOT CARE about the implementation, or
// the setTimeout usage, etc., this method ONLY EXISTS TO
// EMULATE A SLOW SERVER RESPONSE.
// So, we have to ways of using it:
// - load('slow'), will set result after 1 second
// - any other argument will set result instantly.
self.load = function(howFast) {
if (howFast == 'slow') {
setTimeout(function(){
mappedTasks = [];
mappedTasks.push(new Task({
title: 'Some task slowly loaded from server',
isDone: false
}));
}, 1000);
} else {
mappedTasks = [];
mappedTasks.push(new Task({
title: 'Some task quick!',
isDone: false
}));
}
}
// Now please note this:
// - if i use load(), mappedTask is instant updated and
// everything runs fine
// - if i use load('slow'), mappedTask is updated AFTER
// VALUES ARE BOUND, so if you open your browser console
// you will see an "Uncaught ReferenceError: mappedTasks is not defined" error.
self.load();
self.tasks(mappedTasks);
}
ko.applyBindings(new TaskListViewModel());
PROBLEM: Bindings are applied AFTER the ViewModel has done with initializazions, therefore causing errors. I think i provided enough detail in code comments, ask me if you think you need more. Anyway, i am little amazed that nobody ever hit this thing before, so am i missing something VERY important here?
QUESTION: How to avoid this?
Fiddle
You have an html error.
Your setTimeout function is attempting to programmatically set the selected option to an option that isn't in the list. The browser can't do that and so the selection stays where it is.
http://jsfiddle.net/UD89R/6/
function ViewModel() {
// Setup something.
var self = this;
self.foo = ko.observable();
self.options = ko.observableArray([{id:1, name:'Homer'}, {id:2, name:'Barney'}]);
// Make a lot of async data load, like
// $.get('this', function(){ /* Process data */ });
// $.get('that', anotherHandler);
// $.get('somethingElse', self.someObservable);
// Assume the whole process would take 1 second.
setTimeout(function(){
self.options.push({id: 3, name: 'Grimes'});
self.foo(3);
// Too late! foo has been overriden by the select-options binding,
// so this one will not log 'Grimes' as expected.
console.log('Loading done, check foo value:' + self.foo());
}, 1000);
}
Related
I know there's a lot of questions on Stack about JS Scope... but I ran into a specific problem that I'm unable to wrap my head around. I have a Javascript module that looks something like this (albeit dramatically simplified):
module.exports = {
$company: $('#id_company'),
$companyCtrl: null,
$jobType: $('#id_job_type'),
$jobTypeCtrl: null,
init: function() {
var _this = this;
this.$companyCtrl = this.$company.selectize({
onChange: function(value) {
_this.companyChanged(value);
}
})[0].selectize;
},
companyChanged: function() {
// Company changed has been fired and does a few things
// before it calls this:
this.updateJobType();
},
updateJobType: function() {
var _this = this;
$.ajax({
url:'/ajax-url',
data: {
'id': this.companyID
}
})
.done(function(data) {
// If our job type selectize() instance hasn't been setup,
// then create it now
if (_this.$jobTypeCtrl === null) {
// ------------
// PROBLEM BLOCK
_this.$jobTypeCtrl = _this.$jobType.selectize({
onChange: function(value) {
if (_this.currentModel !== 'wire_add') {
_this.jobTypeChanged(value);
}
}
})[0].selectize;
// ------------
}
// Reload and re-enable input
_this.$jobTypeCtrl.reloadFromOriginalInput();
_this.$jobTypeCtrl.enable();
});
},
}
Now, here's what I don't understand, if I move that "PROBLEM BLOCK" outside of the Ajax call, and put it back up into init(), it works fine. However, as far as I can tell, in it's current location, the scope (_this = this) is the exact same as it would be up in the init function.
And to be more specific, the problem I'm experiencing is that the "onChange" handler never fires when the code is inside of the Ajax handler, but the plugin instance is still created and functions as it otherwise should. However, if I move it up to the init(), the onChange handler fires without any other changes to the code
Any help to get me to wrap my head around this would be greatly appreciated. Thank you!
I had a similar issue, where you start chasing your own tail using objects.
The power of using modules, is that they have their own context. So once compiled, the file knows what vars and funcs are residing inside; this negates the need to track this bouncing from function to function, which becomes a nightmare, once you involve async callbacks.
I recommend rewriting your module with vars at the top and functions, so it's easier to call any function without trying to pass the correct _this/self context from here, there and everywhere.
Here's an untested re-write:
module.exports = {
var $company = $('#id_company'),
$companyCtrl = null,
$jobType = $('#id_job_type'),
$jobTypeCtrl = null;
function init() {
$companyCtrl = $company.selectize({
onChange: function(value) {
companyChanged(value); // <== invoke any function and treat them as black-box code
}
})[0].selectize;
}
function companyChanged() {
// Company changed has been fired and does a few things
// before it calls this:
updateJobType();
}
function updateJobType() {
$.ajax({
url:'/ajax-url',
data: {
'id': companyID
}
})
.done(function(data) {
// If our job type selectize() instance hasn't been setup,
// then create it now
if ($jobTypeCtrl === null) {
// ------------
// PROBLEM BLOCK
$jobTypeCtrl = $jobType.selectize({
onChange: function(value) {
if (currentModel !== 'wire_add') {
jobTypeChanged(value);
}
}
})[0].selectize;
// ------------
}
// Reload and re-enable input
$jobTypeCtrl.reloadFromOriginalInput();
$jobTypeCtrl.enable();
});
}
}
I've been working on writing a custom jquery plugin for one of my web applications but I've been running into a strange error, I think it's due to my unfamiliarity with object-oriented programming.
The bug that I've been running into comes when I try to run the $(".list-group").updateList('template', 'some template') twice, the first time it works just fine, but the second time I run the same command, I get an object is not a function error. Here's the plugin code:
(function($){
defaultOptions = {
defaultId: 'selective_update_',
listSelector: 'li'
};
function UpdateList(item, options) {
this.options = $.extend(defaultOptions, options);
this.item = $(item);
this.init();
console.log(this.options);
}
UpdateList.prototype = {
init: function() {
console.log('initiation');
},
template: function(template) {
// this line is where the errors come
this.template = template;
},
update: function(newArray) {
//update code is here
// I can run this multiple times in a row without it breaking
}
}
// jQuery plugin interface
$.fn.updateList = function(opt) {
// slice arguments to leave only arguments after function name
var args = Array.prototype.slice.call(arguments, 1);
return this.each(function() {
var item = $(this), instance = item.data('UpdateList');
if(!instance) {
// create plugin instance and save it in data
item.data('UpdateList', new UpdateList(this, opt));
} else {
// if instance already created call method
if(typeof opt === 'string') {
instance[opt](args);
}
}
});
}
}(jQuery));
One thing I did notice when I went to access this.template - It was in an array so I had to call this.template[0] to get the string...I don't know why it's doing that, but I suspect it has to do with the error I'm getting. Maybe it can assign the string the first time, but not the next? Any help would be appreciated!
Thanks :)
this.template = template
Is in fact your problem, as you are overwriting the function that is set on the instance. You end up overwriting it to your args array as you pass that as your argument to the initial template function. It basically will do this:
this.template = ["some template"];
Thus the next time instance[opt](args) runs it will try to execute that array as if it were a function and hence get the not a function error.
JSFiddle
I apologize if this is a duplicate. I've tried finding a solution for this exact problem but couldn't come up with anything.
Here I have an Object literal:
var Facets = {
searchresults: {},
init: function() {
$('.class').on("click", function(e){
e.preventDefault();
Facets.searchAll();
Facets.getFacets();
});
},
searchAll: function(){
// Some ajax code
this.searchresults = data;
},
getFacets: function(){
$('.somenode').each(function(){
var url = this.makeURL(somedata);
});
},
makeURL: function(){
// creates URL
}
};
Facets.init();
The problem is when the method getFacets calls this.makeURL, I get an error. I'm not entirely sure why because I thought the call to getFacets has the context of the Facets object.
The way I understand it is that when the event handler is triggered, it has the context of the window. Therefore, I call Facets.getFacets and Facets.searchAll so that those functions will know what to call. But I know I'm not understanding something correctly...
I've also tried Facets.getFacets().call(this) to get the correct context, but that didn't work.
You can capture the scope like this:
var Facets = {
searchresults: {},
init: function() {
$('.class').on("click", function(e){
e.preventDefault();
Facets.searchAll();
Facets.getFacets();
});
},
searchAll: function(){
// Some ajax code
this.searchresults = data;
},
getFacets: function(){
var self = this;
$('.somenode').each(function(){
var url = self.makeURL(somedata);
});
},
makeURL: function(){
// creates URL
}
};
Facets.init();
I fear this is something as embarrassing as a typo, but since I´m stuck on this and quite desperate I´m willing to pay with pride. ;)
This is my case:
Task = function (data) {
var self = this;
self.TaskId = data.TaskId;
self.TaskName = ko.observable(data.TaskName);
}
ViewModel = function () {
var self = this;
self.Tasks = ko.observableArray();
self.SelectedTask = ko.observable();
}
$.getJSON("/myService/GetAllTasks",
function (tData) {
var mappedTasks = $.map(tData, function (item) {
return new Task(item)
});
self.Tasks(mappedTasks); // Populate Tasks-array...
});
self.newTaskItem = function () {
var newitem = new Task({
TaskId: -1,
TaskName: "enter taskname here"
});
self.Tasks.push(newitem); // THIS ONE CRASH
self.Tasks().push(newitem); // BUT SUBSTITUTED WITH THIS ONE IT RUNS ON...
self.editTaskItem(newitem);
};
self.editTaskItem = function (item) {
self.SelectedTask(item); // UNTIL TIL LINE WHERE IT CRASHES FOR GOOD...
self.showEditor(true); // makes Task-edior visible in HTML
};
I also hava an "self.SelectedTask.subscription" in my file, but leaving it out of the code makes no difference.
I also should mention that my database table is empty, so the getJSON returns no data to the mappedTasks, leaving self.Tasks() = [ ] (according to Firebug)
I have fixed the incorrectly closed tags in my code.
Part 2:
Decided after a while to redo my code from the starting point. It got me one step further.
The code now stops on the second of these lines (in "self.newTaskItem"):
self.Tasks.push(newitem);
self.SelectedTask(newitem); // Here it fails.
These two observables are connected in my HTML like this:
<select data-bind="options: Tasks, optionsText: '$root.TaskName', value: SelectedTask"</select>
It looks like your ViewModel() function never gets closed. Add a closing } to wherever you want that function declaration to end. It looks to me (based on your formatting) that you want this:
ViewModel = function () {
var self = this;
self.Tasks = ko.observableArray();
self.SelectedTask = ko.observable();
}
Additionally, you need to close your$.getJson call with a );:
$.getJSON("/myService/GetAllTasks",
function (tData) {
var mappedTasks = $.map(tData, function (item) {
return new Task(item)
});
self.Tasks(mappedTasks); // Populate Tasks-array...
});
I am not 100% sure what your problem is or what error you are getting but this is what I would do - change your Task = function to function Task -
function Task(data) {
var self = this;
self.TaskId = data.TaskId;
}
By saying Task = function without using a var in front of it you are registering Task in the global namespace, not a good idea. Same thing with your view model... Fix it if you can still...
self.newTaskItem = function () {
var newitem = new Task({
// Your Task is looking for a TaskId, not a TextBatchId
TaskId: 1
});
self.Tasks.push(newitem);
self.editTaskItem(newitem);
};
Also, you are creating a TextBatchId where I think your Task object is looking for a TaskId. Fix that, or if you are doing it on purpose for some reason please show your view code and give a better explanation of what is going wrong and what errors you see.
(assuming the unclosed stuff isn't present in your real code)
In Task, TaskId isn't an observable, so when you set SelectedTask to a particular task your editor fields won't properly update (it's a fairly common mistake to assume that the elements of an observableArray are themselves observable, but they aren't unless you explicitly make them so).
We are creating a gadget for the opensocial API 0.7.
In some functions we have to decide, if the viewer is the owner.
We couldn't use the usual function for this purpose:
return gadgets.util.getUrlParameters().viewer == gadgets.util.getUrlParameters().owner;
so we had to create a workaround and get the information via a DataRequest.
The DataRequest calls a callback function and has no useable return value.
We tried a quick hack by using global variables to set the corresponding value.
The issue at this point is, that the function does not 'wait' for the callback-function to be finished. We know this is no good code/style at all, but we tried to force a timeout for debug reasons.
Handling all the code within the callback-function (as suggested in the examples of the opensocial docs) is not possible.
We are looking for something like a real 'sleep()' in JavaScript to wait for the callback-function to complete or another alternative to get the owner information about the viewer.
globalWorkaroundIsOwner = false;
function show_teaser(){
if (current_user_is_owner()){
// ...
}
// ...
}
function current_user_is_owner() {
var req = opensocial.newDataRequest();
req.add(req.newFetchPersonRequest(opensocial.DataRequest.PersonId.VIEWER), 'viewer');
// This will set the the correct value
req.send( user_is_owner_workaround );
// This is an attempt to delay the return of the value.
// An alert() at this point delays the return as wanted.
window.setTimeout("empty()", 2000);
// This return seems to be called too early (the variable is false)
return globalWorkaroundIsOwner;
}
function user_is_owner_workaround(dataResponse) {
var viewer = dataResponse.get('viewer').getData();
globalWorkaroundIsOwner = viewer.isOwner();
// value is correct at this point
}
Can you use an additional flag in order to indicate whether the remote query has already returned the required value?
var globalWorkaroundIsOwner = false;
var workaroundStarted = false, workAroundComplete = false;
var checker;
function show_teaser(){
if (!workaroundStarted) {
workaroundStarted = true;
current_user_is_owner();
}
if (workaroundComplete) {
if (globalWorkaroundIsOwner){
// ...
}
// ...
if (checker) {
clearInterval(checker);
}
}
}
function current_user_is_owner() {
var req = opensocial.newDataRequest();
req.add(req.newFetchPersonRequest(opensocial.DataRequest.PersonId.VIEWER), 'viewer');
checker = setInterval("show_teaser()", 1000);
// This will set the the correct value
req.send( user_is_owner_workaround );
}
function user_is_owner_workaround(dataResponse) {
var viewer = dataResponse.get('viewer').getData();
globalWorkaroundIsOwner = viewer.isOwner();
workAroundComplete = true;
// value is correct at this point
}