JQuery $.getJSON asynchronous workaround - caching ajax results - javascript

I am caching the JSON returned from ajax calls and then displaying the results from the cache. My issue is if there is no cache for that Ajax call already, the results only display on refresh. This is down to the fact that ajax is asynchronous but how do I get around that? Ajax async:false has been deprecated so that's not an option. Would the $.getJSON .done() function suffice or is there a better way?
Here is my code so far:
if ((online === true)) {
//get JSON
$.getJSON(baseurl + '/wp-json/app/v2/files?filter[category]' + cat + '&per_page=100', function(jd) {
//cache JSON
var cache = {
date: new Date(),
data: JSON.stringify(jd)
};
localStorage.setItem('cat-' + cat, JSON.stringify(cache));
});
//if not online and no cached file
} else if ((online === false) && (!cache['cat-' + cat])) {
alert('There are no cached files. You need to be online.');
}
//get cached JSON
cache['cat-' + cat] = JSON.parse(localStorage.getItem('cat-' + cat));
var objCache = cache['cat-' + cat].data;
objCache = JSON.parse(objCache); //Parse string to json
//display JSON results from cache
$.each(objCache, function(i, jd) {
var thumb = jd.file_thumbnail.sizes.medium;
//.....etc...
)
}}

A simple rewrite of your code yields the following:
function cacheAsCacheCan(cat, callback) {
if (online === true) {
//get JSON
$.getJSON(baseurl + '/wp-json/app/v2/files?filter[category]' + cat + '&per_page=100', function(jd) {
//cache JSON
var cache = {
date: new Date(),
data: JSON.stringify(jd)
};
localStorage.setItem('cat--' + cat, JSON.stringify(cache));
});
//if not online and no cached file
} else if ((online === false) && (!cache['cat-' + cat])) {
callback('There are no cached files. You need to be online.');
return;
}
//get cached JSON
callback(null, cache['cat-' + cat] = JSON.parse(localStorage.getItem('cat-' + cat)));
}
cacheAsCacheCan('someCat', function(error, cachedata) {
if(error) {
alert(error);
} else {
var objCache = cachedata.data;
objCache = JSON.parse(objCache); //Parse string to json
//display JSON results from cache
$.each(objCache, function(i, jd) {
var thumb = jd.file_thumbnail.sizes.medium;
//.....etc...
)
}
}
);

Related

async XMLHttpRequest blocking UI in Browser

I have the following function in the client-side of a web app:
function fetchDataFromApi(fetchCode, options, callback) {
var dataObject = JSON;
dataObject.fetchCode = fetchCode;
dataObject.options = options;
var xhr = new XMLHttpRequest();
var url = "DATA_API_URL";
// connect to the API
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type",
"application/json"
);
// set callback for when API responds. This will be called once the request is answered by the API.
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
// API has responded;
var json = {
ok: false,
message: 'could not parse response'
};
try {
// parse the raw response into the API response object
json = JSON.parse(xhr.responseText);
} catch (err) {
// probably json parse error; show raw response and error message
console.log(err);
console.log("raw response: " + xhr.responseText);
}
if (json.ok) {
// success, execute callback with argument json.data
callback(json.data);
} else {
// fetch failed;
console.error(json.message);
}
}
};
// send request payload to API
var data = JSON.stringify(dataObject);
xhr.send(data);
}
Since I am using an asynchronous call (the third parameter in xhr.open is set to true), I am surprised to find that this function blocks the UI in the browser. When there is a substantial amount of data grabbed from the server with this function, it can take 3-4 seconds, blocking the UI and generating this error in the Chrome console:
[Violation] 'load' handler took 3340ms
This function is currently in production here, where I am calling the function as so:
function getNamesFromApi() {
fetchDataFromApi('chj-confraternity-list', {}, function (data) {
fadeReplace(document.getElementById('spinner-2'), document.getElementById(
'name-list-container'),
false, true);
// transaction was successful; display names
var listString = "";
if (data.list) {
// add the names to the page
var listLength = data.list.length;
for (var x = 0; x < listLength; x++) {
document.getElementById('name-list-container').innerHTML +=
"<div class='name-list-item'>" +
"<span class='name-list-name'>" +
data.list[x].name +
"</span>" +
"<span class='name-list-location'>" +
data.list[x].location +
"</span>" +
"</div>";
}
}
});
}
window.addEventListener('load', function() {
getNamesFromApi();
});
Why is this blocking the UI, and what am I doing wrong in making an asynchronous XMLHttpRequest?
UPDATE: Thanks to the comments for pointing me in the right direction; the issue was not the XMLHttpRequest, but rather me appending innerHTMl within a loop. The issue is now fixed, with the corrected snippet in the answer.
The UI was blocked because i was appending innerHTML within a loop, an expensive, and UI-blocking operation. The issue is now fixed. Here is the corrected snippet:
function getNamesFromApi() {
fetchDataFromApi('chj-confraternity-list', {}, function (data) {
fadeReplace(document.getElementById('spinner-2'), document.getElementById(
'name-list-container'),
false, true);
// transaction was successful; display names
if (data.list) {
var listString = "";
// add the names to the page
var listLength = data.list.length;
for (var x = 0; x < listLength; x++) {
listString +=
"<div class='name-list-item'>" +
"<span class='name-list-name'>" +
data.list[x].name +
"</span>" +
"<span class='name-list-location'>" +
data.list[x].location +
"</span>" +
"</div>";
}
document.getElementById('name-list-container').innerHTML = listString;
}
});
}
window.addEventListener('load', function() {
getNamesFromApi();
});

