click to view as a presentation
Students Will Be Able To:
- Describe the Use Case for AJAX
- Use the
fetch
API to make AJAX requests to the Puppies API - Use ES2017's
async
/await
to handle promises synchronously
- Setup
- AJAX - What & Why
- Make an HTTP Request Using the Fetch API
- Use ES2017's
async
/await
to Handle Promises - Function Expressions Can Use
await
Too - 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
GET
request to the/users
endpoint 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
fetch
method 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
text
orjson
method 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
fetch
any further, let's see how to use a fantastic new way of working with promises:
async & await -
The purpose of
async
/await
is to allow us to work with asynchronous code almost as if it were synchronous!
-
We use the
async
declaration to mark a function as asynchronous when promises are going to be handled usingawait
within it. -
We can re-write our code to use
async
/await
like 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
await
operator causes the line of code withfetch
to "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
/catch
block: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
catch
block would run if thefetch
failed.
-
So basically, we've seen that
async
/await
replaces the.then(<function>)
method for when a promise resolves. -
In addition, JavaScript
try
/catch
blocks replace the.catch(<function>)
for error handling when a promise is rejected.
-
After the
console.log(users)
, add another AJAX request usingfetch
to JSONPlaceholder's/posts
endpoint. -
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
async
function always returns a promise - not the expression in thereturn
statement. -
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
await
as 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
getUsers
returns 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-view
orcreate-view
(which we'll add in a bit) according to acurrentView
state 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
init
function's responsibility.
-
Let's initialize the state in the
init
function:async function init() { currentView = 'index'; puppies = await fetch(BASE_URL).then(res => res.json()); render(); }
-
Don't forget to add the
async
declaration in front offunction init() {
. -
Next, we'll add some code to the
render
function...
-
Here's our
render
function 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,
reduce
is 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-view
element to cached elements:const createViewEl = document.getElementById('create-view');
-
Now we can update the
render
function 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
inputEls
will be an HTMLCollection of elements that we can access using square bracket notation and evenforEach
over.
-
So far we've used
fetch
to 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
handleAddPuppy
function 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
init
function 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
delete
andupdate
functionality! -
Let's wrap up with a couple of review questions...
-
async/await
provides another way to work with ________? -
Which of the following scenarios can
fetch
be 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.