🐥 JS3
Basically, asynchronous programming, templates, and state in JavaScript
- JS3
- ⏳ Asynchrony : outside time
- 🃏 Reusable components
- 🌐 Requesting from a server side API
- 🍬 async/await
- 🍱 Creating elements with <template>
- 🎱 Rendering based on state
- 🐕 Fetching data
- 🐕 🎞️ fetch films
- 👭🏾 One-to-one mappings
- 💽 Rendering one card
- 💾 ➡️ 💻 Rendering Data as UI
- 📝 Check-in ➡️ Coordinate
- 🔁 Actually re-rendering
- 🔁 Re-rendering
- 🔎 Identifying state
- Chaining Promises
- fetch API
- How the internet works
- 🆕 Introducing new state
- 🌡️ Diagnose
- 📽️ Cinema listings
- 🗓️ Latency
- 🗺️ Applying map to our problem
- 🗺️ Using map
- 🥎 try/catch
- 🦻🏻 Capturing the user event
- 🧩 Break down the problem
- 🧱 Composing elements
- 🧼 Creating elements with functions
- 🧼 Refactoring to state+render
- 🪃 Callbacks
- 🪄 Reacting to user input
- 🪆 .then()
- 🪞 Re-rendering the UI
- 🫱🏿🫲🏽 Promises
JS3 block viewer
This block viewer lets you flick through all the existing blocks in the JS3 folder so you can choose what parts to add to your pages and what parts you might want to create, revise, or leave out.
It's literally just an alphabetical list of whatever is in this folder.
⏳ Asynchrony : outside time
Learning Objectives
We can handle latency using
We have written a lot of JavaScript programs that execute sequentially. This means that each line of code is run in order, one after the other.
For example:
console.log("first");
console.log("second");
console.log("third");
Outputs:
first
second
third
When we call a function, the function will run to completion before the next line of code is executed. But what if we need to wait for something to happen? What if we need to wait for our data to arrive before we can show it? In this case, we can use asynchronous execution.
Event Loop
We have already used asynchronous execution. We have defined eventListener
s that listen for events to happen, then execute a callback function.
const search = document.getElementById("search");
search.addEventListener("input", handleInput);
When we called addEventListener
it didn’t immediately call handleInput
.
But here’s a new idea: eventListeners are part of the Event API. They are not part of the JavaScript language! 🤯 This means you can’t use them in a Node REPL. But they are implemented in web browsers. The core of JavaScript (e.g. strings and functions) is the same everywhere, but different contexts may add extra APIs.
When you set an eventListener you are really sending a call to a Web API and asking it do something for you.
const search = document.getElementById("search");
search.addEventListener("input", handleInput);
The callback handleInput
does not run until the user types. With fetch
, the callback function does not run until the data arrives. In both cases, we are waiting for something to happen before we can run our code.
We use a function as a way of wrapping up the code that needs to be run later on. This means we can tell the browser what to do when we’re done waiting.
🧠 Recap our concept map
🃏 Reusable components
Learning Objectives
Recall our sub-goal:
🎯 Sub-goal: Build a film card component
Now that we have made a card work for one particular film, we can re-use that code to render any film object in the user interface with a general component. To do this, we wrap up our code inside a JavaScript function. JavaScript functions reuse code: so we can implement reusable UI components using functions.
We could use either our createChildElement
implementation or our <template>
implementation - making a component function works the same for either. As an example, we will use the <template>
implementation:
const film = {
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
};
const template = document.getElementById("film-card");
const createFilmCard = (film) => {
const card = template.content.cloneNode(true);
// Now we are querying our cloned fragment, not the entire page.
card.querySelector("h3").textContent = film.title;
card.querySelector("[data-director]").textContent = `Director: ${film.director}`;
card.querySelector("time").textContent = `${film.duration} minutes`;
card.querySelector("[data-certificate]").textContent = `Certificate: ${film.certificate}`;
// Return the card, rather than directly appending it to the page
return card;
};
const filmCard = createFilmCard(film);
// Remember we need to append the card to the DOM for it to appear.
document.body.append(filmCard);
Exercise: Use destructuring
createFilmCard
function to use object destructuring for the film
parameters.🌐 Requesting from a server side API
Learning Objectives
So now we have these pieces of our giant concept map
- 📤 we know that we can send a request using
fetch()
- 🐕 we know that
fetch
is a 💻 client side 🧰 Web API - 🗓️ we know that sending requests over a network takes time
- 🧵 we know that we should not stop our program to wait for data
- 🪃 we know that we can use callbacks to manage events
But we still don’t know how to use fetch
to get data from a server side API. Let’s find this out now.
Let’s pick our film display exercise back up. Before we had a list of films hard-coded in our state
. We’re going to replace the films array with data fetched from a server.
// Begin with an empty state
const state = {
films: [],
searchTerm: "",
};
const endpoint = "https://programming.codeyourfuture.io/dummy-apis/films.json";
const fetchFilms = async () => {
const response = await fetch(endpoint);
return await response.json();
}; // Our async function returns a Promise
fetchFilms().then((films) => {
// When the fetchFilms Promise resolves, this callback will be called.
state.films = films;
render();
});
fetch
returns a Promise
; the Promise
fulfils itself with a response; the response contains our data.
Next we will dig into Promise
s, async
, await
, and then
, and complete our concept map.
🍬 async/await
Learning Objectives
These two blocks of code do exactly the same thing:
const getProfile = async (url) => {
const response = await fetch(url);
const data = await response.json();
const htmlUrl = data.html_url;
console.log(htmlUrl);
}
getProfile("https://api.github.com/users/SallyMcGrath");
const getProfile = (url) => {
return fetch(url)
.then((response) => response.json())
.then((data) => data.html_url)
.then((htmlUrl) => console.log(htmlUrl));
};
getProfile("https://api.github.com/users/SallyMcGrath");
Async/await is
We group async
and await
together: async/await, because we
await
inside an async
function or at the top level of a module.
We use the async
keyword to define a function that returns a Promise. An async function always returns a Promise.
We can see this with a simple function which doesn’t need to await anything. Save this in a file and run it with node:
const getProfile = async (url) => url;
console.log(getProfile("hello")); // Logs a Promise.
getProfile("hello").then((value) => console.log(value)); // Logs a value
Even though the function above doesn’t have a time problem, the fact that we define the function as an async
function means it returns a Promise
.
But let’s do something more interesting - let’s actually solve a time problem.
const getProfile = async (url) => {
// the async keyword tells us this function handles a time problem
};
We use the await
operator to say “don’t move on until this is done”. Importantly, we are not actually waiting for a Promise to resolve. We are scheduling a callback that will be called when the Promise resolves. But this allows us to write code that looks like it’s happening in time order (as if we are waiting), without actually blocking our main thread.
const getProfile = async (url) => {
const response = await fetch(url);
return response.json();
};
getProfile("https://api.github.com/users/SallyMcGrath")
.then((response) => console.log(response))
Save this to a file and run with with node. It works the same as before.
🍱 Creating elements with <template>
Learning Objectives
Using <template>
tags
We could simplify this code with a different technique for creating elements.
Until now, we have only seen one way to create elements: document.createElement
. The DOM has another way of creating elements - we can copy existing elements and then change them.
HTML has a useful tag designed to help make this easy, the <template>
tag. When you add a <template>
element to a page, it doesn’t get displayed when the page loads. It is an inert fragment of future HTML.
We can copy any DOM node, not just <template>
tags. For this problem, we will use a <template>
tag because it is designed for this purpose.
When we copy an element, its children get copied. This means we can write our template card as HTML:
<template id="film-card">
<section>
<h3>Film title</h3>
<p data-director>Director</p>
<time>Duration</time>
<data data-certificate>Certificate</data>
</section>
</template>
This is our template card. Place it in the body of your html. It doesn’t show up! Template HTML is like a wireframe; it’s just a plan. We can use this template to create a card for any film object. We will clone (copy) this template and populate it with data. Replace the contents of your <script>
tag with this:
const film = {
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
};
const card = document.getElementById("film-card").content.cloneNode(true);
// Now we are querying our cloned fragment, not the entire page.
card.querySelector("h3").textContent = film.title;
card.querySelector("[data-director]").textContent = `Director: ${film.director}`;
card.querySelector("time").textContent = `${film.duration} minutes`;
card.querySelector("[data-certificate]").textContent = `Certificate: ${film.certificate}`;
document.body.append(card);
This code will produce the same DOM elements in the page as the two other versions of the code we’ve seen (the verbose version, and the version using createChildElement
).
The first two approaches (the verbose version, and the createChildElement
version) did so by calling the same DOM functions as each other.
This approach uses different DOM functions. But it has the same effect.
Exercise: Consider the trade-offs
We’ve now seen two different ways of simplifying our function: extracting a function, or using a template tag.
Both have advantages and disadvantages.
Think of at least two trade-offs involved. What is better about the “extract a function” solution? What is better about the template tag solution? Could we combine them?
Share your ideas about trade-offs in a thread in Slack.
🎱 Rendering based on state
Learning Objectives
For now, we have set the initial value of the searchTerm
state to “Pirate”. This means that our render function should only create cards for films which contain the word “Pirate” in their title. But right now, our render function creates cards for all of the films.
In our render function, we must filter our list down to the films that match our search term. This does not require us to introduce new state. We can derive a filtered list from our existing state.
Filter function
We can use the higher order array function .filter()
to return a new array of films that include the search term:
const filteredFilms = state.films.filter((film) =>
film.title.includes(state.searchTerm)
);
We can change our render function to always do this. If searchTerm
is empty, our filter function will return all the films:
function render() {
const filteredFilms = state.films.filter((film) =>
film.title.includes(state.searchTerm)
);
const filmCards = filteredFilms.map(createFilmCard);
document.body.append(...filmCards);
}
- At this point in our codealong, when we open our page, what will we see?
- If we change the initial value of
state.searchTerm
back to the empty string and open the page again, what will we see?
If we open our page, we should now only see cards for films containing “Pirate” in their title.
If we change the initial value of state.searchTerm
back to the empty string and open the page again, we should see cards for all of the films.
We have now solved two of our three problems:
- Identify what state we have.
- Define how to render the page based on that state.
- Change state (perhaps in response to some user action).
Making our search more user friendly
💡 Things to consider
One of the nice things about breaking down the problem like this is that it allows us to change rendering without needing to interact with the page.
If we want to improve our search functionality (e.g. to make it work if you searched for PIRATES in all-caps), we can set the initial value of state.searchTerm
to "PIRATES"
and make changes to our render
function. Then every time we open the page, it will be like we searched for “PIRATES”.
This can be a lot quicker than having to refresh the page and type in “PIRATES” in the search box every time we make a change want to see if our search works.
Exercise: Make search more user friendly
🐕 Fetching data
Learning Objectives
So far we have displayed film data stored in our JavaScript code. But real applications fetch data from servers over the internet. We can restate our problem as follows:
Given an API that serves film data
When the page first loads
Then the page shouldfetch
and display the list of film data, including the film title, times, and film certificate.
💻 Client side and 🌐 Server side APIs
We will use fetch()
, a
APIs are useful because they let us get information which we don’t ourselves know. The information may change over time, and we don’t need to update our application. When we ask for the information, the API will tell us the latest version.
We also don’t need to know how the API works in order to use it. It may be written in a different programming language. It may talk to other APIs we don’t know about. All we need to know is how to talk to it. This is called the interface.
Using fetch is simple. But we want to understand what is happening more completely. So let’s take ourselves on a journey through time.
👉🏾 Unfurl to see the journey (we will explain this in little pieces)
😵💫 This is a lot to take in. Let’s break it down and make sense of it.
🐕 🎞️ fetch films
Learning Objectives
Now that we have a basic understanding of Web APIs and Promises, let’s use look again at our code for fetching film data:
const endpoint = "https://programming.codeyourfuture.io/dummy-apis/films.json";
const fetchFilms = async () => {
const response = await fetch(endpoint);
return await response.json();
};
fetchFilms().then((films) => {
// When the fetchFilms Promise resolves, this callback will be called.
state.films = films;
render();
});
We are defining fetchFilms
: an async
function - a function which returns a Promise
.
When we call fetchFilms
, what we get is an unresolved Promise
.
What fetchFilms
does is fetch a URL (with our call to fetch
itself returning a Promise
resolving to a Response
). When the Promise
from fetch
resolves, fetchFilms
reads the body of the Response
(a string), and parses is as JSON. The Promise
returned by fetchFilms
then resolves with the result of parsing the string as JSON.
When the Promise
from fetchFilms
resolves, our next callback is called: We update our state
, and call render()
.
After this is done, the rest of our code works exactly the same as it did before. We have our list of films in our state, so we never need to fetch the list of films again.
render
works the same - it only cares that state.films
is an array of films, it doesn’t care where they came from.
When we change our filter by typing, events fire and our event handler will be called back exactly the same as it did before.
👭🏾 One-to-one mappings
Learning Objectives
We can now render any one film data object in the UI. However, to fully solve this problem we must render a list of all of the film objects. For each film object, we need to render a corresponding film card in the UI. In this case, there is a
Given an array named
films
…
To create an array of card components we can iterate through the film data using a for...of
loop:
const filmCards = [];
for (const item of films) {
filmCards.push(createFilmCard(item));
}
document.body.append(...filmCards);
// invoke append using the spread operator
However, there are alternative methods for building this array of UI components.
💽 Rendering one card
Learning Objectives
🎯 Sub-goal: Build a film card component
To break down this problem, we’ll render a single datum, before doing this for the whole list. Here’s one film:
const film = {
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
};
Starting with this object, we’ll focus only on building this section of the user interface:
🖼️ Open this wireframe of single film card
💾 ➡️ 💻 Rendering Data as UI
Learning Objectives
When we build user interfaces we often take data and
📝 Check-in ➡️ Coordinate
Learning Objectives
- Assemble as group
- Briefly discuss any particular areas of concern following the diagnose block
- Devise strategies for addressing misconceptions
🔁 Actually re-rendering
Learning Objectives
We have seen that when we search, we’re only adding new elements, and not removing existing elements from the page.
We previously identified our strategy of clearing old elements before adding new ones. But we are not doing this.
We can clear out the existing children of an element by setting its textContent
propery to the empty string:
document.body.textContent = "";
Add this to your render
function before you add new elements. Try using your page. Try searching for a particular film.
Oh no, our search box is gone!
exercise
We removed our search box from the page because we removed everything from the entire document body.
This was not our intention - we only wanted to remove any films we had previously rendered.
A way to solve this is to introduce a container element which our render
function will re-fill every time it’s called.
We should identify which elements in our page should be re-drawn every time we render, and which should always be present.
Introduce a new container, and keep any “always present” UI elements outside of it. Update your render
function to clear and append to the container, not the whole body.
Remember to use semantic HTML. Your container should be an appropriate tag for the contents it will have.
🔁 Re-rendering
Learning Objectives
Now that we’ve shown we can log the search text, we can set the new value of the searchTerm
state, and re-render the page.
We should have a page like this:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Film View</title>
</head>
<body>
<label>Search <input type="text" id="search" /></label>
<template id="film-card">
<section>
<h3>Film title</h3>
<p data-director>Director</p>
<time>Duration</time>
<data data-certificate>Certificate</data>
</section>
</template>
<script>
const state = {
films: [
{
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
},
{
title: "Typist Artist Pirate King",
director: "Carol Morley",
times: ["15:00", "20:00"],
certificate: "12A",
duration: 108,
},
],
searchTerm: "",
};
const template = document.getElementById("film-card");
const createFilmCard = (film) => {
const card = template.content.cloneNode(true);
// Now we are querying our cloned fragment, not the entire page.
card.querySelector("h3").textContent = film.title;
card.querySelector("[data-director]").textContent = `Director: ${film.director}`;
card.querySelector("time").textContent = `${film.duration} minutes`;
card.querySelector("[data-certificate]").textContent = `Certificate: ${film.certificate}`;
// Return the card, rather than directly appending it to the page
return card;
};
function render() {
const filteredFilms = state.films.filter((film) =>
film.title.includes(state.searchTerm)
);
const filmCards = filteredFilms.map(createFilmCard);
document.body.append(...filmCards);
}
const searchBox = document.getElementById("search");
searchBox.addEventListener("input", handleSearchInput);
function handleSearchInput(event) {
const searchTerm = event.target.value;
console.log(searchTerm);
}
render();
</script>
</body>
</html>
We want to change our search input handler to update state.searchTerm
and call render()
again.
Implement this and try searching. What happens? Play computer to work out why what’s happening isn’t what we expected.
🔎 Identifying state
Learning Objectives
🕞 State: data which may change over time.
We store each piece of state in a variable. When we render in the UI, our code will look at the state in those variables. When the state changes, we render our UI again based on the new state.
“What the state used to be” or “How the state changed” isn’t something we pay attention to when we render. We always render based only on the current state.
We want to have as few pieces of state as possible. We want them to be fundamental.
Some guidelines for identifying the state for a problem:
✔️ If something can change it should be state.
In our film example, the search term can change, so it needs some state associated with it.
❌ But if something can be derived it should not be state.
In our film example, we would not store “is the search term empty” and “what is the search term” as separate pieces of state. We can work this answer out ourselves. This answer can be derived. We can answer the question “is the search term empty” by looking at the search term. We don’t need two variables: we can use one.
🖇️ If two things always change together, they should be one piece of state.
If a website allows log-in, we would not have one state for “is a user logged in” and one state for “what user is logged in”. We would have one piece of state: The currently logged in user. We would set that state to null
if there is no logged in user. We can answer the question “is a user logged in” by checking if the currently logged in user is null
. We don’t need a separate piece of state.
State in our example
In our film example, we need two pieces of state:
- Our list of all films
- The search term
When we introduce filtering films based on the search term we will not introduce other new state. We will not store a filtered list of films in state. Our filtered list of films can be derived from our existing state.
Chaining Promises
fetch API
Let’s suppose we have a remote API hosted at the following url: “https://api-film-data.com”.
We can use applications like Postman to make requests to APIs. However, we want to make a request for the film data using JavaScript. We can use fetch
to make network requests in JavaScript. Let’s take a look at how we can do this:
const filmData = fetch("https://api-film-data.com/films");
fetch
is a JavaScript function. We call fetch
using the url of the remote API we wish to fetch data from. Once fetch
has got the data then we want to store it in a variable so we can then use it in our application. Let’s log this data:
const filmData = fetch("https://api-film-data.com/films");
console.log(filmData);
However, if we log this variable we don’t get an array of data. We get:
Promise <pending>
How the internet works
Learning Objectives
We’ve been using the internet for years, but how does it actually work? What happens when you type a URL into a browser? How does the browser know where to go? How does it know what to show? How does it know how to show it?
🆕 Introducing new state
We are introducing a new feature: being able to search for films. We have identified that this introduces one new element of state: the search term someone has asked for.
Let’s add it to our state object:
const state = {
films: [
{
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
},
{
title: "Typist Artist Pirate King",
director: "Carol Morley",
times: ["15:00", "20:00"],
certificate: "12A",
duration: 108,
},
],
searchTerm: "",
};
We needed to pick an initial value for this state. We picked the empty string, because when someone first loads the page, they haven’t searched for anything. When someone types in the search box, we will change the value of this state, and re-render the page.
We could pick any initial value. This actually allows us to finish implementing our render function before we even introduce a search box into the page. In real life, our searchTerm
state will be empty at first, but we can use different values to help us with development. We can make the page look like someone searched for “Pirate”, even before we introduce a search box in the UI.
This is because we have split up our problem into three parts:
- 👩🏾🔬 Identify what state we have.
- ✍🏿 Define how to render the page based on that state.
- 🎱 Change state (perhaps in response to some user action).
Let’s try making our render function work for the search term “Pirate”. Change the initial value of the searchTerm
field of the state
object to “Pirate”:
const state = {
films: [
{
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
},
{
title: "Typist Artist Pirate King",
director: "Carol Morley",
times: ["15:00", "20:00"],
certificate: "12A",
duration: 108,
},
],
searchTerm: "Pirate",
};
We expect, if someone is searching for “Pirate”, to only show films whose title contains the word Pirate.
🌡️ Diagnose
Learning Objectives
This is a pairing activity!
Each pair will need to split into navigator and driver. Volunteers can pair up too - they need to drive though! Navigators you can read the instructions for this workshop as you get setup
This activity will consist of the following steps:
🧑💻 Predict ➡️ Explain
Given a program or piece of code, you’ll have to explain what the code currently does. Not what it should do.
🔍🐛 Find the bug
Given a target output/behaviour - trainees can identify a bug in the source code
🪜🧭 Propose a strategy
Given a problem, you’ll have to think about a strategy for solving it. This doesn’t involve coding but stepping back to think about how you could solve the problem. You might want to talk aloud, draw a flow diagram or write out the steps you’d take in your solution.
For the specific task, check with the facilitator on Saturday.
📽️ Cinema listings
Learning Objectives
Suppose you’re building a user interface to display the films that are now showing on a film website. We need to render some cinema listings in the user interface. Let’s define an acceptance criterion:
Given a list of film data
When the page first loads
Then it should display the list of films now showing, including the film title, times and film certificate.
Here are some example film data:
const films = [
{
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
},
{
title: "Typist Artist Pirate King",
directory: "Carol Morley",
times: ["15:00", "20:00"],
certificate: "12A",
duration: 108,
},
];
To visualise the user interface, we can use a
Our task will be to build the film listings view from this list of data.
Create an index.html
file and follow along.
🗓️ Latency
Learning Objectives
Instead of already having our data, we are now sending a request over the network to another computer, and then waiting for that computer to send us a response back. Now that our data is going on a journey over a network, we introduce the problem of latency.
Latency is the time taken for a request to traverse the network.
💡 Network latency is travel time.
Why is latency a problem? Because it means we need to wait for our data. But our program can only do one thing at a time. If we stopped our program to wait for data, then we wouldn’t be able to do anything else (like show the rest of the page, or respond to a user clicking in the page). We need to handle this time problem.
Programming often involves time problems, and latency is just one of them.
🗺️ Applying map to our problem
Learning Objectives
Now that we understand map
, let’s ty to use it in our project.
Given the list of film data:
const films = [
{
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
},
{
title: "Typist Artist Pirate King",
director: "Carol Morley",
times: ["15:00", "20:00"],
certificate: "12A",
duration: 108,
},
];
Use createFilmCard
and map
to create an array of film card components. In your local project, render this array of components in the browser.
🗺️ Using map
Learning Objectives
We want to create a new array by applying a function to each element in the starting array. Earlier, we used a for...of
statement to apply the function createFilmCard
to each element in the array. However, we can also build an array using the map
array method. map
is a
map
. Then map
will use this function to create a new array.
Work through this map
exercise. It’s important to understand map before we apply it to our film data.
const arr = [5, 20, 30];
function double(num) {
return num * 2;
}
Our goal is to create a new array of doubled numbers given this array and function. We want to create the array [10, 40, 60]
. Look, it’s another “one to one mapping”.
We are building a new array by applying double
to each item. Each time we call double
we store its return value in a new array:
function double(num) {
return num * 2;
}
const numbers = [5, 20, 30];
const doubledNums = [
double(numbers[0]),
double(numbers[1]),
double(numbers[2]),
];
But we want to generalise this. Whenever we are writing out the same thing repeatedly in code, we probably want to make a general rule instead. We can do this by calling map
:
|
|
Use the array visualiser to observe what happens when map
is used on the arr
. Try changing the elements of arr
and the function that is passed to map
. Answer the following questions in the visualiser:
- What does
map
do? - What does
map
return? - What parameters does the
map
method take? - What parameters does the callback function take?
Play computer with the example to see what happens when the map
is called.
🥎 try/catch
Learning Objectives
We can handle errors with a try/catch block. We can use the try
keyword to try to do something, and if it fails, catch
the
throw
keyword.
const getProfile = async (url) => {
try {
const response = await fetch(url);
return response.json();
} catch (error) {
console.error(error);
}
};
Let’s trigger an error to see this in action. In a Node REPL in your terminal, call getProfile on an API that does not exist again:
getProfile("invalid_url");
TypeError: Failed to parse URL from invalid_url
[...]
[cause]: TypeError: Invalid URL
[...]
code: 'ERR_INVALID_URL',
input: 'invalid_url'
It’s actually the same error you saw before, without the word ‘Uncaught’ before it. But why do we care about this? It’s not obvious in this simple, single function. If we don’t catch the error, the function will
You need to tell JavaScript what to do when something goes wrong, or it will give up completely. In fact, in synchronous programming, the entire program would crash. In asynchronous programming, only the function that threw the error will crash. The rest of the program will continue to run.
💡 Tip
🦻🏻 Capturing the user event
Learning Objectives
We’ve introduced our state, and our render works for different values of that state. But users of our website can’t change the searchTerm
state themselves. We need to introduce a way for them to change the searchTerm
state via the UI.
To listen for the search input event, we can add an
addEventListener
by passing the event name and a handling function.
const searchBox = document.getElementById("search");
searchBox.addEventListener("input", handleSearchInput);
function handleSearchInput(event) {
// React to input event
}
When the “input” event fires, our handler function will run. Inside the handler we can access the updated input value: const searchTerm = event.target.value;
So our key steps are:
- Add an input event listener to the search box
- In the handler, get
value
of input element - Set the new state based on this value.
- Call our
render
function again.
⚠️ One thing at a time!
console.log
the search term.We will make sure this works before we try to change the UI. Why? If we try to add the event listener and something doesn’t work, we will only have a little bit of code to debug.
If we tried to solve the whole problem (updating the UI) and something didn’t work, we would have a lot of code to debug, which is harder!
We’ve now demonstrated that we can capture search text on every keystroke:
const searchBox = document.getElementById("search");
searchBox.addEventListener("input", handleSearchInput);
function handleSearchInput(event) {
const searchTerm = event.target.value;
console.log(searchTerm);
}
🧩 Break down the problem
Learning Objectives
We already have a website for displaying film listings.
Let’s think through building this film search interface step-by-step. Write down your sequence of steps to build this interface.
Given a view of film cards and search box
When a user types in the search box
Then the view should update to show only matching films.
- 🔍 Display search box and initial list of films
- 🦻🏽 Listen for user typing in search box
- 🎞️ Capture latest string when user types
- 🎬 Filter films list based on search text
- 📺 Update UI with filtered list
The key aspects we need to handle are capturing input and updating UI.
👂🏿 Capturing Input
We need to listen for the input
event on the search box to react as the user types. When the event fires, we can read the updated string value from the search box input element.
🎬 Filtering Data
Once we have the latest search text, we can filter the list of films. We can use JavaScript array methods like .filter()
to return films that match our search string.
🆕 Updating UI
With the latest filtered list of films in hand, we re-render these films to display the updated search results. We can clear the current film list and map over the filtered films to add updated DOM elements.
Thinking through these aspects separately helps frame the overall task. Next we can focus on each piece:
- 👂🏿 Listening for input
- 🎬 Filtering data
- 🆕 Re-rendering UI with the films example.
💡 Tip
💭 Why clear out the list and make new elements?
We could go through the existing elements, and change them. We could add a hidden
CSS class to ones we want to hide, and remove a hidden
CSS class from those we want to show.
But we prefer to clear out the list and make new elements. We do not want to change existing ones.
🧘🏽♂️ Do the simplest thing
It is simpler because we have fewer things to think about. With either approach, we need to solve the problem “which films do we want to show”. By clearing out elements, we then only have to solve the problem “how do I display a film?”. We don’t also need to think about “how do I hide a film?” or “how do I show a film that was hidden?”.
🍱 A place for everything
In our pattern we only deal with how we turn data into a card in one place. If we need to worry about changing how a card is displayed, that would have to happen somewhere else.
By making new cards, we avoid thinking about how cards change.
We can focus.
🧱 Composing elements
Learning Objectives
We can start by calling createElement
to create and
For now, we’ll only consider rendering the title
property from the film
object. Create this script in your index.html:
const film = {
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
};
const filmTitle = document.createElement("h3");
filmTitle.textContent = film.title;
console.log(filmTitle);
If we open up the console tab, we should be able to see this element logged in the console. However, it won’t yet appear in the browser.
💡 tip
If you see this error:
Uncaught ReferenceError: document is not defined
make sure you are running your code in the browser and not a terminal. Node doesn’t have the DOM API. You need to use your browser console. See how to set up your html if you are stuck.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Film View</title>
</head>
<body>
<script>
const film = {
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
};
const filmTitle = document.createElement("h3");
filmTitle.textContent = film.title;
console.log(filmTitle);
</script>
</body>
</html>
Appending elements
To display the film card, we need to append it to another element that is already in the DOM tree. For now let’s append it to the body, because that always exists.
|
|
We can extend this card to include more information about the film by creating more elements:
const film = {
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
};
const card = document.createElement("section");
const filmTitle = document.createElement("h3");
filmTitle.textContent = film.title;
card.append(filmTitle);
const director = document.createElement("p");
director.textContent = `Director: ${film.director}`;
card.append(director);
const duration = document.createElement("time");
duration.textContent = `${film.duration} minutes`;
card.append(duration);
const certificate = document.createElement("data");
certificate.textContent = `Certificate: ${film.certificate}`;
card.append(certificate);
document.body.append(card);
Eventually, we will include all the information, to match the wireframe. This is a bit tedious, as we had to write lots of similar lines of code several times, but it works.
🧼 Creating elements with functions
Learning Objectives
We now have a card showing all of the information for one film. The code we have is quite repetitive and verbose. It does similar things lots of times.
Let’s look at two ways we could simplify this code. First we will explore extracting a function. Then we’ll look at using <template>
tags.
Refactoring: Extracting a function
One way we can simplify this code is to refactor it.
💡 Definition: refactoring
We can identify things we’re doing several times, and extract a function to do that thing for us.
In this example, we keep doing these three things:
- Create a new element (sometimes with a different tag name).
- Set that element’s text content (always to different values).
- Appending that element to some parent element (sometimes a different parent).
We could extract a function which does these three things. The things which are different each time need to be parameters to the function.
We could write a function like this:
function createChildElement(parentElement, tagName, textContent) {
const element = document.createElement(tagName);
element.textContent = textContent;
parentElement.append(element);
return element;
}
And then rewrite our code to create the card like this:
const film = {
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
};
function createChildElement(parentElement, tagName, textContent) {
const element = document.createElement(tagName);
element.textContent = textContent;
parentElement.append(element);
return element;
}
const card = document.createElement("section");
createChildElement(card, "h3", film.title);
createChildElement(card, "p", `Director: ${film.director}`);
createChildElement(card, "time", `${film.duration} minutes`);
createChildElement(card, "data", `Certificate: ${film.certificate}`);
document.body.append(card);
This code does exactly the same thing as the code we had before. By introducing a function we have introduced some advantages:
- Our code is smaller, which can make it easier to read and understand what it’s doing.
- If we want to change how we create elements we only need to write the new code one time, not for every element. We could add a class attribute for each element easily.
- We can see that each element is being created the same way. Before, we would have to compare several lines of code to see this. Because we can see they’re calling the same function, we know they’re made the same way.
- We’re less likely to make mistakes copying and pasting the code. In the first version of this content, we actually wrote
duration.textContent = `Certificate: ${film.certificate}`;
instead ofcertificate.textContent = `Certificate: ${film.certificate}`;
because we were just copying and pasting and missed an update. The less we need to copy and paste and update code, the less likely we are to miss an update.
There are also some drawbacks to our refactoring:
- If we want to change how we create some, but not all, elements, we may have made it harder to make these changes. When we want to include an image of the director, or replace the certificate text with a symbol, we will have to introduce branching logic.
- To follow how something is rendered, we need to look in a few places. This is something you will need to get used to, so it’s good to start practising now.
🧼 Refactoring to state+render
Learning Objectives
We are going to introduce a common pattern in writing UIs, which is to define and use a function called render
.
Up until now, our film website has been static: it never changes. By introducing a search input, our website is becoming dynamic: it can change. This means that we may need to re-run the code which creates our UI elements.
So before we add the new functionality to our website, we are going to
render
:
const films = [
// You have this array from before.
];
function createFilmCard(filmData) {
// You should have an implementation of this function from before.
}
function render() {
const filmCards = films.map(createFilmCard);
document.body.append(...filmCards);
}
We’re missing one thing: We’re never calling our render
function! Call your render
function after you define it:
const films = [
// You have this array from last week.
];
function createFilmCard(filmData) {
// You should have an implementation of this function from last week.
}
function render() {
const filmCards = films.map(createFilmCard);
document.body.append(...filmCards);
}
render();
Your application should now work exactly the same as it did before. Because we moved our code into a function, this means we can call that function again if we need to, for instance when someone searches for something.
We saw this same pattern when we made the character limit component. We called the same function on page load, and when someone typed something.
Storing our state somewhere
Up until now, we had a variable called films
, and we created some cards based on that variable.
Let’s move this films
variable inside an object called state
, to make it clear to us what the state is in our application.
const state = {
films: [
{
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
},
{
title: "Typist Artist Pirate King",
director: "Carol Morley",
times: ["15:00", "20:00"],
certificate: "12A",
duration: 108,
},
],
};
Each time we need to store more information we should think: Is this a piece of state, or is this something we’re deriving from existing state? Whenever something in our state changes, we will tell our UI just to show “whatever is in the state” by calling the render
function. In this way, we simplify our UI code by making it a function of the state.
💡 Tip
state
. It was already state when it was called films
. But naming this variable state
can help us to think about it more clearly.Make sure to update any references to the films
variable you may have had before to instead reference state.films
.
This is another refactoring: we didn’t change what our application does, we just moved a variable.
🪃 Callbacks
Learning Objectives
Consider this visualisation of an asynchronous program:
👉🏽 Code running out of order and off the thread
When we call setTimeout
we send a function call to a client side Web API. The code isn’t executing in our single thread any more, so we can run the next line. The countdown is happening, but it’s not happening in code we control.
When the time runs out, the Web API sends a message to our program to let us know. This is called an
💡 Our call is back
With a pen and paper, draw a diagram of your mental model of the event loop.
Use your model to predict the order of logged numbers in the following code snippet:
setTimeout(function timeout1() {
console.log("1");
}, 2000);
setTimeout(function timeout2() {
console.log("2");
}, 500);
setTimeout(function timeout3() {
console.log("3");
}, 0);
console.log("4");
Did yours look different? There are many ways to visualise the event loop. Work on building your own mental model that helps you predict how code will run.
🪄 Reacting to user input
Learning Objectives
As users interact with web applications, they trigger events by doing things like clicking buttons, submitting forms, or typing text. We need to respond to these events. Let’s explore a common example: searching.
<label>
Search <input type="search" id="q" name="q" placeholder="Search term" /> 🔍
</label>
When a user types text into a search box, we want to capture their input and use it to filter and redisplay search results. This means the state of the application changes as the user types. We need to react to this change by updating the UI.
We’ll explore these ideas today. Code along with the examples in this lesson.
🪆 .then()
Learning Objectives
.then()
is a method that all Promise
s have. You can interpret this code:
const url = "https://api.github.com/users/SallyMcGrath";
const callback = (response) => response.json(); // .json() is an instance method that exists for all Response objects.
fetch(url).then(callback);
- given a request to
fetch
some data - when the
response
comes back / the promise resolves to a response object then
do this next thing with the data / execute this callback
The .then()
method takes in a callback function that will run once the promise resolves.
We can also inline the callback
variable here - this code does exactly the same as the code above:
const url = "https://api.github.com/users/SallyMcGrath";
fetch(url).then((response) => response.json());
It’s a similar idea as the event loop we have already investigated, but this time we can control it clearly. The .then()
method queues up callback functions to execute in sequence once the asynchronous operation completes successfully. This allows us to write code as if it was happening in time order.
💡 tip
then()
method of a Promise
always returns a new Promise
.We can chain multiple .then()
calls to run more logic, passing the resolved value to the next callback in the chain. This allows us to handle the asynchronous response in distinct steps. Let’s create a getProfile function in a file, call it, and try running the file with node:
const getProfile = (url) => {
return fetch(url)
.then((response) => response.json()) // This callback consumes the response string and parses it as JSON into an object.
.then((data) => data.html_url) // This callback takes the object and gets one property of it.
.then((htmlUrl) => console.log(htmlUrl)); // This callback logs that property.
};
getProfile("https://api.github.com/users/SallyMcGrath");
So then
returns a new Promise
, and you can call then
again on the new object. You can chain Promises in ever more complex dependent steps. This is called Promise chaining.
It’s important to understand some of what is happening with Promises and then
. But for the most part, you will not be writing code in this style.
🪞 Re-rendering the UI
Learning Objectives
With state updated from user input, we can re-render:
const render = (films) => {
// Clear existing DOM elements
// Map films to DOM elements
};
function handleInput(event) {
// capture search term
const { searchTerm } = event.target;
// Filter films on search term
filteredFilms = films.filter((film) => film.title.includes(searchTerm));
// Set new state
state.films = filteredFilms;
// Re-render UI with updated films
render(state.films);
}
💡 Things to consider
To re-render the UI, we need to update the DOM elements to match the latest state. We can do this by:
- Clearing existing DOM elements
- Mapping updated films data to new DOM elements
- Appending new elements to DOM
This is how we update the user interface in response to updated application state! We declare that our UI is a function of the state.
🧠 Our UI is a function of the state
Recalling our card function, let’s see how we can update the UI with the latest films data.
const render = (container, list) => {
container.textContent = ""; // clear the view
const cards = list.map((film) => createCard(template, film));
container.append(...cards);
};
const createCard = (template, { title, director }) => {
const card = template.content.cloneNode(true);
card.querySelector("h3").textContent = title;
card.querySelector("dd").textContent = director;
return card;
};
<template id="filmCardTemplate">
<section class="film-card">
<h3></h3>
<dl>
<dt>Director</dt>
<dd></dd>
</dl>
</section>
</template>
const films = [
{
title: "The Matrix",
director: "Lana Wachowski",
certificate: "15",
},
{
title: "Inception",
director: "Christopher Nolan",
certificate: "12A",
},
];
🫱🏿🫲🏽 Promises
Learning Objectives
To get data from a server, we make a request with fetch
. We act on what comes back: the response. But what happens in the middle? We already know that JavaScript is single-threaded: it can only do one thing at a time.
So do we just stop and wait? No! We have a special object to handle this time problem. Put this code in a file and run it with node:
const url = "https://api.github.com/users/SallyMcGrath"; // try your own username
const response = fetch(url);
console.log(response);
Your Promise should look like this:
Promise {
Response {
[Symbol(realm)]: null,
[Symbol(state)]: {
aborted: false,
rangeRequested: false,
timingAllowPassed: true,
requestIncludesCredentials: true,
type: 'default',
status: 200,
timingInfo: [Object],
cacheState: '',
statusText: 'OK',
headersList: [HeadersList],
urlList: [Array],
body: [Object]
},
[Symbol(headers)]: HeadersList {
cookies: null,
[Symbol(headers map)]: [Map],
[Symbol(headers map sorted)]: null
}
},
[Symbol(async_id_symbol)]: 54,
[Symbol(trigger_async_id_symbol)]: 30
}
The response
variable in this code is not labelling the data. It’s labelling a Promise
.
A promise is exactly what it sounds like: a promise to do something. You can use this promise object to sequence your code. You can say, “When the data comes back, then
do this.”
You will explore Promises in more detail as you build more complex applications. For now, let’s move on to .then()
.