How do I use the variable out of its asynchronous function scope - javascript

I need to use the variable that I declared inside the asynchronous function out of that scope. Is that possible?
movies.forEach((movie) => {
const { title, poster_path, vote_average, overview, release_date, id } = movie
async function getCredits(url) {
const res = await fetch(url)
const data = await res.json()
const directors = [];
const directorsName = directors.join(", ") // The one I want to bring out of its scope
}
const CREDITS_URL = `the credits url goes here and it uses this -> ${id}`
getCredits(CREDITS_URL)
const directorsName = directors.join(", ") // Like this
const card = document.createElement("div")
card.innerHTML = `
<div class="director">
<h3>Directed by ${directorsName}</h3> // This is where I need the variable
</div>
`
cards.appendChild(card)
})

It's possible to return something from your async function. You can move your getCredits function out of the loop, and make the loop async, something like:
async function getCredits(url) {
const res = await fetch(url)
const data = await res.json()
const directors = [];
// Do something with data?
return directors.join(", ");
}
movies.forEach(async (movie) => {
const { id } = movie
const CREDITS_URL = `the movie url goes here and it uses this -> ${id}`
const response = await getCredits(CREDITS_URL);
const card = document.createElement("div")
card.innerHTML = `
<div class="director">
<h3>Directed by ${response}</h3> // This is where I need the variable
</div>
`
cards.appendChild(card)
});

If you'd like to have the DOM be created more quickly and have a sort of "lazy loading" of directors' names, you could do something like this:
movies.forEach((movie) => {
const { title, poster_path, vote_average, overview, release_date, id } = movie;
const card = document.createElement('div');
card.innerHTML = `<div class="director">
<h3>Directed by <span class='dname'>...</span></h3>
</div>`;
const dname = card.querySelector('.dname');
async function getCredits(url) {
const res = await fetch(url);
const data = await res.json();
const directors = [];
dname.textContent = directors.join(', ');
}
const CREDITS_URL = `the credits url goes here and it uses this -> ${id}`;
getCredits(CREDITS_URL);
cards.appendChild(card);
});
Here you're creating the div and appending it immediately, but with a ... for directors' names. But you put that part in a span and then set the textContent of the span once getCredits resolves.
*Edit: This also has the added side-benefit of preventing HTML injection from your director's return, by not inserting them using innerHTML.

Related

Know how many promise are pending

