Module 4: Consuming Data From the Network
Asynchronous JavaScript and Promises
When we introduced timers and events in past modules, we discussed the concept of 'asynchronous' code. Asynchronous, or async, code runs independently of other code and is an integral part of JS development. Async code is often associated with tasks running "in the background" or in parallel with other tasks.
Before jumping into async programming, you should be familiar with three central concepts of JS async programming that you should understand before we move on.
Events trigger tasks
A typical JS application is composed of individual tasks triggered by events. Developers will define a task (usually as a function) and the task's corresponding event via callbacks.
You have already learned how to use callbacks with event listeners:
document.body.addEventListener('click', handleClick);
The code above associates the handleClick callback to the click on the body event. When the user clicks on the body of the page, the JS runtime engine that monitors click events calls the handleClick function.
Pro-tip: Some tasks are not part of the application but "run in the background" when triggered by the browser runtime. Some of these background tasks include refreshing the screen every 1/60 of a second and updating the pointer position when the user moves the mouse.
How about setTimeout:
setTimeout( () => {
console.log('Hello!');
}, 1000);
console.log('Over here!');
This code assigns an inline callback to a timer event, which triggers at 1000 milliseconds (1 second). That's why "Over here!" is logged before "Hello!". Again, the JavaScript runtime engine tracks the timer in parallel with your code, so you can do other things while waiting.
In both examples, an event caused some code to execute, and the code ran asynchronously, allowing other code to run simultaneously.
Now, let's talk about the second central concept of async code: blocking.
Blocking code
Every line of code requires a certain amount of time to complete, and the runtime engine executes each line sequentially. During the execution of a task, the application will "block" or stop other tasks from running.
If the currently running task is too long, other events that wish to execute will accumulate in a queue. As a result, the application can become sluggish, non-responsive, or even break. As a developer, you have to try and design efficient, short, and fast functions to give room for other tasks to run when needed.
However, building code that executes quickly is not always possible with synchronous code. Many everyday tasks, such as fetching large amounts of data, can easily take over a second. For longer-running functions, you must use special async non-blocking functions designed to run "in parallel" with other tasks and won't block the app.
We won't cover how to write non-blocking functions in this module, but you need to understand that they exist.
Some tasks depend on other tasks
A task may need to wait for another task to finish before executing.
A classic example is parsing data from the disk. You can only manipulate data once it's fully loaded, but loading data from the disk might take a lot of time. The solution is to use an asynchronous function that can run in the background (non-blocking) and then have it invoke the next task when it's finished.
Invoking a function after a long-running function is common in JS. Many native and third-party async functions expect to receive a callback as an argument to call when the task is completed. Here's an example using a function from the fs file-system library:
const fs = require('fs'); // this will "import" the fs library/object and all its methods
const readFromDisk = (file) => {
fs.readFile(file, 'utf8', (err, data) => {
// this is the callback that's called when readFile finishes
if (err) {
console.log("Error:", err);
}
else {
console.log(data);
}
});
};
When readFromDisk is called, the file parameter is passed to readFile. readFile receives an inline callback. This callback is triggered when the code loads the file from disk. Notice that readFile will always send two arguments to the callback. The number of arguments changes with each callback, so code your callback to receive and handle the arguments properly.
The async readFile function is non-blocking, so it won't prevent the application from running other tasks while the file is loaded. Under the hood, readFromDisk schedules the callback and immediately returns. That's why the app can continue to run code and handle events.
Pro-tip: Remember, we are talking about "computer time" here. If loading a file from disk takes 200ms and blocks the application, it may prevent dozens of tasks from running!
Take a moment to review these three concepts above. For the remainder of this module, you will concentrate on the third concept and learn how to use async functions to nest tasks using different approaches.
Using functions that return promises
Nesting functions in JS is a design pattern that guarantees the order of execution of tasks. "Producer" functions send its results to a "consumer" function so it can execute. Granted, we specifically refer to async producers that take considerable time to run.
The traditional way of nesting tasks is to use callbacks. There are plenty of native and third-party async functions that expect to receive a callback (consumer) as an argument.
To simulate an async function, we will use setTimeout in many of our examples. setTimeout is an async function and will immediately return to allow the app to continue execution and be available for other tasks. In parallel, setTimeout will keep track of the timer and eventually trigger the callback passed as an argument. Take a moment to understand the following example:
// a traditional async function will receive the callback (consumer) as an argument
function producer(DataIn, consumer) {
console.log("Producer was triggered and received:", DataIn);
console.log("Async task will take some time...");
// The code below is simulating an async task, by using the async setTimeout() function
setTimeout(() => {
const DataOut = "Produced value";
console.log("Producer task finished");
console.log("Calling consumer with data");
// when the async task is finished, the callback is invoked to consume the data produced
consumer(DataOut);
}, 3000);
}
function consumer(DataIn) {
console.log("Consumer was triggered and received:", DataIn);
}
const data = "Initial value";
producer(data, consumer);
console.log(
"This is another independent task, executed while the producer is running"
);
This traditional way of nesting callbacks works fine but can lead to code that's hard to write, read, and maintain, particularly when the nesting is deep (e.g., a callback invokes a callback, which invokes another callback, and so on).
Fortunately, modern JS offers other approaches to work with nested callbacks. In practice, these alternatives accomplish the same thing but are much easier to deal with. One of the more popular approaches is using Promises. Many popular JS libraries have been upgraded over the last few years to work with Promises, so let's understand how they work.
Promises
A promise returned by a function is a proxy for a value not yet known. This lets asynchronous functions act like regular synchronous functions by immediately returning a value. However, instead of returning the final value, the function returns a "promise" to supply the future value, effectively yielding control back to the application. The async task will continue to execute in parallel, and the consumer will be triggered when it's finished.
Let's re-write the same producer function from above to see how an async function returns a promise:
function producer(DataIn) {
console.log("Producer was triggered and received:", DataIn);
console.log("Async task will take some time...");
// this async producer returns a promise, so the app can continue executing
return new Promise((resolve) => {
// The code below is simulating an async task, by using the async setTimeout() function
setTimeout(() => {
console.log("Producer task finished");
console.log("Data available to consumer");
const DataOut = "Produced value"
// The async task will call resolve() to signal that the task is finished
resolve(DataOut);
}, 3000);
});
}
const data = "Initial value";
myPromise = producer(data); // future code to consume the promise
console.log(
"This is another task, executed while the producer is running"
);
While the code above is performing the same thing as the first example that uses a callback, notice these key differences:
- The producer does not receive a callback
- Instead, the producer returns a Promise object, which is running some async code
- The application continues and runs other tasks
- At some point, when the async task finishes, the resolve method from the promise is called with the data created by the producer
- Any consumer monitoring the returned promise will be able to access the data sent by the producer and consume it.
Pro Tip: it is fundamental to understand that if you have synchronous code inside the return new Promise() statement, it will still have to finish execution before the promise returns to the caller. Any pending tasks in the queue execute only when the promise returns. That's why we are using setTimeout, a true async function that will allow the promise to be returned and continue execution in the background.
How to Build It
For the Kolors app, you already know you will fetch data from a remote server containing palettes with color codes, names, and additional information. With your understanding of promises and async functions, you can visualize that some tasks in the app will be async and that some other tasks will have to wait until data is available.
You design this temporary code, which should run upon page load:
const initializePalettes = (url) => {
// use an async function to fetch JSON from url
// async function will return a promise
// You will later consume the promise to be able to parse the JSON data and update the colors of all the bars/rectangles on the screen
}
You notice there is more pseudo-code than actual code in your plan. Not to worry! As you go through this module, you'll iterate over this function a few times until it is complete, concise, and functional.
Handling success with .then()
Previously, when you used the async function that takes a callback, the code looked like this:
function consumer(DataIn) {
console.log("Consumer was triggered and received:", DataIn);
}
const data = "Initial value";
producer(data, consumer);
Now, for the new version that uses promises, you will write the following code:
function consumer(DataIn) {
console.log("Consumer was triggered and received:", DataIn);
}
const data = "Initial value";
const myPromise = producer(data);
myPromise.then(consumer(data));
From the development perspective, the only difference is that now you are receiving a promise from the
producer and using its .then method to nest the consumer callback. The final behavior of the code is the same.
The myPromise.then(consumer(data));
line of code will simply "schedule" the consumer to be
triggered when the promise "resolves", and does not block the application.
As a developer, you must know if the async function receives a callback or returns a promise.
Here's a common real-world scenario:
const fs = require('fs').promises; // import the file-system library that uses promises
fs.readFile(file, 'utf-8')
.then(data => { console.log('File content:', data); });
Let's follow the execution step-by-step:
- The readFile function is called and immediately returns a promise object whose value is still "pending".
- The .then method is called to assign the consumer, signaling to the application that it wants to execute code when, and only when, the promise is fulfilled.
- Eventually, the promise fulfills and calls the inline consumer, passing data.
And that's how you consume a promise!
Chaining tasks
Often, your application will need to chain multiple consumers, which also act as producers (except for the last one in the chain). For example, you may need to fetch some JSON data from a remote server, then parse it into a JS object, then search for a particular item inside the object, manipulate it with some complex arithmetic, update a local database on local storage, and finally update the DOM (phew!).
You can chain consumers by using the following syntax:
function producer(data) {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Value from producer");
}, 2000);
});
}
function consumer1(data) {
return "Value from consumer1";
}
function consumer2(data) {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Value from consumer2");
}, 2000);
});
}
function consumer3(data) {
console.log("Chain finished!");
}
producer("Initial value")
.then(consumer1)
.then(consumer2)
.then(consumer3);
console.log("Chain triggered!");
Let's analyze the execution step-by-step:
- Producer is called with initial value and returns a promise. The app is free to execute other tasks.
- Producer finishes the async task and sends the resulting value to consumer1 through its resolve.
- Consumer1 is a sync producer, runs a fast task (say, simple math operation), and immediately sends the resulting value to consumer2 through return. The app is free to execute other tasks.
- Consumer2 is an async producer and returns a promise. The app is free to execute other tasks.
- Consumer2 finishes the async task and sends the resulting value to consumer3 through its resolve.
- Consumer3 finishes the chain by using the final value to update the DOM.
Notice how you can mix sync and async code using the same .then syntax. Very powerful and easy to read!
Compare the promise syntax with the traditional way of passing callbacks:
Promises:
producer("Initial value")
.then(consumer1)
.then(consumer2)
.then(consumer3);
Callbacks:
producer(DataIn, consumer1(data1, consumer2(data2, consumer3(data3))));
Which one is easier to read?
Waiting for multiple promises
Sometimes, a consumer may have to wait for multiple promises to fulfill before executing. For example, the application might need to retrieve data for three different users from a remote server and then create a new element in the DOM by combining all the information.
For cases like this, you can also use the helpful Promise.all() method:
const promise1 = producer1(data1);
const promise2 = producer2(data2);
const promise3 = producer3(data3);
const consumer = (data) => { //consumer code };
Promise.all([promise1, promise2, promise3])
.then(consumer);
The .all() method receives an array with references for all producers. It doesn't matter how long each task takes to finish or the order of completion. What matters is that the consumer is called only when all promises fulfill!
How you handle async non-blocking code is most important, not how to implement it. As a developer, you must understand that async functions either receive callbacks or return a promise.
How to Build It
Returning to the previous initialization code for the Kolors app, you know you need to call a function that fetches the data first. You don't know how to fetch data yet, but you know the fetch function will return a promise. With that knowledge, you at least can code the chaining portion:
const initializePalettes = (url) => {
const fetchPromise = // async function to fetch JSON from url
fetchPromise
.then(// code to parse JSON into a JS object)
.then(// code to iterate over the object, while changing the background color of the rectangles)
}
Handling failure with .catch()
Internally, async functions that return promises usually implement error handling by defining a second parameter (reject) when creating the promise object:
function producer(data) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const error = false;
if (!error) {
const value = "Produced value";
resolve("Task successful: " + value);
} else {
errorMessage = "Something went wrong";
reject("Task failed: " + errorMessage);
}
}, 2000);
});
}
The logic of the async function is solely responsible for calling either resolve or reject. On the consumer side, you will create a "happy" and a "sad" path to deal with each case:
producer("Initial data")
.then((ret) => console.log("Success:",ret))
.catch((ret) => console.log("Error:",ret));
In case of a failure, the .catch method captures whatever is sent by reject.
As long as you know what data the async functions expect to receive as arguments and what type of data it returns via resolve and reject, you can properly write a consumer function to manipulate the data.
Pro tip: many async functions return a JS Error object when the promise rejects.
You can also use .catch with Promise.all():
Promise.all([promise1, promise2, promise3])
.then(happy_consumer)
.catch(sad_consumer);
Any failure in any of the promises will trigger the callback inside .catch. In other words, Promise.all() will only trigger the consumer inside .then if all promises resolve.
How to Build It
What if the Kolors' server is out of service? What if the network connection is down? It would help if you implemented error handling so the user knows what's going on and the app can gracefully recover from unexpected situations:
const initializePalettes = (url) => {
const fetchPromise = fetch(url)
fetchPromise
.then(response => response.json())
.then(data => {
data.forEach((palette, index) => {
const colorBars = document.querySelectorAll(
"#palette" + index + " .rectangle"
);
colorBars.forEach((bar, i) => {
bar.style.backgroundColor = palette[i];
});
});
})
.catch(error => {
console.error("Failed to fetch palettes:", error);
alert("Unable to load color palettes. Retrying in 30 seconds...");
setTimeout(() => initializePalettes(url), 30000);
});
}
Hopefully, the async function that you choose to fetch data will be able to send back error data and use .catch.
Using async await for more readable code
Promises can make the code easier to read, write, and maintain than traditional callback-passing. Nevertheless, JS offers an alternative "syntax sugar" for dealing with promises that some developers prefer.
The "async/await" syntax attempts to simplify code by making the code look "synchronous" or "continuous". You will notice, however, that you will still be dealing with promises under the hood. Async functions used with this syntax should still return promises.
Let's compare the same code using the two syntaxes.
then/catch:
producer(data)
.then(consumer1)
.then(consumer2)
.then(value=>console.log(value)); // this will log when all promises are fulfilled.
console.log("Some other task"); // this will log after `producer` returns the first promise, even if it is still pending (not resolved).
async/await:
async function chainedTask(data) {
const data1 = await producer(data);
const data2 = await consumer1(data1);
const value = await consumer2(data2);
console.log(value); // this will log when all promises are fulfilled.
}
chainedTask("Some data");
console.log("Some other task"); // this will log after `producer` returns the first promise, even if it is still pending (not resolved).
Again, both examples behave the same; only the syntax is different. Declaring a function with the async keyword does not magically transform the code inside of it into non-blocking. Every async sub-task inside the wrapper async function still needs to return a promise to yield control to the application and allow the program to execute pending tasks.
Like each .then statement executes after the previous .then, each line in the wrapper function will execute after the previous await is resolved.
An await statement always returns a promise, and the execution moves to the next line when the promise resolves. So you could get the same result as above by re-writing the code to mix .then, async, and await:
async function chainedTask(data) {
const data1 = await producer(data);
const data2 = await consumer1(data1);
const value = await consumer2(data2); // next line will execute when consumer2 resolves.
return value;
}
chainedTask("Some Data")
.then(value=>console.log(value)); // this callback will be triggered when `chainedTask` returns `value`, as `value` is a fulfilled promise at that point.
Using async/await does require more initial code and an extra wrapper function. But if you are writing libraries that use async functions deeply nested and declare them in a separate file, it may be easier and more intuitive just to call one function in your main app and continue with other tasks:
chainedTask("Some Data"); // complex nesting of async functions
console.log("Some other task"); // this will log while `chainedTask` is still pending.
Handling errors is also different with async/await. Instead of using the .catch method as below:
producer(data)
.then(consumer1)
.then(consumer2)
.then(value=>console.log(value))
.catch(handleError);
With the async/await approach, you will have to resort to the traditional JS try/catch:
async function chainedTask(data) {
try {
const data1 = await producer(data);
const data2 = await consumer1(data1);
console.log( await consumer2(data2) );
}
catch(err) {
handleError(err);
}
}
How to Build It
You are ready to start implementing the fetch() function to get the color data from the server. The server will return a JSON data set with all the color information, which you will parse and use to populate the color bars.
First, you create a function that will fetch the data from the server:
async function getColors() {
try {
const response = await fetch('https://api.kolor.com/colors');
const colors = await response.json();
return colors;
}
catch(err) {
console.log('Error fetching colors:', err);
}
}
Then, you can use this function to populate the color bars:
async function changePalette(id_number) {
try {
const colors = await getColors();
const colorBars = document.querySelectorAll(
"#palette" + id_number + " .rectangle"
);
colorBars.forEach((bar, index) => {
bar.style.backgroundColor = colors[index].hex;
bar.title = `${colors[index].name}\nCMYK: ${colors[index].cmyk}\nRGB: ${colors[index].rgb}`;
});
}
catch(err) {
console.log('Error changing palette:', err);
}
}
The code above uses async/await to handle the asynchronous fetch operation. When the data arrives, it updates the color bars with the new colors and their information. If there is an error, it will be caught and logged to the console.
Conclusion
Async code and JavaScript are inseparable! From simple event handlers to complex chains of non-blocking code, virtually every web application relies on callbacks and promises.
Whether you need to fetch remote data, query large databases, or perform complex arithmetic, always remember that there are many great libraries available for your async needs. And most of them have already been updated to work with promises.
Speaking of promises, JS is kind enough to let you choose different syntaxes. As long as you understand that the two approaches accomplish the same thing, it's only a matter of preference. But try to keep it consistent. Mixing async/await and then/catch in the same application can make your code hard to read.
HTTP Communication and Fetching Data
Alright, we have covered a lot of concepts in the first Core Competency. Now let's have some fun and actually retrieve data from remote servers!
Web applications often rely on sending and retrieving across the Internet. In this Core Competency, you will learn about HTTP methods, and how to use them to read, write, update, or delete data from web servers.
Using the HTTP protocol
HTTP is a network protocol: a set of rules governing how web clients, like a browser, communicate with web servers over the internet.
Most content servers in the Internet "speak" HTTP and expect to receive HTTP-formatted messages containing a method (type of request), a possible payload (e.g. arguments passed to the method), along with headers that specify things like type of data, protocol version etc.
Similarly, HTTP clients expect to receive responses containing specific components, such as a Status Code (request successful, resource not found, etc.), headers, and, in most cases, the payload with the data returned by the server.
If the client is a browser accessing a web page, payloads will typically be the HTML, CSS, or JS code necessary to build the page in the browser. After the application is running in the browser, at some point, the JS script may request additional data from the server, such as some JSON data to be manipulated by the app.
Regardless of the payload type and who is actually requesting the data from the server, all the communication back and forth happens under the HTTP protocol.
The following is a breakdown of the different components of the HTTP protocol.
Endpoints
Every web server has at least one endpoint (or URL) to which requests from clients are sent. Endpoints are like directories containing resources (files) the client can access.
Example of an endpoint: https://api.twitter.com/2/tweets/
Although you should be able to directly access some endpoints by typing the URL in the browser's address bar (and actually see the JSON response), these URLs are usually not supposed to be linked in a page or typed in the address bar. Your JS script will use them to get data programmatically.
HTTP Methods
Four basic HTTP methods can be sent to an endpoint:
- GET: to request a specific resource (file/data/payload)
- POST (or PUT): to create a new resource. Although originally designed to function as the "write" method, POST can also be used to send any information to the server, such as passwords, to request payloads that need authorization.
- UPDATE: to change a specific resource on the server
- DELETE: to erase a specific resource on the server
Most of the time, clients will use GET to ask an endpoint for a specific resource (i.e., index.html).
Status codes
HTTP Status Codes are used to indicate if a request has been successful or not and why. The server will include the status code in all responses sent back to the client.
Status codes are divided into series:
- 1xx – Informational Response.
- 2xx – Success (This status code depicts that the server has fulfilled the request made and the expected response has been achieved).
- 3xx – Redirection (The requested URL is redirected elsewhere).
- 4xx – Client Errors (This indicates that the page is not found).
- 5xx – Server Errors (A request made by the client but the server fails to complete the request).
Some of the most common status codes are:
- 200 (SUCCESS/OK)
- 307 (Temporary Redirect)
- 400 (Bad Request)
- 401 (Unauthorized Error)
- 404 (Not Found)
- 500 (Internal Server Error)
How to send HTTP requests
HTTP messages can be created and sent in many different ways. What matters is that the HTTP message can reach the endpoint and contains at least the method, the endpoint URL, and the headers.
In the next Learning Objective, you will explore some of the possibilities that can be used to send HTTP requests.
How to Build It
Fortunately, you have been informed by the PM that the endpoints created by the back-end developer are HTTP-compliant.
According to server specifications, only one endpoint is available. If you send a GET request to https://webapis.bloomtechdev.com/palettes, you should get a JSON containing 30 popular palettes, with five hex color codes each. That sounds great, and you can't wait to test it! In fact, you can put the url into your browser's address bar and see what the payload looks like.
You could use the endpoint to load all the palettes when the app is initialized. By understanding what type of data an endpoint returns, you can better design the application from the start.
Using HTTP methods
As mentioned before, whenever your browser opens a page, it sends multiple requests to a web server until it gets all the resources needed to load the page. To give you an idea of all the work the browser is automating, here's a high-level overview:
- The browser gets the endpoint (and eventual resource) typed by the user in the address bar
- If no specific resource is specified, will let the server decide what resource to send back
- Creates and sends an HTTP GET message
- Receives and executes payload (e.g. HTML code)
- Creates additional messages for all of the images on the page, the linked CSS and JS files, etc.
- Requests additional resources
- Loads the page
And just like that, you will get a working page/application in your browser without worrying about creating and sending HTTP messages.
However, as a developer, you need to learn how to create and send HTTP messages because, sooner or later, your application will need to retrieve resources from remote servers.
A data endpoint can be programmed to send back any information (payload). For instance, a developer may have configured a server to send a JSON array with all clients when it receives a GET message in the following endpoint: https://server.com/clients, effectively returning the payload below:
[
{"id": 001, "name": "John Doe", "age": 45},
{"id": 002, "name": "Jane Foe", "age": 35},
...
{"id": 099, "name": "Mark Twain", "age": 27}
]
The server may also send back data about client #1 through the following endpoint: https://server.com/client/1:
{"id": 001, "name": "John Doe", "age": 45}
How the server interprets the URL - and what it sends back - is entirely up to the code running in the server. This behavior is known as the server API (Application Programming Interface), and it will dictate how the client needs to format an HTTP request.
Now that you understand that each data server has a unique API (although many follow the same basic structure), let's learn how to send HTTP GET requests and retrieve some actual data to manipulate in your application.
One of the tools for sending HTTP messages is called curl. Curl is a universal command line tool for any operating system. It's a simple enough tool to use; you'll see a few examples here. Curl is not required but useful, as you will see below. You can learn all about curl by accessing this page.
If you have curl installed in your system, you can go to the terminal and test it:
- To get the home page's HTML content from google.com
curl https://www.google.com
- To get only the header information (notice the 200 Status Code in the first line of the response):
curl -I https://www.google.com
- To get much more details about the connection and the data exchanged (verbose mode):
curl -I -v https://www.google.com
Pro Tip: The curl tool is easy to use and helpful during development. Remember that you can also use Chrome's dev tools to get detailed information about the HTTP messages processed by your browser. Check the "Network" and "Sources" tabs!
How to Build It
With the Kolors API endpoint in hand, you use curl to hit the endpoint to understand precisely how the JSON data is structured. You first check if your machine has curl installed, and it does!
You type:
curl https://webapis.bloomtechdev.com/palettes
And sure enough, you get a long JSON array of data in your terminal (click the expand arrow below to see the data):
JSON
'[[{"hex":"#F44336","name":"Red"},{"hex":"#E91E63","name":"Pink"},{"hex":"#9C27B0","name":"Purple"},{"hex":"#673AB7","name":"Deep Purple"},{"hex":"#3F51B5","name":"Indigo"}],[{"hex":"#1ABC9C","name":"Turquoise"},{"hex":"#2ECC71","name":"Emerald"},{"hex":"#3498DB","name":"Peter River"},{"hex":"#9B59B6","name":"Amethyst"},{"hex":"#34495E","name":"Wet Asphalt"}],[{"hex":"#B58900","name":"Yellow"},{"hex":"#CB4B16","name":"Orange"},{"hex":"#DC322F","name":"Red"},{"hex":"#D33682","name":"Magenta"},{"hex":"#6C71C4","name":"Violet"}],[{"hex":"#77DD77","name":"Pastel Green"},{"hex":"#FFB347","name":"Pastel Orange"},{"hex":"#FF6961","name":"Pastel Red"},{"hex":"#AEC6CF","name":"Pastel Blue"},{"hex":"#F49AC2","name":"Pastel Pink"}],[{"hex":"#39FF14","name":"Neon Green"},{"hex":"#FFD700","name":"Neon Yellow"},{"hex":"#FF00FF","name":"Neon Pink"},{"hex":"#00FFFF","name":"Cyan"},{"hex":"#FF4500","name":"Neon Orange"}],[{"hex":"#8B4513","name":"Saddle Brown"},{"hex":"#CD853F","name":"Peru"},{"hex":"#D2691E","name":"Chocolate"},{"hex":"#8B0000","name":"Dark Red"},{"hex":"#228B22","name":"Forest Green"}],[{"hex":"#1E90FF","name":"Dodger Blue"},{"hex":"#00BFFF","name":"Deep Sky Blue"},{"hex":"#4682B4","name":"Steel Blue"},{"hex":"#4169E1","name":"Royal Blue"},{"hex":"#000080","name":"Navy"}],[{"hex":"#FF4500","name":"OrangeRed"},{"hex":"#FFA500","name":"Orange"},{"hex":"#FFD700","name":"Gold"},{"hex":"#8B4513","name":"Saddle Brown"},{"hex":"#A0522D","name":"Sienna"}],[{"hex":"#FF69B4","name":"Hot Pink"},{"hex":"#FFC0CB","name":"Pink"},{"hex":"#FF1493","name":"Deep Pink"},{"hex":"#C71585","name":"Medium Violet Red"},{"hex":"#FFB6C1","name":"Light Pink"}],[{"hex":"#00FF00","name":"Lime"},{"hex":"#32CD32","name":"Lime Green"},{"hex":"#228B22","name":"Forest Green"},{"hex":"#008000","name":"Green"},{"hex":"#006400","name":"Dark Green"}],[{"hex":"#FF4500","name":"OrangeRed"},{"hex":"#FFA500","name":"Orange"},{"hex":"#FFD700","name":"Gold"},{"hex":"#8B4513","name":"Saddle Brown"},{"hex":"#A0522D","name":"Sienna"}],[{"hex":"#FF6347","name":"Tomato"},{"hex":"#FF7F50","name":"Coral"},{"hex":"#FF4500","name":"OrangeRed"},{"hex":"#DAA520","name":"Goldenrod"},{"hex":"#CD5C5C","name":"Indian Red"}],[{"hex":"#191970","name":"Midnight Blue"},{"hex":"#000080","name":"Navy"},{"hex":"#00008B","name":"Dark Blue"},{"hex":"#4169E1","name":"Royal Blue"},{"hex":"#4682B4","name":"Steel Blue"}],[{"hex":"#FFFF00","name":"Yellow"},{"hex":"#FFD700","name":"Gold"},{"hex":"#FFA500","name":"Orange"},{"hex":"#FF8C00","name":"Dark Orange"},{"hex":"#FF4500","name":"OrangeRed"}],[{"hex":"#FFDAB9","name":"PeachPuff"},{"hex":"#FFE4B5","name":"Moccasin"},{"hex":"#FAF0E6","name":"Linen"},{"hex":"#FFEBCD","name":"Blanched Almond"},{"hex":"#FFFAF0","name":"Floral White"}],[{"hex":"#8B0000","name":"Dark Red"},{"hex":"#B22222","name":"Fire Brick"},{"hex":"#DC143C","name":"Crimson"},{"hex":"#A52A2A","name":"Brown"},{"hex":"#8B4513","name":"Saddle Brown"}],[{"hex":"#8B4513","name":"Saddle Brown"},{"hex":"#CD853F","name":"Peru"},{"hex":"#D2691E","name":"Chocolate"},{"hex":"#8B0000","name":"Dark Red"},{"hex":"#228B22","name":"Forest Green"}],[{"hex":"#FFFFFF","name":"White"},{"hex":"#F5F5F5","name":"Gainsboro"},{"hex":"#DCDCDC","name":"Gainsboro Light"},{"hex":"#A9A9A9","name":"Dark Gray"},{"hex":"#696969","name":"Dim Gray"}],[{"hex":"#4F86F7","name":"Blue"},{"hex":"#8EC5FC","name":"Light Blue"},{"hex":"#C1E0FF","name":"Powder Blue"},{"hex":"#4F86F7","name":"Blue"},{"hex":"#2A4EBB","name":"Dark Blue"}],[{"hex":"#228B22","name":"Forest Green"},{"hex":"#2E8B57","name":"Sea Green"},{"hex":"#32CD32","name":"Lime Green"},{"hex":"#3CB371","name":"Medium Sea Green"},{"hex":"#90EE90","name":"Light Green"}],[{"hex":"#FF4500","name":"OrangeRed"},{"hex":"#FFA500","name":"Orange"},{"hex":"#FFD700","name":"Gold"},{"hex":"#8B4513","name":"Saddle Brown"},{"hex":"#A0522D","name":"Sienna"}],[{"hex":"#77DD77","name":"Pastel Green"},{"hex":"#FFB347","name":"Pastel Orange"},{"hex":"#FF6961","name":"Pastel Red"},{"hex":"#AEC6CF","name":"Pastel Blue"},{"hex":"#F49AC2","name":"Pastel Pink"}],[{"hex":"#FF0000","name":"Red"},{"hex":"#FF6600","name":"Orange"},{"hex":"#FFCC00","name":"Yellow"},{"hex":"#33CC33","name":"Green"},{"hex":"#3366FF","name":"Blue"}],[{"hex":"#FFDDC1","name":"Peach"},{"hex":"#FFB7B2","name":"Pink"},{"hex":"#FF9AA2","name":"Salmon"},{"hex":"#E6A8D7","name":"Lavender"},{"hex":"#B5A8B0","name":"Lilac"}],[{"hex":"#00CED1","name":"Dark Turquoise"},{"hex":"#87CEEB","name":"Sky Blue"},{"hex":"#1E90FF","name":"Dodger Blue"},{"hex":"#4169E1","name":"Royal Blue"},{"hex":"#000080","name":"Navy"}],[{"hex":"#F8F9FA","name":"Light Gray"},{"hex":"#CED4DA","name":"Gray"},{"hex":"#AAB7B8","name":"Slate Gray"},{"hex":"#808B96","name":"Gray Blue"},{"hex":"#495057","name":"Dark Gray"}],[{"hex":"#FF5733","name":"Orange"},{"hex":"#C70039","name":"Crimson"},{"hex":"#900C3F","name":"Deep Red"},{"hex":"#581845","name":"Plum"},{"hex":"#14213D","name":"Navy Blue"}],[{"hex":"#FFFFFF","name":"White"},{"hex":"#F5F5F5","name":"Gainsboro"},{"hex":"#DCDCDC","name":"Gainsboro Light"},{"hex":"#A9A9A9","name":"Dark Gray"},{"hex":"#696969","name":"Dim Gray"}],[{"hex":"#87CEEB","name":"Sky Blue"},{"hex":"#4682B4","name":"Steel Blue"},{"hex":"#4169E1","name":"Royal Blue"},{"hex":"#0000CD","name":"Medium Blue"},{"hex":"#000080","name":"Navy"}],[{"hex":"#FF1493","name":"Deep Pink"},{"hex":"#FF69B4","name":"Hot Pink"},{"hex":"#FFD700","name":"Gold"},{"hex":"#FFA500","name":"Orange"},{"hex":"#FF6347","name":"Tomato"}],[{"hex":"#FF4500","name":"OrangeRed"},{"hex":"#FFA500","name":"Orange"},{"hex":"#FFD700","name":"Gold"},{"hex":"#FF6347","name":"Tomato"},{"hex":"#FA8072","name":"Salmon"}]]'
You realize that you got into programming for moments like this. Hitting the endpoint is very exciting! You got useful data from a remote server after you sent a simple GET request from your terminal.
You try to paste the endpoint URL in the browser, and... it also works! The next step is to access the endpoint from within your application, but that's for the next reading.
Hitting an API with Postman
Postman is a popular and flexible tool that can prepare, customize, send, receive, inspect, and visualize HTTP messages. If you don't already have a Postman account, take a moment to create a free account here and start using it right away. Postman also has a stand-alone app, a VSCode extension, and a CLI tool.
Inside Postman, you can go under "My Workspace", hit "New" and then "HTTP". You should be ready to send a simple GET request to any website or data endpoint while inspecting the details of the HTTP messages exchanged.
Using Postman, try hitting a public API called "Cat Facts", by pasting this endpoint into the GET field:
https://cat-fact.herokuapp.com/facts/
Now go to the bottom of the Postman interface and check "Body" which represents the JSON payload received. The request is relatively straightforward.
Sometimes, you will see GET requests that connect to more complex APIs and include many custom fields in the URL. Try this more complicated url:
https://openlibrary.org/api/books?bibkeys=ISBN:0201558025,LCCN:93005405&format=json
Everything after the ? are arguments sent with the URL request, similar to sending arguments to a function. This particular API can receive arguments from the URL and return the corresponding resources. You can use Postman to change any GET parameters under the "Params" tab.
Most public APIs will provide documentation for developers to understand how to use them properly. Some APIs provide a JavaScript library (i.e., a collection of objects and methods) that you can use from within your application to access their data without worrying about HTTP messages.
How to Build It
When testing the same Kolor endpoints with Postman, you were able to visualize a lot of the details about the HTTP messages. Postman can display the JSON data in a "pretty" format, much like an indented JS object, so it helps you to visualize the array:
JSON
[
[
{ hex: "#F44336", name: "Red" },
{ hex: "#E91E63", name: "Pink" },
{ hex: "#9C27B0", name: "Purple" },
{ hex: "#673AB7", name: "Deep Purple" },
{ hex: "#3F51B5", name: "Indigo" },
],
[
{ hex: "#1ABC9C", name: "Turquoise" },
{ hex: "#2ECC71", name: "Emerald" },
{ hex: "#3498DB", name: "Peter River" },
{ hex: "#9B59B6", name: "Amethyst" },
{ hex: "#34495E", name: "Wet Asphalt" },
],
[
{ hex: "#B58900", name: "Yellow" },
{ hex: "#CB4B16", name: "Orange" },
{ hex: "#DC322F", name: "Red" },
{ hex: "#D33682", name: "Magenta" },
{ hex: "#6C71C4", name: "Violet" },
],
[
{ hex: "#77DD77", name: "Pastel Green" },
{ hex: "#FFB347", name: "Pastel Orange" },
{ hex: "#FF6961", name: "Pastel Red" },
{ hex: "#AEC6CF", name: "Pastel Blue" },
{ hex: "#F49AC2", name: "Pastel Pink" },
],
[
{ hex: "#39FF14", name: "Neon Green" },
{ hex: "#FFD700", name: "Neon Yellow" },
{ hex: "#FF00FF", name: "Neon Pink" },
{ hex: "#00FFFF", name: "Cyan" },
{ hex: "#FF4500", name: "Neon Orange" },
],
[
{ hex: "#8B4513", name: "Saddle Brown" },
{ hex: "#CD853F", name: "Peru" },
{ hex: "#D2691E", name: "Chocolate" },
{ hex: "#8B0000", name: "Dark Red" },
{ hex: "#228B22", name: "Forest Green" },
],
[
{ hex: "#1E90FF", name: "Dodger Blue" },
{ hex: "#00BFFF", name: "Deep Sky Blue" },
{ hex: "#4682B4", name: "Steel Blue" },
{ hex: "#4169E1", name: "Royal Blue" },
{ hex: "#000080", name: "Navy" },
],
[
{ hex: "#FF4500", name: "OrangeRed" },
{ hex: "#FFA500", name: "Orange" },
{ hex: "#FFD700", name: "Gold" },
{ hex: "#8B4513", name: "Saddle Brown" },
{ hex: "#A0522D", name: "Sienna" },
],
[
{ hex: "#FF69B4", name: "Hot Pink" },
{ hex: "#FFC0CB", name: "Pink" },
{ hex: "#FF1493", name: "Deep Pink" },
{ hex: "#C71585", name: "Medium Violet Red" },
{ hex: "#FFB6C1", name: "Light Pink" },
],
[
{ hex: "#00FF00", name: "Lime" },
{ hex: "#32CD32", name: "Lime Green" },
{ hex: "#228B22", name: "Forest Green" },
{ hex: "#008000", name: "Green" },
{ hex: "#006400", name: "Dark Green" },
],
[
{ hex: "#FF4500", name: "OrangeRed" },
{ hex: "#FFA500", name: "Orange" },
{ hex: "#FFD700", name: "Gold" },
{ hex: "#8B4513", name: "Saddle Brown" },
{ hex: "#A0522D", name: "Sienna" },
],
[
{ hex: "#FF6347", name: "Tomato" },
{ hex: "#FF7F50", name: "Coral" },
{ hex: "#FF4500", name: "OrangeRed" },
{ hex: "#DAA520", name: "Goldenrod" },
{ hex: "#CD5C5C", name: "Indian Red" },
],
[
{ hex: "#191970", name: "Midnight Blue" },
{ hex: "#000080", name: "Navy" },
{ hex: "#00008B", name: "Dark Blue" },
{ hex: "#4169E1", name: "Royal Blue" },
{ hex: "#4682B4", name: "Steel Blue" },
],
[
{ hex: "#FFFF00", name: "Yellow" },
{ hex: "#FFD700", name: "Gold" },
{ hex: "#FFA500", name: "Orange" },
{ hex: "#FF8C00", name: "Dark Orange" },
{ hex: "#FF4500", name: "OrangeRed" },
],
[
{ hex: "#FFDAB9", name: "PeachPuff" },
{ hex: "#FFE4B5", name: "Moccasin" },
{ hex: "#FAF0E6", name: "Linen" },
{ hex: "#FFEBCD", name: "Blanched Almond" },
{ hex: "#FFFAF0", name: "Floral White" },
],
[
{ hex: "#8B0000", name: "Dark Red" },
{ hex: "#B22222", name: "Fire Brick" },
{ hex: "#DC143C", name: "Crimson" },
{ hex: "#A52A2A", name: "Brown" },
{ hex: "#8B4513", name: "Saddle Brown" },
],
[
{ hex: "#8B4513", name: "Saddle Brown" },
{ hex: "#CD853F", name: "Peru" },
{ hex: "#D2691E", name: "Chocolate" },
{ hex: "#8B0000", name: "Dark Red" },
{ hex: "#228B22", name: "Forest Green" },
],
[
{ hex: "#FFFFFF", name: "White" },
{ hex: "#F5F5F5", name: "Gainsboro" },
{ hex: "#DCDCDC", name: "Gainsboro Light" },
{ hex: "#A9A9A9", name: "Dark Gray" },
{ hex: "#696969", name: "Dim Gray" },
],
[
{ hex: "#4F86F7", name: "Blue" },
{ hex: "#8EC5FC", name: "Light Blue" },
{ hex: "#C1E0FF", name: "Powder Blue" },
{ hex: "#4F86F7", name: "Blue" },
{ hex: "#2A4EBB", name: "Dark Blue" },
],
[
{ hex: "#228B22", name: "Forest Green" },
{ hex: "#2E8B57", name: "Sea Green" },
{ hex: "#32CD32", name: "Lime Green" },
{ hex: "#3CB371", name: "Medium Sea Green" },
{ hex: "#90EE90", name: "Light Green" },
],
[
{ hex: "#FF4500", name: "OrangeRed" },
{ hex: "#FFA500", name: "Orange" },
{ hex: "#FFD700", name: "Gold" },
{ hex: "#8B4513", name: "Saddle Brown" },
{ hex: "#A0522D", name: "Sienna" },
],
[
{ hex: "#77DD77", name: "Pastel Green" },
{ hex: "#FFB347", name: "Pastel Orange" },
{ hex: "#FF6961", name: "Pastel Red" },
{ hex: "#AEC6CF", name: "Pastel Blue" },
{ hex: "#F49AC2", name: "Pastel Pink" },
],
[
{ hex: "#FF0000", name: "Red" },
{ hex: "#FF6600", name: "Orange" },
{ hex: "#FFCC00", name: "Yellow" },
{ hex: "#33CC33", name: "Green" },
{ hex: "#3366FF", name: "Blue" },
],
[
{ hex: "#FFDDC1", name: "Peach" },
{ hex: "#FFB7B2", name: "Pink" },
{ hex: "#FF9AA2", name: "Salmon" },
{ hex: "#E6A8D7", name: "Lavender" },
{ hex: "#B5A8B0", name: "Lilac" },
],
[
{ hex: "#00CED1", name: "Dark Turquoise" },
{ hex: "#87CEEB", name: "Sky Blue" },
{ hex: "#1E90FF", name: "Dodger Blue" },
{ hex: "#4169E1", name: "Royal Blue" },
{ hex: "#000080", name: "Navy" },
],
[
{ hex: "#F8F9FA", name: "Light Gray" },
{ hex: "#CED4DA", name: "Gray" },
{ hex: "#AAB7B8", name: "Slate Gray" },
{ hex: "#808B96", name: "Gray Blue" },
{ hex: "#495057", name: "Dark Gray" },
],
[
{ hex: "#FF5733", name: "Orange" },
{ hex: "#C70039", name: "Crimson" },
{ hex: "#900C3F", name: "Deep Red" },
{ hex: "#581845", name: "Plum" },
{ hex: "#14213D", name: "Navy Blue" },
],
[
{ hex: "#FFFFFF", name: "White" },
{ hex: "#F5F5F5", name: "Gainsboro" },
{ hex: "#DCDCDC", name: "Gainsboro Light" },
{ hex: "#A9A9A9", name: "Dark Gray" },
{ hex: "#696969", name: "Dim Gray" },
],
[
{ hex: "#87CEEB", name: "Sky Blue" },
{ hex: "#4682B4", name: "Steel Blue" },
{ hex: "#4169E1", name: "Royal Blue" },
{ hex: "#0000CD", name: "Medium Blue" },
{ hex: "#000080", name: "Navy" },
],
[
{ hex: "#FF1493", name: "Deep Pink" },
{ hex: "#FF69B4", name: "Hot Pink" },
{ hex: "#FFD700", name: "Gold" },
{ hex: "#FFA500", name: "Orange" },
{ hex: "#FF6347", name: "Tomato" },
],
[
{ hex: "#FF4500", name: "OrangeRed" },
{ hex: "#FFA500", name: "Orange" },
{ hex: "#FFD700", name: "Gold" },
{ hex: "#FF6347", name: "Tomato" },
{ hex: "#FA8072", name: "Salmon" },
],
]
You notice that the palettes (i.e., each array of five colors) do not have names. How should users identify, choose, and communicate about a particular palette? By using the indexes of the palettes? What if the indexes change? You should require the backend developer to include the palette name in the data.
Hitting an API with fetch
So far, you have learned how to make HTTP requests using some development tools. Let's get back to JavaScript!
The global fetch method has been the tried and true way of making simple GET requests from JS applications. fetch is a true async promise-based method that makes bringing remote data into your script easy and logical.
A basic fetch request looks like this:
async function logMovies() {
const response = await fetch("http://example.com/movies.json");
const movies = await response.json();
console.log(movies);
}
In the code above, the function requests a JSON resource from the endpoint, parses it into a JS object, and finally logs it to the console.
Pro tip: the returned promise will fulfill (resolve) any Status Code sent from the server, so you will need to check some properties from the response to verify if the request was successful. The promise will only reject network failures that prevent the request from completing.
How to Build It
It's time to fetch data in the Kolors app! You continue by implementing the initializePalettes() function to make a GET request using the 'fetch' method:
const initializePalettes = async (url) => {
const data = await fetch(url);
const paletteArray = await data.json();
console.log(paletteArray);
}
initializePalettes('https://webapis.bloomtechdev.com/palettes');
It just works (you can copy and paste it into the console to see)! Your console displays an array containing all 30 palettes from a remote server.
Conclusion
When you understand how HTTP works, a whole new world of tools and libraries becomes available to help you design, code, test, and implement applications.
You can learn much about any data endpoint by reading the documentation and sending requests with tools like Postman. When you are ready to start coding, you will know exactly how to fetch and manipulate useful data for your application.
When fetching data, one of the critical decisions you make as a developer is: should I fetch a large amount of data when the page loads, or should I wait to fetch what I need based on user interactions? Both approaches have pros and cons. Can you list some of them?
In the end, it all comes down to optimizing the user experience!
Building DOM Using Fetched Data
In the previous Core Competency you have learned about HTTP methods, tools and async functions that fetch data.
This last Core Competency will give another option to fetch data, teach how to improve the user experience by displaying spinners on the screen, and complete the whole task of fetching data, manipulating it, and updating the DOM!
Hitting an API with Axios
As an alternative to fetch, axios is a third-party Javascript HTTP library for sending HTTP requests. It is also asynchronous, promises-based, and arguably offers some advantages over the native fetch method, including the ability to automatically parse the JSON payload to a JS object in one single step (nice!).
Here's the complete documentation for Axios that you can use as a reference as you use it.
To import the axios library into your project, add the following line to the head section of your HTML:
<script src="https://unpkg.com/axios/dist/axios.min.js" defer></script>
Once you've imported the library, the axios object becomes globally exposed to your script, containing many methods, such as .get.
As with any promises, you can use either .then / .catch, or async/await, to deal with the data:
axios
.get("http://serverlocation.com/data")
.then((response) => {
// deal with response.data in here
})
.catch((err) => {
// deal with the error in here
});
The response return value already contains a JS object parsed from JSON in the property response.data.
How to Build It
Alright, your plan for your Kolors page is looking good.
The main script's first code block should fetch 30 palettes and store them in a global variable. That way, other functions will be able to get what they need, whenever they need, instantly.
Next, you will call the components to create the dark theme button and the three palette blocks. Then, each block will be initialized with a distinct palette and attached to the DOM. Finally, the app will enter a "react" state to respond to clicks.
You get back to your high-level design and list the steps of the application-building phase:
- Basic HTML: initialize the page, load styles and scripts, display a welcome message, and the dark mode button.
- Fetch and store JSON palettes
- Create/Initialize elements: blocks of palettes with initial colors and names
- Attach elements to the DOM
When your code completes all the steps above, the application will enter the "reactive" phase to respond to clicks indefinitely, dynamically updating the page (possibly fetching/creating or updating elements / attaching them to the DOM).
To implement Step 1, you quickly edit the HTML to include the Axios library as the first script at the end of the body:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Kolors Palette Selector</title>
<!-- CSS stylesheets will be imported -->
<link rel="stylesheet" href="kolors.css" />
<link rel="stylesheet" href="dark-theme.css" />
</head>
<body>
<h1>Kolors Palette Selector</h1>
<h2>
Welcome, we are excited to help you choose the perfect palette! Use the
buttons to shuffle through some beautiful color combinations.";
</h2>
<!-- All scripts will be imported in the correct order -->
<script src="https://unpkg.com/axios/dist/axios.min.js" defer></script>
<script src="dark-theme.js" defer></script>
<script src="palette.js" defer></script>
<script src="kolors.js" defer></script>
</body>
</html>
Step 2: Fetch 30 palettes in JSON format, and store the parsed JSON into a global variable (you decide to use AXIOS)
var palettes = [];
axios.get('https://webapis.bloomtechdev.com/palettes')
.then(response=>palettes=response.data)
.catch(//handle error)
Great! Following a step-by-step design, you could separate concerns and deal with each file independently. Implementing steps 1 and 2 was easy, with just a few lines of code. Steps 3, 4, and 5 still need to be implemented, but that's all for now. You deserve an overdue coffee break.
Handling spinners and failures
Fetching data from the network can take a long time, as you never know how fast is the user's connection or how responsive is the web API at any given moment.
It is a good practice in UX (user experience) design to let the user know when a task is still pending. Most applications will display a spinner or progress bar to indicate that something is running.
Of course, you don't want your spinner blocking the application, so it must execute asynchronously. Spinners are usually implemented through images, text, vectors, CSS animations, and styling, taking advantage of the async nature of the browser's screen rendering task. Spinners can also use JS async coding techniques - like worker threads and setTimeout - to coordinate functions and nestings.
In this Learning Objective, you will learn how to manage a spinner in a typical "fetching data" scenario. You are going to use a relatively simple CSS-based spinner:
CSS:
.spinner {
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
border: 7px solid #000;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
/* The `animation` below will be created by the `@keyframes` statement.*/
animation: rotation 1s ease infinite;
}
/* This `@keyframes` CSS statement creates a simple animation*/
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
The code above will define a class and an animation. The @keyframes declaration will instruct the browser to animate (rotate) the element via its recurring rendering task. The browser automatically refreshes the screen every 1/60 of a second, as long as your application is not blocking.
CSS animations are great for adding animated elements to the DOM without writing complex JS async code!
This is an excellent moment to introduce you to another Promise method: .finally. You already know that we can assign different callbacks for a promise's happy (fulfilled) and sad (rejected) paths. But sometimes you want to run an extra callback after the promise resolves either way (fulfilled or rejected):
myPromise
.then( /*happy callback…*/ )
.catch( /*sad callback */ )
.finally( /*final callback, will run either way */ );
To implement a spinner, all you have to do is add a simple <div> with an ID for the spinner so the JS script can later select it and activate it with a class. You will also add a button to trigger "fetch" and do some styling in the button during and after the fetch, for an even better UX!
Here's the final code:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<style>
.spinner {
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
border: 7px solid #000;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s ease infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<!-- this button will be assigned a click handler -->
<button id="fetch">Fetch data!</button>
<!-- this div is a placeholder for the spinner -->
<div id="spin"></div>
<script>
const button = document.getElementById("fetch");
const spinner = document.getElementById("spin");
// this function simulates an async task which will take 5 seconds to complete
function longFetch() {
return new Promise((resolve) => {
setTimeout(resolve, 5000);
});
}
// The handler will change the button's text and style, add the class to the spinner div, then call the async fetch.
function handleFetch() {
button.innerText = "Fetching...";
button.style.backgroundColor = "gold";
spinner.classList.add("spinner");
longFetch()
.then(() => console.log("This will manipulate the fetched data"))
.catch(() => console.log("This will handle any errors"))
// The spinner is removed from the screen by removing the class from the div
.finally(() => {
spinner.classList.remove("spinner");
button.innerText = "Ready!";
button.style.backgroundColor = "lime";
});
}
button.addEventListener("click", handleFetch);
</script>
</body>
</html>
The longFetch function simulates a network fetch, which takes 5 seconds. The handleFetch function is the callback invoked by the button. Notice how this handler will perform multiple tasks, including updating texts, colors, and displaying and hiding the spinner. You can use the .finally method to hide the spinner regardless of the promise outcome (successful or not).
If you feel adventurous, you can simulate an error and add code to .catch to display a warning message for 3 seconds. And maybe change the button's color to red.
By combining the then/catch/finally structure with CSS animations and DOM manipulation, you can create an excellent user experience!
How to Build It
You are ready to finish Step 2 (fetching and storing data) from your design!
Fetching 30 palettes from the Kolors API should be fast, but you can never be sure. You decide to use a loading spinner to communicate to the user that data is coming.
First, you update the HTML by linking spinner.css to the project (inside the head section) and adding the spinner <div> to the body:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Kolors Palette Selector</title>
<link rel="stylesheet" href="kolors.css" />
<link rel="stylesheet" href="dark-theme.css" />
<!--The stylesheet for the spinner-->
<link rel="stylesheet" href="spinner.css" />
</head>
<body>
<h1>Kolors Palette Selector</h1>
<h2>
Welcome, we are excited to help you choose the perfect palette! Use the
buttons to shuffle through some beautiful color combinations.
</h2>
<!--The div placeholder for the spinner-->
<div id="spin"></div>
<script src="https://unpkg.com/axios/dist/axios.min.js" defer></script>
<script src="dark-theme.js" defer></script>
<script src="palette.js" defer></script>
<script src="kolors.js" defer></script>
</body>
</html>
Then, you add some code to the main script to show the spinner while the app executes the API call. Since you want the spinner to disappear regardless of the API call being successful or not, you choose to remove the spinner using .finally:
const spinner = document.getElementById("spin");
var palettes = [];
// The spinner becomes visible when the class is added to the div
spinner.classList.add("spinner");
axios
.get("https://webapis.bloomtechdev.com/palettes")
.then((response) => (palettes = response.data))
.catch()
// And disappears when the class is removed
.finally(() => {
spinner.classList.remove("spinner");
});
Beautiful, easy, and clean work! You still have some work to do to implement the next steps: create/initialize elements, attach elements to the DOM, and handle user events.
Iterating over fetched data to build DOM
Alright! Now it is time to put everything together: build the DOM dynamically by fetching JSON data.
You can now build a component function that will asynchronously request dynamic data from a server, build elements based on that data, and add those elements to the DOM.
You have previously used component functions to build the DOM in Module 3, but you were getting data from an existing object/array. The main difference now is that you will be fetching data from a remote endpoint.
When an app loads, it will probably create and initialize some elements. But after that, it will enter the responsive state to handle user interactions. At that phase, the cycle will always be:
- The user performs an action (e.g., clicks on a button or submits a form) that triggers the handler to fetch data from an external source.
- The external source sends back the requested data, the code consumes the value returned by the promise (using .then or await) and calls a component function. Component functions usually receive arguments to build and initialize the elements with content.
- The next block of code attaches the returned element to the DOM. As a general rule, component functions should not attach the element to the DOM, so the application can decide where (and when) to display them.
- The cycle repeats!
This approach enables the application to dynamically create virtually any page while keeping your initial HTML short and simple.
How to Build It
You refer back to your design and remember that the Kolors app will load the basic HTML (step 1) and fetch 30 palettes (step 2) before it enters the "responsive state". As implemented, a global JS array will store the data sent by the server. Now, you need to implement steps 3 and 4, to create the initial elements of the page (e.g., buttons, color bars) and attach them to the DOM. Only then will the app be ready to enter the reactive state and continuously handle user interactions.
Step 1 - HTML: check! Step 2 - Fetch: check! (including a nice spinner).
Step 3 - Create and initialize elements: You edit the code at palette.js (component function) to initialize the page:
function createPalette(id_number) {
// create the html element
const palette = document.createElement("div");
palette.id = "palette" + id_number;
palette.classList.add("container");
const paletteBtn = document.createElement("button");
paletteBtn.id = "palettebtn" + id_number;
paletteBtn.classList.add("palettebtn");
paletteBtn.textContent = "Palette #" + id_number + " - Click to advance";
// add an event listener to call changePalette and pass the id_number on click
paletteBtn.addEventListener("click", () => {
changePalette(id_number);
});
palette.appendChild(paletteBtn);
// create rectangles showing each of the colors in the palette with their hex values
for (let i = 1; i <= 5; i++) {
const rectangle = document.createElement("div");
rectangle.id = id_number + "rectangle" + i;
rectangle.classList.add("rectangle");
rectangle.style.backgroundColor = colorPalettes[id_number - 1][i - 1].hex;
rectangle.textContent = colorPalettes[id_number - 1][i - 1].name;
palette.appendChild(rectangle);
}
return palette;
}
The createPalette() component function will return a container with a button (assigned to a handler), and five color bars properly initiated according to the palette number ID, which is passed as an argument. You chose to add IDs to every element so the same handler can be used to manipulate all palette blocks.
You add code to the changePalette button handler in the same file:
const changePalette = (id_number) => {
const colorBars = document.querySelectorAll(
"#palette" + id_number + " .rectangle"
);
const button = document.getElementById("palettebtn" + id_number);
currentPalette[id_number - 1]++;
if (currentPalette[id_number - 1] > 29) {
currentPalette[id_number - 1] = 0;
}
next = currentPalette[id_number - 1];
button.textContent = "Palette #" + (next + 1) + " - Click to advance";
colorBars.forEach((bar, index) => {
bar.style.backgroundColor = colorPalettes[next][index].hex;
bar.textContent = colorPalettes[next][index].name;
});
};
Nice! Whenever the user clicks on a button, the corresponding palette will change. You had to create a global array to keep track of the current palette number in each palette container. You were also careful to verify that the colorPalettes array will never be accessed out of bounds.
The page will display the first three palettes of the colorPalettes array. So now you can edit your main script inside kolors.js to call the function component and append the returned component to the DOM (Step 4).
const spinner = document.getElementById("spin");
spinner.classList.add("spinner");
const toggleBtn = createDarkThemeBtn();
document.body.appendChild(toggleBtn);
var colorPalettes = [];
var currentPalette = [0, 1, 2];
axios
.get("https://webapis.bloomtechdev.com/palettes")
.then((response) => (colorPalettes = response.data))
.then(() => {
document.body.appendChild(createPalette(1));
document.body.appendChild(createPalette(2));
document.body.appendChild(createPalette(3));
})
.catch()
.finally(() => {
spinner.classList.remove("spinner");
});
Wow! Your main script is so simple, and yet the application works great! You have effectively completed steps 3 and 4 from the main design: create/initialize elements, and attach them to the DOM. The remaining of your code will only deal with user interactions, and update the DOM as needed.
You should spend some time styling the elements. But for now, with a few components, the core of the app is ready.
Conclusion
You have learned so many concepts in this last Core Competency! When you understand async programming and how to deal with promises in JS, you can effectively consume remote data in your application, while creating a great user experience.
Remember, a single page app (SPA) is all about monitoring user interaction to fetch JSON and update the DOM. Gone are the days of refreshing the page to load HTML files!
Module 4 Project: Weather API
In this project, you will build a weather widget that fetches and displays real-time weather data from an API. The project involves manipulating the Document Object Model (DOM) to display the fetched data, handling user interactions with a dropdown menu, and managing API requests and responses. You will also learn to handle errors and work with JSON data. The final product will be a functional weather widget displaying weather data for different cities.
The module project contains advanced problems that will challenge and stretch your understanding of the module's content. The project has built-in tests for you to check your work, and the solution video is available in case you need help or want to see how we solved each challenge, but remember, there is always more than one way to solve a problem. Before reviewing the solution video, be sure to attempt the project and try solving the challenges yourself.
Instructions
The link below takes you to Bloom's code repository of the assignment. You'll need to fork the repo to your own GitHub account, and clone it down to your computer:
Starter Repo: Weather API
- Fork the repository,
- clone it to your machine, and
- open the README.md file in VSCode, where you will find instructions on completing this Project.
- submit your completed project to the BloomTech Portal