How to send array in Ajax and get it in PHP as array

I want to send in ajax array in js, get it in php as an array and insert into with sql.
My ajax call looks like:
jQuery("#Save").click(function () {
$(".linesView").each(function () {
var action_name = "";
if (window.current_category === "Expenses") {
action_name = "Save_Expenses"
} else if(window.current_category === "Incomes") {
action_name = "Save_Incomes"
}
var line_id = $(this).attr("id").substring(5);
var category = $("#CategoryValue_" + line_id).html();
var date = $("#DateValue_" + line_id).html();
var amount = $("#AmountValue_" + line_id).val();
var repeated = $("#RepeatedValue_" + line_id).html();
var note = $("#NoteValue_" + line_id).val();
var data = json_encode([category,date,amount,repeated,note]);
$.post("AjaxHandler.php", { "action_name": action_name, "data": data }, function () {
//$("#ExpensesId_" + id).css('display', 'none');
});
});
});
The PHP code that needs to get the ajax call and add the data (by sql insert) looks like:
if(isset($_POST['action_name','data'])) {
$action_name = $_POST['action_name'];
$data=json_decode($_POST['data']);
$query = mysql_query("INSERT INTO Expenses (accountid, category, date, amount, repeated) VALUES ('$accountid', '$data[0]', '$data[1]', '$data[2]', '0')");
}
The accountid coming from the top of the page, I already do delete action and it works fine, so the accountid is ok. All others, I don't know.
I tried to do encode and then decode. I am not sure if the syntax is right. Anyway if I didn't write elegant code, please show me how it needs to look. Maybe i need to take each param from the data and not to call data[x]?
Use JSON.stringify()
var data = ([category,date,amount,repeated,note]);
$.post("AjaxHandler.php", { "action_name": action_name, "data": JSON.stringify(data) }, function () {
//$("#ExpensesId_" + id).css('display', 'none');
});
Encode the data array to JSON string using JSON.stringify() in javascript. At server side use json_decode() to decode the data.
jQuery:
jQuery("#Save").click(function() {
$(".linesView").each(function() {
var action_name = "";
if (window.current_category === "Expenses") {
action_name = "Save_Expenses"
} else if (window.current_category === "Incomes") {
action_name = "Save_Incomes"
}
var line_id = $(this).attr("id").substring(5);
var category = $("#CategoryValue_" + line_id).html();
var date = $("#DateValue_" + line_id).html();
var amount = $("#AmountValue_" + line_id).val();
var repeated = $("#RepeatedValue_" + line_id).html();
var note = $("#NoteValue_" + line_id).val();
var data = JSON.stringify([category, date, amount, repeated, note]);
//-----------------^--- Array to JSON string
$.post("AjaxHandler.php", {
"action_name": action_name,
"data": data
}, function() {
//$("#ExpensesId_" + id).css('display', 'none');
});
});
});
PHP :
if(isset($_POST['action_name','data'])){
$action_name = $_POST['action_name'];
$data=json_decode(json_decode($_POST['data']));
//-----^--- decoding JSON string
$query = mysql_query("INSERT INTO Expenses (accountid, category, date, amount, repeated) VALUES ('$accountid', '$data[0]', '$data[1]', '$data[2]', '0')");
}

