So basically I have this very simple counter that loads JSON files from 5 different locations and counts the number of entries:
let count = {
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
total: 0
};
let url = {
1: "https://jsonplaceholder.typicode.com/posts",
2: "https://jsonplaceholder.typicode.com/comments",
3: "https://jsonplaceholder.typicode.com/albums",
4: "https://jsonplaceholder.typicode.com/todos",
5: "https://jsonplaceholder.typicode.com/users"
};
function fetchJSONList(url) {
return new Promise(function(resolve, reject) {
fetch(url)
.then(response => { return response.json(); })
.then(resolve)
.catch(reject);
});
}
function checkAll() {
return new Promise(function(resolve, reject) {
$.each(url, function(key) {
fetchJSONList(url[key])
.then(list => {
if ($.isEmptyObject(list)) {
const spanID = "#api-" + key;
$(spanID).text(0);
}
$.each(list, function() {
count[key]++;
count.total++;
const spanID = "#api-" + key;
$(spanID).text(count[key]);
});
})
.catch(reject);
});
resolve();
});
}
checkAll()
.then($("#count-total").text(count.total))
.catch(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="site-list">
<li><span>API 1: </span><span id="api-1">Loading...</span></li>
<li><span>API 2: </span><span id="api-2">Loading...</span></li>
<li><span>API 3: </span><span id="api-3">Loading...</span></li>
<li><span>API 4: </span><span id="api-4">Loading...</span></li>
<li><span>API 5: </span><span id="api-5">Loading...</span></li>
</ul>
<span>Total: </span><span id="count-total">Loading...</span>
As you can see, now the total amount is updated to "0" immediately after the scripts loads (i.e. the promise returned by function checkAll()
is resolved immediately). However, I would like the total value to update only after $.each
has finished iterating through the url
variable.
Any ideas how to do that?
Edit: So considering T.J. Crowder's answer,
let count = {
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
total: 0
};
let url = {
1: "https://jsonplaceholder.typicode.com/posts",
2: "https://jsonplaceholder.typicode.com/comments",
3: "https://jsonplaceholder.typicode.com/albums",
4: "https://jsonplaceholder.typicode.com/todos",
5: "https://jsonplaceholder.typicode.com/users"
};
function fetchJSONList(url) {
return new Promise(function(resolve, reject) {
fetch(url)
.then(response => { return response.json(); })
.then(resolve)
.catch(reject);
});
}
Promise
.all(
Object.entries(url).map(([key, value]) => {
fetchJSONList(value)
.then(list => {
if ($.isEmptyObject(list)) {
const spanID = "#api-" + key;
$(spanID).text(0);
}
$.each(list, function() {
count[key]++;
count.total++;
const spanID = "#api-" + key;
$(spanID).text(count[key]);
});
})
})
)
.then($("#count-total").text(count.total))
.catch(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="site-list">
<li><span>API 1: </span><span id="api-1">Loading...</span></li>
<li><span>API 2: </span><span id="api-2">Loading...</span></li>
<li><span>API 3: </span><span id="api-3">Loading...</span></li>
<li><span>API 4: </span><span id="api-4">Loading...</span></li>
<li><span>API 5: </span><span id="api-5">Loading...</span></li>
</ul>
<span>Total: </span><span id="count-total">Loading...</span>
Welp, still no luck.
However, I would like the total value to update only after
$.each
has finished iterating through theurl
variable.
Well, it does. :-) It's just that $.each
doesn't wait for the asynchronous processes it starts to finish.
If you want all of those running at once, use map
to gather the promises and Promise.all
to wait for them all to be fulfilled (or for one of them to be rejected):
function checkAll() {
return Promise.all(Object.entries(url).map(([key, value]) =>
fetchJSONList(value)
.then(list => {
if ($.isEmptyObject(list)) {
const spanID = "#api-" + key;
$(spanID).text(0);
}
$.each(list, function() {
count[key]++;
count.total++;
const spanID = "#api-" + key;
$(spanID).text(count[key]);
});
})
));
}
You also need to fix your call to then
here:
.then($("#count-total").text(count.total))
That calls $(/*...*/).text(/*...*/)
and passes the return value into then
. You want to pass then
a function:
.then(() => $("#count-total").text(count.total))
Also, there's no reason for new Promise
in fetchJSONList
; you already have a promise, just chain from it. But you do need to deal with the fetch
API footgun that it doesn't reject on HTTP error (just network error):
function fetchJSONList(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
});
}
Putting that all together:
let count = {
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
total: 0
};
let url = {
1: "https://jsonplaceholder.typicode.com/posts",
2: "https://jsonplaceholder.typicode.com/comments",
3: "https://jsonplaceholder.typicode.com/albums",
4: "https://jsonplaceholder.typicode.com/todos",
5: "https://jsonplaceholder.typicode.com/users"
};
function fetchJSONList(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
});
}
function checkAll() {
return Promise.all(Object.entries(url).map(([key, value]) =>
fetchJSONList(value)
.then(list => {
if ($.isEmptyObject(list)) {
const spanID = "#api-" + key;
$(spanID).text(0);
}
$.each(list, function() {
count[key]++;
count.total++;
const spanID = "#api-" + key;
$(spanID).text(count[key]);
});
})
));
}
checkAll()
.then(() => $("#count-total").text(count.total))
.catch(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="site-list">
<li><span>API 1: </span><span id="api-1">Loading...</span></li>
<li><span>API 2: </span><span id="api-2">Loading...</span></li>
<li><span>API 3: </span><span id="api-3">Loading...</span></li>
<li><span>API 4: </span><span id="api-4">Loading...</span></li>
<li><span>API 5: </span><span id="api-5">Loading...</span></li>
</ul>
<span>Total: </span><span id="count-total">Loading...</span>
If you want to wait for each of them to finish before starting the next one, build a promise chain with a loop:
function checkAll() {
let p = Promise.resolve();
for (const [key, value] of Object.entries(url)) {
p = p.then(() => fetchJSONList(value))
.then(list => {
if ($.isEmptyObject(list)) {
const spanID = "#api-" + key;
$(spanID).text(0);
}
$.each(list, function() {
count[key]++;
count.total++;
const spanID = "#api-" + key;
$(spanID).text(count[key]);
});
});
}
return p;
}
let count = {
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
total: 0
};
let url = {
1: "https://jsonplaceholder.typicode.com/posts",
2: "https://jsonplaceholder.typicode.com/comments",
3: "https://jsonplaceholder.typicode.com/albums",
4: "https://jsonplaceholder.typicode.com/todos",
5: "https://jsonplaceholder.typicode.com/users"
};
function fetchJSONList(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
});
}
function checkAll() {
let p = Promise.resolve();
for (const [key, value] of Object.entries(url)) {
p = p.then(() => fetchJSONList(value))
.then(list => {
if ($.isEmptyObject(list)) {
const spanID = "#api-" + key;
$(spanID).text(0);
}
$.each(list, function() {
count[key]++;
count.total++;
const spanID = "#api-" + key;
$(spanID).text(count[key]);
});
});
}
return p;
}
checkAll()
.then(() => $("#count-total").text(count.total))
.catch(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="site-list">
<li><span>API 1: </span><span id="api-1">Loading...</span></li>
<li><span>API 2: </span><span id="api-2">Loading...</span></li>
<li><span>API 3: </span><span id="api-3">Loading...</span></li>
<li><span>API 4: </span><span id="api-4">Loading...</span></li>
<li><span>API 5: </span><span id="api-5">Loading...</span></li>
</ul>
<span>Total: </span><span id="count-total">Loading...</span>
Or if your environment supports them, use an async
function:
async function checkAll() {
for (const [key, value] of Object.entries(url)) {
const list = await fetchJSONList(value);
if ($.isEmptyObject(list)) {
const spanID = "#api-" + key;
$(spanID).text(0);
}
$.each(list, function() {
count[key]++;
count.total++;
const spanID = "#api-" + key;
$(spanID).text(count[key]);
});
}
}
let count = {
1: 0,
2: 0,
3: 0,
4: 0,
5: 0,
total: 0
};
let url = {
1: "https://jsonplaceholder.typicode.com/posts",
2: "https://jsonplaceholder.typicode.com/comments",
3: "https://jsonplaceholder.typicode.com/albums",
4: "https://jsonplaceholder.typicode.com/todos",
5: "https://jsonplaceholder.typicode.com/users"
};
function fetchJSONList(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
throw new Error("HTTP error " + response.status);
}
return response.json();
});
}
async function checkAll() {
for (const [key, value] of Object.entries(url)) {
const list = await fetchJSONList(value);
if ($.isEmptyObject(list)) {
const spanID = "#api-" + key;
$(spanID).text(0);
}
$.each(list, function() {
count[key]++;
count.total++;
const spanID = "#api-" + key;
$(spanID).text(count[key]);
});
}
}
checkAll()
.then(() => $("#count-total").text(count.total))
.catch(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul id="site-list">
<li><span>API 1: </span><span id="api-1">Loading...</span></li>
<li><span>API 2: </span><span id="api-2">Loading...</span></li>
<li><span>API 3: </span><span id="api-3">Loading...</span></li>
<li><span>API 4: </span><span id="api-4">Loading...</span></li>
<li><span>API 5: </span><span id="api-5">Loading...</span></li>
</ul>
<span>Total: </span><span id="count-total">Loading...</span>