My problem is the following :
I have an array of ids that I need to map to some database Ids using an HTTP request to my API like HTTP GET /foo/{id}
I need to wait for all values to show the data in my application
I'm currently doing this the following
async getValuesByIds({}, ids){
const valuesPromise = ids.map(async (id) => this.$axios.$get(`/foo/${id}`))
return Promise.all(valuesPromise)
}
And in the code before printing the values :
this.loading = true
this.getLeadsValuesByIds(idsArray).then(data => {
this.loading = false
this.values = data
})
The code is working fine but takes some times to run if i have a lot of ids.
In general, the first request ended in about 0.5 seconds and depending on the number of request, the last one can go up to 4 to 5 seconds
My goal here is to display a loading text informating the user how many request are left and how many are done.
Here is a short example using the jsonPlaceHolder API.
Basically what i want to have is instead of loading.. The number of request left (like {n} / 99 loaded
const loadData = () => {
const ids = Array.from({length: 99}, (_, i) => i + 1)
updateDataText('loading....')
const dataPromise = ids.map(async(id) => {
const post = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
return post.data
})
Promise.all(dataPromise).then(res => {
updateDataText(JSON.stringify(res))
})
}
const updateDataText = (text) => {
const div = document.getElementById('dataText')
div.innerText = text
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js"></script>
<button onclick="loadData()">LoadData</button>
<p id="dataText"></p>
Note : I'm using Nuxt, i don't know if that change something.
You could use a <progress> tag like this
I've also added inflight and finished counter if you prefer that
const loadData = () => {
const ids = Array.from({length: 99}, (_, i) => i + 1)
let nStarted = 0, nFinished=0;
const inflight = document.getElementById('inflight');
const finished = document.getElementById('finished');
const progress = document.getElementById('progress');
progress.max = ids.length;
updateDataText('loading....')
const dataPromise = ids.map(async(id) => {
nStarted++;
inflight.textContent = nStarted - nFinished;
const post = await axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
progress.value++;
nFinished++;
finished.textContent = nFinished;
inflight.textContent = nStarted - nFinished;
return post.data
})
Promise.all(dataPromise).then(res => {
updateDataText(JSON.stringify(res, null, 4))
})
}
const updateDataText = (text) => {
const div = document.getElementById('dataText')
div.innerText = text
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.27.2/axios.min.js"></script>
<button onclick="loadData()">LoadData</button>
<progress id="progress" value=0></progress>
<div>In Flight: <span id="inflight"></span></span>
<div>Finished: <span id="finished"></span></span>
<pre id="dataText"></pre>

How to insert data from another API call into function

I am building a project website which uses the rawg api to fetch data about games and displays them on the screen. I have also implemented a search functionality. My function getGame is using the base url which is called API_URL. My problem is that the data returned by this link does not return description as one of its variables. It has other stuff like game id and game name etc but no description. I have checked the docs and it says that in order to get the details of a game i need to use a different fetch link which takes in id as a parameter. So i implemented another function called getDescription() which takes in an id from the games.forEach function. I am able to console log all the descriptions of the games that are being displayed. My question is how would i be able to insert the description into the dom inside my showGames function. My code is attached below. Thanks.
const API_URL = `https://api.rawg.io/api/games?key=${apiKey}&page=5&page_size=40`;
const SEARCH_API = `https://api.rawg.io/api/games?key=${apiKey}&search="`;
const form = document.getElementById('form');
const search = document.getElementById('search-game');
const main = document.getElementById('main');
getGames(API_URL)
async function getGames(url){
const request = await fetch(url)
const response = await request.json()
showGames(response.results)
}
async function getDescription(id){
const request = await fetch(`https://api.rawg.io/api/games/${id}?key=${apiKey}`)
const response = await request.json();
showDescription(response.description)
}
function showGames(games){
main.innerHTML = '';
games.forEach(game => {
const { background_image, name, released, rating_top, id } = game;
const gameEl = document.createElement('div');
const platform = game.parent_platforms.map(platform => platform.platform.name).join(', ');
getDescription(id)
gameEl.classList.add('card');
gameEl.innerHTML = `
<div class="card-top">
<img src="${background_image}" alt="" class="card-image">
</div>
<div class="card-bottom">
<div class="card-info">
<h1 class="game-name">${name}</h1>
<h3>Release date: ${released}</h3>
<p>Platforms: <span>${platform}</span></p>
<p>*****I WANT TO ENTER THE DESCRIPTION HERE*****</p>
</div>
<div class="card-rating">
<span class="${getColorByRating(rating_top)}">${rating_top}</span>
</div>
</div>
`;
main.appendChild(gameEl);
})
}
Edit
Here is my console log for response inside getDescription function
Console log
I was able to get my wanted output by adding async right after my forEach like this games.forEach(async game => {....})
Here is my working code
const API_URL = `https://api.rawg.io/api/games?key=${apiKey}&page=5&page_size=40`;
const SEARCH_API = `https://api.rawg.io/api/games?key=${apiKey}&search="`;
const form = document.getElementById('form');
const search = document.getElementById('search-game');
const main = document.getElementById('main');
getGames(API_URL)
async function getGames(url){
const request = await fetch(url)
const response = await request.json()
showGames(response.results)
}
async function getDescription(id){
const request = await fetch(`https://api.rawg.io/api/games/${id}?key=${apiKey}`)
const response = await request.json();
return response;
}
function showGames(games){
main.innerHTML = '';
games.forEach(async game => {
const { background_image, name, released, rating_top, id } = game;
const gameEl = document.createElement('div');
const platform = game.parent_platforms.map(platform => platform.platform.name).join(', ');
const response = await getDescription(id)
gameEl.classList.add('card');
gameEl.innerHTML = `
<div class="card-top">
<img src="${background_image}" alt="" class="card-image">
</div>
<div class="card-bottom">
<div class="card-info">
<h1 class="game-name">${name}</h1>
<h3>Release date: ${released}</h3>
<p>Platforms: <span>${platform}</span></p>
<p>${response.description}</p>
</div>
<div class="card-rating">
<span class="${getColorByRating(rating_top)}">${rating_top}</span>
</div>
</div>
`;
main.appendChild(gameEl);
})
}
As you said that getDescription function just returns a string, so you can do something like this
async function getDescription(id){
const request = await fetch(`https://api.rawg.io/api/games/${id}?key=${apiKey}`)
const response = await request.json();
//showDescription(response.description)
//instead of using a function, just return the response from this func
return response
}
function showGames(games){
main.innerHTML = '';
games.forEach(game => {
const { background_image, name, released, rating_top, id } = game;
const gameEl = document.createElement('div');
const platform = game.parent_platforms.map(platform => platform.platform.name).join(', ');
//save response into a variable
const response = getDescription(id)
gameEl.classList.add('card');
gameEl.innerHTML = `
<div class="card-top">
<img src="${background_image}" alt="" class="card-image">
</div>
<div class="card-bottom">
<div class="card-info">
<h1 class="game-name">${name}</h1>
<h3>Release date: ${released}</h3>
<p>Platforms: <span>${platform}</span></p>
<p>${response.description}</p>
</div>
<div class="card-rating">
<span class="${getColorByRating(rating_top)}">${rating_top}</span>
</div>
</div>
`;
main.appendChild(gameEl);
})
}
Then use that variable response.description in the html string literal
You should be able to save the response of showDescription in a variable and then include it in your inner Html.
What it struggle with this is the fact that if array.forEach has a callback that returns a promise (like the async showDescription()), the javascript engine will not wait it to be resolved or rejected, so console will show an error that description is null.
There is a nice question where you can find how to loop promises here
You have to update getDescription like this:
async function getDescription(id){
const request = await fetch(`https://api.rawg.io/api/games/${id}?key=${apiKey}`)
const response = await request.json();
return response.description;
}
edit showGames like this
async function showGames(games) {
...
and use for inside showGames
for (const game of games) {
const description = await getDescription(game.id)
...
}
Every loop will wait getDescription() to be completed before start the next loop

How/When to remove child elements to clear search result?

Trying to clear my search result after I submit a new API call. Tried implementing gallery.remove(galleryItems); at different points but to no avail.
A bit disappointed I couldn't figure it out but happy I was able to get a few async functions going. Anyway, here's the code:
'use strict';
const form = document.querySelector('#searchForm');
const gallery = document.querySelector('.flexbox-container');
const galleryItems = document.getElementsByClassName('flexbox-item');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const userSearch = form.elements.query.value; // grab user input
const res = await getRequest(userSearch); // async func that returns a fully parsed Promise
tvShowMatches(res.data); // looks for matches, creates and appends name + image;
form.elements.query.value = '';
});
const getRequest = async (search) => {
const config = { params: { q: search } };
const res = await axios.get('http://api.tvmaze.com/search/shows', config);
return res;
};
const tvShowMatches = async (shows) => {
for (let result of shows) {
if (result.show.image) {
// new div w/ flexbox-item class + append to gallery
const tvShowMatch = document.createElement('DIV')
tvShowMatch.classList.add('flexbox-item');
gallery.append(tvShowMatch);
// create, fill & append tvShowName to tvShowMatch
const tvShowName = document.createElement('P');
tvShowName.textContent = result.show.name;
tvShowMatch.append(tvShowName);
// create, fill & append tvShowImg to tvShowMatch
const tvShowImg = document.createElement('IMG');
tvShowImg.src = result.show.image.medium;
tvShowMatch.append(tvShowImg);
}
}
};
Thanks
Instead of gallery.remove(galleryItems); consider resetting gallery.innerHTML to an empty string whenever a submit event occurs
Like this:
form.addEventListener('submit', async (e) => {
e.preventDefault();
gallery.innerHTML = ''; // Reset here
const userSearch = form.elements.query.value; // grab user input
const res = await getRequest(userSearch); // async func that returns a fully parsed Promise
tvShowMatches(res.data); // looks for matches, creates and appends name + image;
form.elements.query.value = '';
});
I believe this will do it.. you were close.
const galleryItems = document.getElementsByClassName('flexbox-item');
// to remove
galleryItems.forEach(elem => elem.remove() );

Query JSON-based API with user input with just javascript

I've been watching tutorials on using JSON data and JS, decided to work with an API and make a simple APP. I ran into a snag and I'm not sure what's causing the issue. The issue is around the way I'm using user input to modify the query string. When I make my endpoint something static and get rid of the 'movieSearch' function,like this:
const movies = [];
const endpoint = 'http://www.omdbapi.com/?apikey=myAPIkey=batman';
fetch(endpoint)
.then(blob => blob.json())
.then(data => movies.push(...data.Search));
It works as desired, granted it's static.
My current code is:
const movies = [];
function movieSearch() {
const replace = this.value;
const endpoint = 'http://www.omdbapi.com/?apikey=myAPIkey=' + replace;
movies.length = 0;
fetch(endpoint)
.then(blob => blob.json())
.then(data => movies.push(...data.Search));
}
function findMatches(wordToMatch, movies) {
return movies.filter(film => {
const regex = new RegExp(wordToMatch, 'gi');
return film.Title.match(regex) || film.Year.match(regex)
})
}
function displayMatches() {
const matchArray = findMatches(this.value, movies);
const html = matchArray.map(film => {
const regex = new RegExp(this.value, 'gi');
const titleName = film.Title.replace(regex, `<span class="hl">${this.value}</span>`)
const yearName = film.Year.replace(regex, `<span class="hl">${this.value}</span>`)
return `
<li>
<span class="name">${titleName}, ${yearName}</span>
<span class="population">${film.imdbID}</span>
</li>
`;
}).join('');
suggestions.innerHTML = html;
}
const searchInput = document.querySelector('.search');
const suggestions = document.querySelector('.suggestions');
searchInput.addEventListener('keyup', displayMatches);
searchInput.addEventListener('change', displayMatches);
searchInput.addEventListener('keyup', movieSearch);
The displayMatches function starts acting funny and sometimes returns the list items and other times doesn't. I can't figure out what's causing it. Whichever way I call my endpoint my movies array looks the same, so I'm thoroughly confused.
Any suggestions? Is there a better way to do this?
My HTML is fairly simple right now:
<form class="search-form">
<input type="text" class="search" placeholder="Movies">
<ul class="suggestions">
<li>test1</li>
<li>test2</li>
</ul>
</form>
Thanks!
(I'm trying to do this all in JS)
Edit:
An example of the JSON data when searching batman with the API:
{"Search":[{"Title":"Batman Begins","Year":"2005","imdbID":"tt0372784","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BZmUwNGU2ZmItMmRiNC00MjhlLTg5YWUtODMyNzkxODYzMmZlXkEyXkFqcGdeQXVyNTIzOTk5ODM#._V1_SX300.jpg"},{"Title":"Batman v Superman: Dawn of Justice","Year":"2016","imdbID":"tt2975590","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BYThjYzcyYzItNTVjNy00NDk0LTgwMWQtYjMwNmNlNWJhMzMyXkEyXkFqcGdeQXVyMTQxNzMzNDI#._V1_SX300.jpg"},{"Title":"Batman","Year":"1989","imdbID":"tt0096895","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTYwNjAyODIyMF5BMl5BanBnXkFtZTYwNDMwMDk2._V1_SX300.jpg"},{"Title":"Batman Returns","Year":"1992","imdbID":"tt0103776","Type":"movie","Poster":"https://ia.media-imdb.com/images/M/MV5BOGZmYzVkMmItM2NiOS00MDI3LWI4ZWQtMTg0YWZkODRkMmViXkEyXkFqcGdeQXVyODY0NzcxNw##._V1_SX300.jpg"},{"Title":"Batman Forever","Year":"1995","imdbID":"tt0112462","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNWY3M2I0YzItNzA1ZS00MzE3LThlYTEtMTg2YjNiOTYzODQ1XkEyXkFqcGdeQXVyMTQxNzMzNDI#._V1_SX300.jpg"},{"Title":"Batman & Robin","Year":"1997","imdbID":"tt0118688","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMGQ5YTM1NmMtYmIxYy00N2VmLWJhZTYtN2EwYTY3MWFhOTczXkEyXkFqcGdeQXVyNTA2NTI0MTY#._V1_SX300.jpg"},{"Title":"The Lego Batman Movie","Year":"2017","imdbID":"tt4116284","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTcyNTEyOTY0M15BMl5BanBnXkFtZTgwOTAyNzU3MDI#._V1_SX300.jpg"},{"Title":"Batman: The Animated Series","Year":"1992–1995","imdbID":"tt0103359","Type":"series","Poster":"https://m.media-amazon.com/images/M/MV5BNzI5OWU0MjYtMmMwZi00YTRiLTljMDAtODQ0ZGYxMDljN2E0XkEyXkFqcGdeQXVyNTA4NzY1MzY#._V1_SX300.jpg"},{"Title":"Batman: Under the Red Hood","Year":"2010","imdbID":"tt1569923","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BYTdlODI0YTYtNjk5ZS00YzZjLTllZjktYmYzNWM4NmI5MmMxXkEyXkFqcGdeQXVyNTA4NzY1MzY#._V1_SX300.jpg"},{"Title":"Batman: The Dark Knight Returns, Part 1","Year":"2012","imdbID":"tt2313197","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMzIxMDkxNDM2M15BMl5BanBnXkFtZTcwMDA5ODY1OQ##._V1_SX300.jpg"}],"totalResults":"344","Response":"True"}
Issues causing this behavior:
The movieSearch function is async and might not update the data in time.
The API sometimes return an error.
This solved by the code below, note that I moved everything into the fetch resolver making sure the search only executes when the API has responded.
Here is a JS Bin: https://jsbin.com/kicesivigu/1/edit?html,js,output
function findMatches(wordToMatch, movies) {
return movies.filter(film => {
console.log(film.Title, wordToMatch);
console.log(film.Title.toLowerCase().includes(wordToMatch));
return film.Title.toLowerCase().includes(wordToMatch) || film.Year.toLowerCase().includes(wordToMatch);
});
}
function displayMatches(movies, value) {
const matchArray = findMatches(value.toLowerCase(), movies);
const html = matchArray.map(film => {
const regex = new RegExp(value, 'gi');
const titleName = film.Title.replace(regex, `<span class="hl">${value}</span>`);
const yearName = film.Year.replace(regex, `<span class="hl">${value}</span>`);
return `
<li>
<span class="name">${titleName}, ${yearName}</span>
<span class="population">${film.imdbID}</span>
</li>
`;
}).join('');
suggestions.innerHTML = html;
}
const searchInput = document.querySelector('.search');
const suggestions = document.querySelector('.suggestions');
searchInput.addEventListener('keyup', () => {
const endpoint = 'https://www.omdbapi.com/?apikey=63f88e02&s=' + searchInput.value;
fetch(endpoint)
.then(blob => blob.json())
.then(data => {
console.log('response from API');
console.log(data);
if (!data.Error) displayMatches(data.Search, searchInput.value);
});
});

await in nested for ... of loop

async traverse(url) {
const ts = new TournamentScraper()
const ms = new MatchScraper()
const results = []
const tournaments = await ts.run(url)
for(let href of tournaments.map(t => t.href)){
let matches = await ms.run(href)
let pages = ms.getPages()
let seasons = ms.getSeasons()
//console.log(pages)
//console.log(seasons)
results.push(matches)
for(let href of pages) {
//console.log(href)
matches = await ms.run(href)
//console.log(matches)
results.push(matches)
}
}
return results
}
TournamentScraper returns an array of objects, which typically looks like this:
{name: 'Foo', href: 'www.example.org/tournaments/foo/'}
The link points to the tournament's last season's first page. This page contains the links to the other seasons and a paginator (if any).
MatchScraper's run returns some data, and sets the instance's dom property. getPages() and getSeasons() consumes this property and each returns an array of links.
The problem that results contains only the first batch of matches. I can see the 2nd page's matches in the console log, but they are not in the results array when traverse returns.
I found this rule which is against await in for loop. The problem, that I have to wait for ms.run(href), because it sets dom, and getPages() and getSeasons() needs it to be set, to extract the needed links.
I think this should work. It utilizes Promise all rather than for loops
const run = href => ms.run(href);
async function getMatches(href) {
const out = [];
const matches = await run(href);
const pages = ms.getPages();
out.push(matches);
if(pages.length) {
const pageResults = await Promise.all(pages.map(href => run(href)));
out.push(...pageResults);
}
return out;
}
async function traverse(url) {
const ts = new TournamentScraper();
const ms = new MatchScraper();
const tournaments = await ts.run(url)
const matches = await Promise.all(tournaments.map(t => getMatches(t.href)));
return matches.reduce((a, b) => {
a.push(...b);
return a;
}, []);
}

Categories

Resources