Office.context.document.getFileAsync throwing errors

I am getting a very strange issue whereby when I try to extract the word document as a compressed file for processing in my MS Word Task Pane MVC app the third time, it will blow up.
Here is the code:
Office.context.document.getFileAsync(Office.FileType.Compressed, function (result) {
if (result.status == "succeeded") {
var file = result.value;
file.getSliceAsync(0, function (resultSlice) {
//DO SOMETHING
});
} else {
//TODO: Service fault handling?
}
});
The error code that comes up is 5001. I am not sure how to fix this.
Please let me know if you have any thoughts on this.
Additional Details:
From MSDN:
No more than two documents are allowed to be in memory; otherwise the
getFileAsync operation will fail. Use the File.closeAsync method to
close the file when you are finished working with it.
Make sure you call File.closeAsync before you read the file again - that could explain the issue you are seeing.
More at: https://msdn.microsoft.com/en-us/library/office/jj715284.aspx
I have an example about how to use this API correctly. Actually the current example in the MSDN is not very correct. This code is tested in Word.
// Usually we encode the data in base64 format before sending it to server.
function encodeBase64(docData) {
var s = "";
for (var i = 0; i < docData.length; i++)
s += String.fromCharCode(docData[i]);
return window.btoa(s);
}
// Call getFileAsync() to start the retrieving file process.
function getFileAsyncInternal() {
Office.context.document.getFileAsync("compressed", { sliceSize: 10240 }, function (asyncResult) {
if (asyncResult.status == Office.AsyncResultStatus.Failed) {
document.getElementById("log").textContent = JSON.stringify(asyncResult);
}
else {
getAllSlices(asyncResult.value);
}
});
}
// Get all the slices of file from the host after "getFileAsync" is done.
function getAllSlices(file) {
var sliceCount = file.sliceCount;
var sliceIndex = 0;
var docdata = [];
var getSlice = function () {
file.getSliceAsync(sliceIndex, function (asyncResult) {
if (asyncResult.status == "succeeded") {
docdata = docdata.concat(asyncResult.value.data);
sliceIndex++;
if (sliceIndex == sliceCount) {
file.closeAsync();
onGetAllSlicesSucceeded(docdata);
}
else {
getSlice();
}
}
else {
file.closeAsync();
document.getElementById("log").textContent = JSON.stringify(asyncResult);
}
});
};
getSlice();
}
// Upload the docx file to server after obtaining all the bits from host.
function onGetAllSlicesSucceeded(docxData) {
$.ajax({
type: "POST",
url: "Handler.ashx",
data: encodeBase64(docxData),
contentType: "application/json; charset=utf-8",
}).done(function (data) {
document.getElementById("documentXmlContent").textContent = data;
}).fail(function (jqXHR, textStatus) {
});
}
You may find more information from here:
https://github.com/pkkj/AppForOfficeSample/tree/master/GetFileAsync
Hope this could help.
Additional to Keyjing Peng's answer (which I found very helpful, thanks!) I thought I'd share a variation on the encodeBase64, which you don't want to do if you are uploading via REST to SharePoint. In that case you want to convert the byte array to a Uint8Array. Only then could I get it into a SharePoint library without file corruption.
var uArray = new Uint8Array(docdata);
Hope this helps someone, couldn't find this info anywhere else online...
See this link
http://msdn.microsoft.com/en-us/library/office/jj715284(v=office.1501401).aspx
it contains this example method:
var i = 0;
var slices = 0;
function getDocumentAsPDF() {
Office.context.document.getFileAsync("pdf",{sliceSize: 2097152}, function (result) {
if (result.status == "succeeded") {
// If the getFileAsync call succeeded, then
// result.value will return a valid File Object.
myFile = result.value;
slices = myFile.sliceCount;
document.getElementById("result").innerText = " File size:" + myFile.size + " #Slices: " + slices;
// Iterate over the file slices.
for ( i = 0; i < slices; i++) {
var slice = myFile.getSliceAsync(i, function (result) {
if (result.status == "succeeded") {
doSomethingWithChunk(result.value.data);
if (slices == i) // Means it's done traversing...
{
SendFileComplete();
}
}
else
document.getElementById("result").innerText = result.error.message;
});
}
myFile.closeAsync();
}
else
document.getElementById("result2").innerText = result.error.message;
});
}
change "pdf" to "compressed" and the method call doSomethingWithChunk() needs to be created and should probably do something like this:
function base64Encode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
I use this technique to successfully save to Azure blob storage.
Obviously you should rename the method as well.

