click to view as a presentation
Students Will Be Able To:
- Describe the Use Case for AJAX
- Use the
fetchAPI to make AJAX requests to the Puppies API - Use ES2017's
async/awaitto handle promises synchronously
- Setup
- AJAX - What & Why
- Make an HTTP Request Using the Fetch API
- Use ES2017's
async/awaitto Handle Promises - Function Expressions Can Use
awaitToo - Let's Build a Puppy SPA
- Using Other HTTP Methods with Fetch
- Essential Questions
-
We'll be using Repl.it during this lesson to learn about AJAX and
async/await. -
Create a new HTML, CSS, JS repl and name it something like AJAX with Fetch.
-
AJAX is short for Asynchronous JavaScript And XML.
-
In case you're wondering what the XML is about... It's the granddaddy of all markup languages, including HTML.
-
Once upon a time, XML was the de facto format for transferring data between two computers - that's why it's in the name AJAX. However, JSON has since become the data transfer format of choice.
-
Clients (browsers) use AJAX to make HTTP requests using JavaScript.
-
The browser can send AJAX requests to any API server, as long as the server is CORS compliant.
-
Using AJAX, we can send an HTTP request that uses any HTTP method including,
GET,POST,PUT&DELETE- no need formethod-override!
-
But, here's the best part - unlike when we click a link or submit a form on a web page, AJAX does not trigger a page reload!
-
We can use AJAX to communicate with servers to do lots of things, including to read, create, update & delete data without the user seeing a page refresh.
-
AJAX has made possible the modern-day Single Page Application (SPA) like what you're going to build during this unit!
-
AJAX was originally made possible back in 1998 when IE5 introduced the
XMLHttpRequest(XHR) object and today it's in all browsers. However, it's a bit clunky to use. -
One of the reasons jQuery became popular was because it made making AJAX requests easier.
-
However, we no longer have to use the XHR object or load jQuery to make AJAX calls thanks to the Fetch API which is part of the collection of Web APIs included in modern browsers.
-
So, the A in AJAX stands for asynchronous.
-
Indeed, making an AJAX request is an asynchronous operation. So far, we've seen two approaches that enable us to run code after an asynchronous operation has completed.
β What are they?
-
The Fetch API, like any new Web API asynchronous method, uses promises instead of callbacks.
-
Let's make a
GETrequest to the/usersendpoint of JSONPlaceholder, a fake RESTful API for developers:fetch('https://jsonplaceholder.typicode.com/users') .then(response => console.log(response))
When ran, we'll see that the
fetchmethod returns a promise that resolves to a Fetch Response object, which has properties such asstatus, etc.
-
To obtain the data in the body of the response, we need to call either the
textorjsonmethod which returns yet another promise:// fetch defaults to making a GET request fetch('https://jsonplaceholder.typicode.com/users') .then(response => response.json()) .then(users => console.log(users))
As you can see, the
json()method returns a promise that resolves to the data returned by the server, as JSON.
-
Before we continue to use
fetchany further, let's see how to use a fantastic new way of working with promises:
async & await -
The purpose of
async/awaitis to allow us to work with asynchronous code almost as if it were synchronous!
-
We use the
asyncdeclaration to mark a function as asynchronous when promises are going to be handled usingawaitwithin it. -
We can re-write our code to use
async/awaitlike this:async function printUsers() { const endpoint = 'https://jsonplaceholder.typicode.com/users'; let users = await fetch(endpoint).then(res => res.json()); console.log(users); } printUsers();
The
awaitoperator causes the line of code withfetchto "pause" until the promise resolves with its value - in this case an array of users.
-
When using
async/await, we cannot use a.catch()to handle a promise's rejection, instead we use JavaScripts'stry/catchblock:async function printUsers() { const endpoint = 'https://jsonplaceholder.typicode.com/users'; let users; try { users = await fetch(endpoint).then(res => res.json()); console.log(users); } catch(err) { console.log(err); } }
The
catchblock would run if thefetchfailed.
-
So basically, we've seen that
async/awaitreplaces the.then(<function>)method for when a promise resolves. -
In addition, JavaScript
try/catchblocks replace the.catch(<function>)for error handling when a promise is rejected.
-
After the
console.log(users), add another AJAX request usingfetchto JSONPlaceholder's/postsendpoint. -
Log out the returned posts.
-
Node.js also has
async/await, so you can now work with Mongoose code like this:async function index(req, res) { const movies = await Movie.find({}); res.render('movies/index', { title: 'All Movies', movies }); }
Instead of this:
function index(req, res) { Movie.find({}).then(function(movies) { res.render('movies/index', { title: 'All Movies', movies }); }); }
-
Why is AJAX required to be able to build Single Page Applications like Gmail?
-
What is wrong with the following code?
function show(req, res) { const movie = await Movie.findById(req.params.id); res.render('movies/show', { title: 'Movie Detail', movie }); }
Hint: Something is missing
-
Note that an
asyncfunction always returns a promise - not the expression in thereturnstatement. -
For example, the following code will not work:
async function getUsers() { const endpoint = 'https://jsonplaceholder.typicode.com/users'; let users; try { users = await fetch(endpoint).then(res => res.json()); return users; } catch(err) { console.log(err); } } let users = getUsers(); console.log(users);
Note that a Promise was logged out instead of the users.
-
You might try to simply add an
awaitas follows:let users = await getUsers();
-
However, the error says it all:
SyntaxError: await is only valid in async function... -
The solution is to wrap the code within an "async" immediately invoked function expression (IIFE)...
-
Function expressions can also use
await:(async function() { let users = await getUsers(); console.log(users); })();
Now, whatever
getUsersreturns will be assigned tousers. -
Basically any function can be declared as
async, including callbacks, arrow functions, etc.
-
Let's build an ugly (no CSS) little SPA that uses the RESTful Puppies API we built in Unit 2.
-
Upon loading, the app will fetch and display all puppies.
-
We'll also include create functionality.
-
The Puppies RESTful API (code in the lesson's folder) has been deployed to Heroku at this URL:
https://sei-puppies-api.herokuapp.com/and has the following endpoints:
Endpoint CRUD Operation GET /api/puppies Index GET /api/puppies/:id Show POST /api/puppies Create PUT /api/puppies/:id Update DELETE /api/puppies/:id Delete
-
Now, a little markup for navigation and the Puppies List "view":
<body> <nav> <button id="index-view-btn">List Puppies</button> <button id="create-view-btn">Add a Puppy</button> </nav> <main id="index-view"> <h1>Puppies List</h1> <section></section> </main> <script src="script.js"></script> </body>
-
Our SPA's JS will hide/show either the
index-vieworcreate-view(which we'll add in a bit) according to acurrentViewstate variable.
-
Let's structure the initial JavaScript:
/*-- constants --*/ const BASE_URL = 'https://sei-puppies-api.herokuapp.com/api/puppies/'; /*-- cached elements --*/ const indexViewEl = document.getElementById('index-view'); const listContainerEl = document.querySelector('#index-view section'); /*-- functions --*/ init(); function init() { render(); } function render() { }
-
Does the structure look familiar? π
-
We're also going to need to define some variables to hold the app's state:
/*-- app's state vars --*/ let currentView, puppies; /*-- cached elements --*/
-
Remember, we just define the variables - initializing their values is the
initfunction's responsibility.
-
Let's initialize the state in the
initfunction:async function init() { currentView = 'index'; puppies = await fetch(BASE_URL).then(res => res.json()); render(); }
-
Don't forget to add the
asyncdeclaration in front offunction init() {. -
Next, we'll add some code to the
renderfunction...
-
Here's our
renderfunction so far:function render() { indexViewEl.style.display = currentView === 'index' ? 'block' : 'none'; if (currentView === 'index') { let html = puppies.reduce((html, pup) => html + `<div>${pup.name} (${pup.breed}) - age ${pup.age}</div>`, ''); listContainerEl.innerHTML = html; } else if (currentView === 'create') { // TODO } }
-
Since we want a single value, a string, from an array,
reduceis the most suitable iterator method. -
The list of puppies should now be rendering.
-
Now we're going to build the Add a Puppy functionality.
-
Let's start by adding an event listener for when the [Add a Puppy] button is clicked:
document.getElementById('create-view-btn') .addEventListener('click', function() { // Update state, call render... currentView = 'create'; render(); });
-
Yup, in response to user interaction, we update state and call
render().
-
Next up, let's add some markup for the create view:
</main> <!-- new html below --> <main id="create-view"> <h1>Add a Puppy</h1> <section> <div>Name: <input></div> <div>Breed: <input></div> <div>Age: <input type="number"></div> <button id="add-puppy-btn">Add Puppy</button> </section> </main>
-
Note that since we never submit forms in a SPA, they are not required. However, they can be beneficial for performing validation and styling when using a CSS framework.
-
Let's add the
create-viewelement to cached elements:const createViewEl = document.getElementById('create-view');
-
Now we can update the
renderfunction to show only the "current" view:indexViewEl.style.display = currentView === 'index' ? 'block' : 'none'; // Add code below createViewEl.style.display = currentView === 'create' ? 'block' : 'none';
-
Add the following in the event listeners section:
document.getElementById('add-puppy-btn') .addEventListener('click', handleAddPuppy);
-
Let's also cache the
<input>elements to make it easier to access their data:const inputEls = document.querySelectorAll('#create-view input');
Note that
inputElswill be an HTMLCollection of elements that we can access using square bracket notation and evenforEachover.
-
So far we've used
fetchto issue only a basic GET request without a data payload. -
By providing a second "options" argument, we're able to specify the HTTP method of the request, include a data payload in the body of the request, set headers, etc.
-
Next, lets code the
handleAddPuppyfunction that sends the new puppy's data to the server as JSON using a POST request...
-
We'll review as we type the following code:
async function handleAddPuppy() { // Ensure there's a name entered if (inputEls[0].value) { let newPup = await fetch(BASE_URL, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ name: inputEls[0].value, breed: inputEls[1].value, age: inputEls[2].value }) }).then(res => res.json()); alert(`Pup added has an id of ${newPup._id}`); // Clear the inputs inputEls[0].value = inputEls[1].value = inputEls[2].value = ''; } }
-
All that's left is to write the code for when the [List Puppies] button is clicked.
-
Since we want to do exactly what the
initfunction does, let's cheat a bit:document.getElementById('index-view-btn') .addEventListener('click', init);
-
Congrats on writing an ugly little SPA!
-
Now that you know how to send AJAX requests to a server's API, why not challenge yourself by implementing both
deleteandupdatefunctionality! -
Let's wrap up with a couple of review questions...
-
async/awaitprovides another way to work with ________? -
Which of the following scenarios can
fetchbe used for?- Creating a new movie in an app's database without refreshing the page.
- Deleting a fun fact about a student from an app's database without refreshing the page.
- Submitting a form to create a cat and redirecting to the cats index page.
