I'm developing a webpage that retrieves some database info via a (jquery) ajax call and then manipulates the data and displays it on the page via various javascript functions. I use a module pattern ('userapp' in the code example below) to avoid globals and to use the data by the various functions.
The simplified code below works, but only if I include all the functions (such test() in the code below) within the ajax call, and I think that's going to result in ugly complex code (also in line with some best javascript practises I read on the web). When I tried to call the example function test() outside /after the ajax call (embedded in init()), test() does not work as I think the ajax call has not completed setting the variables ('products[]' in example) yet.
My question: is it possible to call other functions outside the ajax call, and if so, how? I looked at callback examples but I'm not sure whether/how that would solve my problem...
simplified code:
userapp = function(){
//userapp properties
var today = new Date();
var products = [];
var copyItems = function(source_items, target_items){
//do something
};//var copyItems
var init = function(){
$.ajax({
url: "user_admin_get.php",
data: { command: "getuserbaseinfo", val1: "", val2: "" },
success: function (msg) {
copyItems(msg.eproducts,products); //set values for 'products' used by test()
test(); //calling test() here works as the the values has been set()
},//success: function(msg)
error: function(msg) {
console.log('error result from php file:');
},//error:
dataType: "json"
});//$.ajax({
};//var init = function(){
var test = function(){
//do something
};//test()
return{init:init, test:test} //return (only) public functions and vars
}(); //userapp()
//main function document ready
$(document).ready(function(){
userapp.init();
//userapp.test(); //test() is not working here as init() has not set the requirement parameters tey
}); //$(document).ready
You want to pass a callback to init and call test inside this callback.
var init = function(callback){
$.ajax({
....
success: function (msg) {
....
callback();
}
...
};
...
userapp.init(function() {
// user app is ready!
userapp.test();
});
Related
I'm trying to read data from a json file into JavaScript so it can be used by other functions that are called when a user interacts with the page. I've tried, using jQuery and JS:
var products = null;
$.getJSON("products.json", function(data) {
products = data;
console.log(data);
});
console.log(products);
Which produces, as expected:
null
Array [ {…}, {…} ]
I understand this is because of the asynchronous nature of the jQuery code execution. I've read into it a bit, but I'm just struggling to wrap my head around how to re-structure my code (coming from pretty much exclusively Python).
This is how I'm seeing it:
var products = null;
$.getJSON("products.json", function(data) {
products = data;
console.log(data);
});
function do_stuff(){
// Access the attributes of data and do stuff
}
function do_more_stuff(){
// Do some more stuff with the data
}
I would probably have do_stuff() execute when the page body loads, and let's say do_more_stuff executes when the user selects something in a dropdown menu. Both need to access the data in products.json. I know there are many examples out there, but I'm just not getting it. How can I re-structure the code to actually work?
I believe you are looking for something like this:
function get_json(cb) {
$.getJSON("products.json", function(data) {
cb(data);
});
}
function cb(data) {
// do stuff here if you want
console.log(data)
}
get_json(cb)
Create a callback function cb (or call it do_stuff if you'd like). Pass a reference to that function to your async function (get_json). When the async operation is complete, call your callback function with the data you received.
For our internal js framework, to ensure good usage of the framework, I want to forbid the usage of Ajax requests in some parts of the framework.
Is there a way to achieve something similar to this:
function doSomething() {
instructions with ajax calls...
withAjaxForbidden(function() {
instructions using ajax calls should raise exception here
});
instructions with ajax calls...
}
Is it possible to implement something like withAjaxForbidden?
Note that obviously I expect the ajax system to be leaved in a consistent state in case an error is thrown.
Just create your own ajax function and make it dependent on some global variable. After that you can just remove the regular ajax functions from the scope and you're done :)
var ajaxEnabled = true;
function withAjaxForbidden(f){
ajaxEnabled = false;
f();
ajaxEnabled = true;
}
function ajax(...){
if(!ajaxEnabled)throw 'Ajax requests are forbidden within this block';
...
}
Here's a fiddle as an example: http://jsfiddle.net/u98b7bk3/1/
<div id="console">
No messages yet
</div>
<script type="text/javascript">
// Disable Ajax globally, to keep it working for your own library make
// sure you save this locally before overwriting
XMLHttpRequest = undefined;
</script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script type="text/javascript">
// Try an ajax request with jQuery
$.ajax('/').always(function(data, status, error){
$('#console').html("data: " + data + "<br>status: " + status + "<br>error: " + error);
});
</script>
Local variables override global variables within the function's scope. Simply create a variable for each native and library ajax function and set it to undefined from within your protected function. It will be prevented from being able to make use of the functions and variables by the same name. As long as they are set using "var" it should only affect the function's scope and not the global scope.
Edit: This may not be exactly what you were looking for, but as far as I know the precise way you want to accomplish this is not possible due to variable scoping but if you don't mind the extra code around your AJAX-restricted code you can use this method. Unlike the other answer this will work asynchronously and does not affect global variables, which makes it much less likely that problems will occur from the use of this code:
Edit 2 (with jQuery support):
var testFunction = (function($){
// Global to local/lexical:
var XMLHttpRequest = undefined;
var eval = undefined;
var setTimeout = undefined;
var setInterval = undefined;
var Function = undefined;
var window = undefined;
$ = (function($){ if($) {
var newjq = function(s, c) {
// Reroute main function
return $(s, c);
};
var jQueryBlacklist = {
// Initialize blacklist
"ajax": true,
"post": true,
"get": true,
"getJSON": true,
"getScript": true
};
for(i in $) // Reconstruct Object
if($.hasOwnProperty(i)
&& !jQueryBlacklist[i])
newjq[i] = $[i];
return newjq;
} }($));
// Real testFunction() below:
return function() {
// AJAX-forbidden code
// $.ajax should be undefined
}
}($));
// $.ajax should work normally here
testFunction(); // not in here
// $.ajax should work normally here
Click here to see the fiddle
http://jsfiddle.net/FZ6K6/24/
I have a button (Remove inputs) with enable and css bindings that are returned when an observable array contains more than 2 items.
<button data-bind="click: removeInput, enable: Integers().length >2, css { red: Integers().length >2 }">Remove Input</button>
I also have a function (loadIntegerSorter) that sets the observable array to contain 2 items.
self.loadIntegerSorter = function () {
self.Integers([new integer(0, 0, 0), new integer(0, 0, 0)]);
};
I also have a save function that submits via ajax. Within the success callback, loadIntegerSorter is called.
success: function (result) {
if (result.Status == "success") {
isvm.loadSortedIntegers();
}
}
However, this seems to break the enable binding. The CSS binding behaves as expected with the array items = 2. But the Enable binding does not. I can run loadIntegerSorter outside of the Ajax function successfully so I suppose this is a synchronization problem but I don't know what the solution is.
The fiddle I've linked to doesn't fully demonstrate the problem because it depends on making a genuine Ajax request. But I hope it shows enough to understand.
Elaboration:
This results in the expected behaviour from the enable binding:
self.save = function () {
self.isloading();
};
But this doesn't:
self.save = function () {
$.ajax("/Home/Index", {
data: ko.toJSON(self.integerSorter),
cache: false,
type: "post",
contentType: "application/json",
context: self,
success: function (result) {
this.isloading();
}
});
};
And nor does this:
self.save = function () {
self.isloading();
$.ajax("/Home/Index", {
data: ko.toJSON(self.integerSorter),
cache: false,
type: "post",
contentType: "application/json",
context: self,
success: function (result) {
}
});
};
Whatever the cause of the problem, it seems to be related to the ajax call.
1)
Inside of your self.save function you're calling
self.isLoading(true);
Which yields
TypeError: 'undefined' is not a function (evaluating
'self.isLoading(true)')
telling you that self.isLoading is not declared anywhere in your code. This will break code execution even before the ajax request is sent.
2)
Same as 1) but this time for self.msgbox.status(). Undeclared: will break your code.
3)
The function self.loadIntegerSorter appears as self.loadSortedIntegers in the success function. Also, the self.save function appears declared two times. The second one will ovverride the first, but I guess the first one is there just in the fiddle.
4)
Inside of the success function, result.Status doesn't have any sense. You must understand that result is just a string of plain text, accessing the Status property of a string will result in an error. Perhaps you expect the response to be a JSON object with a Status property? If that is the case, you have to deserialize the string either by yourself (JSON.parse(response)) or by telling jQuery to do that for you (replace $.ajax with $.getJSON).
However, it may also be that you're not receiving any JSON back and you just wanted to access the response status, assuming you could do it that way. You can't. Being inside of a success function, you already know that your request has been successfully sent and a response received. No need to check it again.
5)
You're calling the loadSortedIntegers() method on the variable isvm. That's a totally wrong approach, even if it should work now it may cause huge troubles in the future. isvm is a global variable you use to contain an instance of your viewModel. The success function is contained in the viewModel itself, you should access it's own methods with this or self. A class should not access an instance of itself with a global variable. Question: how can I make this and/or self available in the success function? this can be reached by setting the context property to your $.ajax object. Exactly as you write success: function(){} you should write, just before that, context: this or, in your case, context: self.
Do that, and then just change the success function contents with this.loadSortedIntegers().
I've took the liberty to make some edits to your fiddle. Take your time to examine the difference here and to run it here.
Try to use valueHasMutated to push update for observable directly:
self.loadIntegerSorter = function () {
self.Integers([new integer(0, 0, 0), new integer(0, 0, 0)]);
self.Integers.valueHasMutated();
};
I'm trying to initialize a variable in javascript (specifically, I want to use a remote template with the jQuery template plugin) and then have multiple asynchronous callbacks wait for it to be initialized before proceeding. What I really want is to be able to link to the remote template via a <script type="text/x-jquery-tmpl" src="/my/remote_template"> tag, but barring that I could get away with the javascript equivalent of a pthread_once.
Ideally, the api would look something like:
$.once(function_to_be_called_once, function_to_be_called_after_first)
And used like:
var remote_template = "";
function init_remote_template() {
remote_template = $.get( {
url: "/my/remote/template",
async: false
});
}
$.once(init_remote_template, function () {
// Add initial things using remote template.
});
And then later, elsewhere:
$.get({
url: "/something/that/requires/an/asynchronous/callback",
success: function () {
$.once(init_remote_template, function () {
// Do something using remote template.
}
}
});
Does such a thing exist?
Looks like jQuery's promises can help you out here:
var templatePromise = $.get({
url: "/my/remote/template"
});
templatePromise.done(function(template) {
// Add initial things using remote template.
});
and elsewhere you can do:
$.get({
url: "/something/that/requires/an/asynchronous/callback",
success: function () {
templatePromise.done(function(template) {
// Do more things using remote template.
});
}
});
Usually $.get (and $.ajax, etc) are used with success: and error: callbacks in the initial invocation, but they also return a promise object that acts just like a $.Deferred, documented here: http://api.jquery.com/category/deferred-object/ which allows you to do what you're asking. For error handling, you can use templatePromise.fail(...) or simply add error: ... to the initial $.get.
In general it's best to avoid synchronous AJAX calls because most browsers' interfaces will block while the HTTP request is being processed.
If I understand correctly, jQuery will do what you want of it by way of Deferreds/promises.
You can even generalise the remote template fetcher by
using a js plain object in which to cache any number of templates
renaming the function and passing a url to it, get_remote_template(url)
js :
var template_cache = {};//generalised template cache
//generalised template fetcher
function get_remote_template(url) {
var dfrd = $.Deferred();
if(!template_cache[url]) {
$.get(url).done(function(tpl) {
template_cache[url] = tpl; //we use the url as a key.
dfrd.resolve(tpl);
});
}
else {
dfrd.resolve(template_cache[url]);
}
return dfrd.promise();
}
Then :
var url1 = "/my/remote/template"; //the url of a particular template
get_remote_template(url1).done(function(tpl) {
// Add initial things using tpl.
});
And, earlier or later :
$.get({
url: "/something/that/requires/an/asynchronous/callback",
success: function(data) {
init_remote_template(url1).done(function (tpl) {
// Do something using tpl (and data).
});
}
});
Note how get_remote_template() returns a promise. If the requested template is already cached, the promise is returned ready-resolved. If the template is not yet in cache (ie. it needs to be downloaded from the server), then the promise will be resolved some short time later. Either way, the fact that a promise is returned allows a .done() command to be chained, and for the appropriate template to be accessed and used.
EDIT
Taking #SophieAlpert's points on board, this version caches the promise associated with a tpl rather than the tpl itself.
var template_cache = {};//generalised cache of promises associated with templates.
//generalised template fetcher
function get_remote_template(url) {
if(!template_cache[url]) {
template_cache[url] = $.get(url);
}
return template_cache[url];//this is a promise
}
This version of get_remote_template() would be used in the same way as above.
I've been using jQuery for a couple of years now with very limited understanding of vanilla javascript. Scope, the object model, and many of the design patterns that I see used in javascript baffle me. I'm trying to implement a class that will eventually be used in a scheduling plugin that I need to write and I'm having a hard time understanding why data stored in one of my class members doesn't seem to be available. I'm not sure if the issue is with scope or some other behavior that I don't understand.
I have the following code with 2 questions in the comments at the appropriate places. The first question is whether or not my scope workaround in my getJSON call is the correct way of handling the scope issue inside getJSON. My second question is why I can't directly access schedule.data.
function Schedule() {
this.year = null;
this.month = null;
this.day = null;
this.start_datetime = null;
this.start_timestamp = null;
this.end_datetime = null;
this.end_timestamp = null;
this.data = [];
return this;
}
Schedule.prototype.init = function() {
var url = '/tripsys/new_admin/employee_schedule/get_employee_schedule_data/' + this.start_timestamp + '/' + this.end_timestamp;
var self = this; // 1. trying to work around scope issues. Is this the correct way to handle the scope problems here?
$.getJSON(url, function(data) {
self.data = data;
});
}
var schedule = new Schedule();
$(document).ready(function() {
schedule.year = $('#year').text();
schedule.month = $('#month').text();
schedule.day = $('#day').text();
schedule.start_datetime = new Date(schedule.year, schedule.month - 1, schedule.day);
schedule.start_timestamp = Math.round(schedule.start_datetime.getTime() / 1000);
schedule.end_datetime = new Date(schedule.year, schedule.month - 1, schedule.day, 23, 59, 59);
schedule.end_timestamp = Math.round(schedule.end_datetime.getTime() / 1000);
schedule.init();
console.log(schedule); // if I log the whole schedule object the data that I expect to be in the "data" member is there
console.log(schedule.data); // 2. why is the data that I expect to be in the "data" member not there when I access schedule.data directly?
});
Thanks for your insight.
Well point number one is correct in that you need to save the this reference while you still can because when the inner function is called by jQuery, this inside the function will refer to the ajax object.
In the second comment you are logging schedule.data before the ajax request has completed. You can see schedule.data when you log schedule because when you log an object in google chrome, the object properties are retrieved after you manually "expand" the object in chrome console. When you manually "expand" it, at that time the request has already completed.
You can reproduce it like this:
var a = {};
console.log(a); //do not "expand" the object properties yet
console.log(a.property); //undefined
a.property = "value";
//"expand" the already logged object and it will have "value"
Yes, that will work, although it isn't a scope issue as much as it uses variable scope to get around a context issue.
To access schedule.data, you need to wait until the data has arrived. In other words, place the console.log code in the callback.
The issue is that the ajax call has not returned before you log the object. If you want to make the ajax call synchronous and the init function gets a result before you log, use the async param on an ajax jQuery call:
$.ajax({
url: url,
dataType: 'json',
async: false,
success: function(data){
self.data = data;
console.log(data);
}
});
This is probably because in this line schedule.init();, it makes an ajax call which has not completed yet when you then do console.log(schedule.data);. Ajax calls are asynchronous. Calling them only starts the networking operation and then they return immediately. They are not completed until the success handler function has been called (and that's when self.data is assigned).
So, if you want to look at the data for the schedule object that was obtained in the .init() function, you have to wait until that ajax call has completed or do something with the data in the completion function.