Angular deferred implementation only outputs last value of loop

I have a custom synchronization process where I queue up, in order, all of my sync records. When my service retrieves more than 1 sync record, it will process them, then update my last sync date for every successful record, or log my error when it fails (without updating the last sync date) and abort the sync process.
I've implemented the $q.all from AngularJS. Here's a subset of the sync loop:
var processes = [];
for (var i in data) {
if (data[i] === null || data[i].TableName == null || data[i].Query == null || data[i].Params == null) {
// Let's throw an error here...
throw new TypeError("ERROR! The data retrieved from the download sync process was of an unexpected type.");
}
var params = data[i].Params;
var paramsMassaged = params.replaceAll("[", "").replaceAll("]", "").replaceAll(", ", ",").replaceAll("'", "");
var paramsArray = paramsMassaged.split(",");
mlog.Log("Query: " + data[i].Query);
mlog.Log("Params: " + paramsArray);
if (data[i].TableName === "table1") {
var process = $table1_DBContext.ExecuteSyncItem(data[i].Query, paramsArray);
process.then(
function () {
$DBConfigurations_DBContext.UpdateLastSyncDate(data[i].CreatedDate, function (response) {
mlog.Log(response);
});
},
function (response) {
mlog.LogSync("Error syncing record: " + response, "ERROR", data[i].Id);
},
null
);
processes.push(process);
} else if (data[i].TableName === "table2") {
var process = $table2_DBContext.ExecuteSyncItem(data[i].Query, paramsArray);
process.then(
function () {
$DBConfigurations_DBContext.UpdateLastSyncDate(data[i].CreatedDate, function (response) {
mlog.Log(response);
});
},
function (response) {
mlog.LogSync("Error syncing record: " + response, "ERROR", data[i].Id);
},
null
);
processes.push(process);
} else {
mlog.LogSync("WARNING! This table is not included in the sync process. You have an outdated version of the application. Table: " + data[i].TableName);
}
}
$q.all(processes)
.then(function (result) {
mlog.LogSync("---Finished syncing all records");
}, function (response) {
mlog.LogSync("Sync Failure - " + response, "ERROR");
});
Example ExecuteSyncItem function:
ExecuteSyncItem: function (script, params) {
window.logger.logIt("In the table1 ExecuteSyncItem function...");
var primaryKey = params[params.length - 1];
var deferred = $q.defer();
$DBService.ExecuteQuery(script, params,
function (insertId, rowsAffected, rows) {
window.logger.logIt("rowsAffected: " + rowsAffected.rowsAffected);
if (rowsAffected.rowsAffected <= 1) {
deferred.resolve();
} else {
deferred.resolve(errorMessage);
}
},
function (tx, error) {
deferred.reject("Failed to sync table1 record with primary key: " + primaryKey + "; Error: " + error.message);
}
);
return deferred.promise;
}
The problem I'm running into is, if there are more than 1 sync records that fail, then this line displays the same value for all records that failed (not sure if it's the first failure record, or the last).
mlog.LogSync("Error syncing record: " + response, "ERROR", data[i].Id);
How do I get it to display the information for the specific record that failed, instead of the same message "x" times?
As mentioned by comradburk wrapping your processes in a closure within a loop is a good solution, but there is an angular way in solving this problem. Instead of using the native for-in loop, you can do it via angular.forEach() and loop through all the data elements.
var processes = [];
angular.forEach(data, function(item) {
if (item === null || item.TableName == null || item.Query == null || item.Params == null) {
// Let's throw an error here...
throw new TypeError("ERROR! The data retrieved from the download sync process was of an unexpected type.");
}
var params = item.Params;
var paramsMassaged = params.replaceAll("[", "").replaceAll("]", "").replaceAll(", ", ",").replaceAll("'", "");
var paramsArray = paramsMassaged.split(",");
mlog.Log("Query: " + item.Query);
mlog.Log("Params: " + paramsArray);
if (item.TableName === "table1") {
var process = $table1_DBContext.ExecuteSyncItem(item.Query, paramsArray);
process.then(
function () {
$DBConfigurations_DBContext.UpdateLastSyncDate(item.CreatedDate, function (response) {
mlog.Log(response);
});
},
function (response) {
mlog.LogSync("Error syncing record: " + response, "ERROR", item.Id);
},
null
);
processes.push(process);
} else if (item.TableName === "table2") {
var process = $table2_DBContext.ExecuteSyncItem(item.Query, paramsArray);
process.then(
function () {
$DBConfigurations_DBContext.UpdateLastSyncDate(item.CreatedDate, function (response) {
mlog.Log(response);
});
},
function (response) {
mlog.LogSync("Error syncing record: " + response, "ERROR", item.Id);
},
null
);
processes.push(process);
} else {
mlog.LogSync("WARNING! This table is not included in the sync process. You have an outdated version of the application. Table: " + item.TableName);
}
});
$q.all(processes)
.then(function (result) {
mlog.LogSync("---Finished syncing all records");
}, function (response) {
mlog.LogSync("Sync Failure - " + response, "ERROR");
});
The problem is due the closure you have on i. When the callback function executes, the value of i will be the last value in the for loop. You need to bind that value i to a separate, unchanging value. The easiest way to do that is with a self invoking function.
for (var i in data) {
(function(item) {
// Put your logic in here and use item instead of i, for example
mlog.LogSync("Error syncing record: " + response, "ERROR", data[item].Id
})(i);
}
Here's a good read for why closures cause this (it's a pretty common problem):
Javascript infamous Loop issue?

Populating an object with ajax in a loop

I need to pull data from a series of .csv files off the server. I am converting the csvs into arrays and I am trying to keep them all in an object. The ajax requests are all successful, but for some reason only the data from the last request ends up in the object. Here is my code:
var populate_chart_data = function(){
"use strict";
var genders = ["Boys","Girls"];
var charts = {
WHO: ["HCFA", "IWFA", "LFA", "WFA", "WFL"],
CDC: ["BMIAGE", "HCA", "IWFA", "LFA", "SFA", "WFA", "WFL", "WFS"]
};
var fileName, fileString;
var chart_data = {};
for (var i=0; i < genders.length; i++){
for (var item in charts){
if (charts.hasOwnProperty(item)){
for (var j=0; j<charts[item].length; j++) {
fileName = genders[i] + '_' + item + '_' + charts[item][j];
fileString = pathString + fileName + '.csv';
$.ajax(fileString, {
success: function(data) {
chart_data[fileName] = csvToArray(data);
},
error: function() {
console.log("Failed to retrieve csv");
},
timeout: 300000
});
}
}
}
}
return chart_data;
};
var chart_data = populate_chart_data();
The console in Firebug shows every ajax request successful, but when I step through the loops, my chart_data object is empty until the final loop. This is my first foray into ajax. Is it a timing issue?
There are two things you need to consider here:
The AJAX calls are asynchronous, this means you callback will only be called as soon as you receive the data. Meanwhile your loop keeps going and queueing new requests.
Since you're loop is going on, the value of filename will change before your callback is executed.
So you need to do two things:
Push the requests into an array and only return when the array completes
Create a closure so your filename doesn't change
.
var chart_data = [];
var requests = [];
for (var j=0; j<charts[item].length; j++) {
fileName = genders[i] + '_' + item + '_' + charts[item][j];
fileString = pathString + fileName + '.csv';
var onSuccess = (function(filenameinclosure){ // closure for your filename
return function(data){
chart_data[filenameinclosure] = csvToArray(data);
};
})(fileName);
requests.push( // saving requests
$.ajax(fileString, {
success: onSuccess,
error: function() {
console.log("Failed to retrieve csv");
},
timeout: 300000
})
);
}
$.when.apply(undefined, requests).done(function () {
// chart_data is filled up
});
I'm surprised that any data ends up in the object. The thing about ajax is that you can't depend on ever knowing when the request will complete (or if it even will complete). Therefore any work that depends on the retrieved data must be done in the ajax callbacks. You could so something like this:
var requests = [];
var chart_data = {};
/* snip */
requests.push($.ajax(fileString, {
/* snip */
$.when.apply(undefined, requests).done(function () {
//chart_data should be full
});

Categories