🐥 JS3
Basically, asynchronous programming, templates, and state in JavaScript
- JS3
- ⏳ Asynchrony : outside time
- 🃏 Building a component
- 🌐 Requesting from a server side API
- 🍬 async/await
- 🍱 Simplifying element creation
- 🎱 Rendering based on state
- 🐕 Fetching data
- 🐕 🎞️ fetch films
- 👭🏾 One to one
- 💽 Single datum
- 💾 ➡️ 💻 Data to UI
- 📝 Check-in ➡️ Coordinate
- 🔎 Identifying state
- Chaining Promises
- fetch API
- How the internet works
- 🆕 Introducing new state
- 🌡️ Diagnose
- 📽️ Cinema listings
- 🗓️ Latency
- 🗺️ Using map
- 🥎 try/catch
- 🦻🏻 Capturing the user event
- 🧩 Break down the problem
- 🧱 Composing elements
- 🧼 Refactoring to state+render
- 🧼 Simplifying element creation
- 🪃 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. But here’s a new idea: eventListeners are part of the Event API. They are not part of JavaScript! 🤯 This means you can’t use them in a Node REPL, but they are implemented in web browsers. The core of JavaScript 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
cannot run until the user types. With fetch
, the callback function cannot 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
🃏 Building a component
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.
const film = {
title: "Killing of Flower Moon",
director: "Martin Scorsese",
times: ["15:35"],
certificate: "15",
duration: 112,
};
const createFilmCard = (template, 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("p").textContent = `Director: ${film.director}`;
card.querySelector("time").textContent = `${film.duration} minutes`;
card.querySelector("data").textContent = `Certificate: ${film.certificate}`;
// Return the card, rather than directly appending it to the page
return card;
};
const template = document.getElementById("film-card");
const filmCard = createFilmCard(template, film);
// Remember we need to append the card to the DOM for it to appear.
document.body.append(filmCard);
exercise
createFilmCard
function to use object destructuring in the 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. In our filterFilms code, replace the films array with data fetched from a server.
// Begin with an empty state
const state = {
films: [],
};
// Data
const endpoint = "//curriculum.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) => {
render(filmContainer, films); // when
});
🐕 fetch
returns a 🫱🏿🫲🏽 Promise
; the 🫱🏿🫲🏽 Promise
fulfils itself with a 📥 response; the response contains our 💾 data.
We will dig into this syntax: Promises
, async
, await
, and then
in our next sprint and complete our concept map.
🍬 async/await
Learning Objectives
Async/await is
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:
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 wait for a Promise to resolve. This allows us to write code that looks like it’s happening in time order, but doesn’t block our main thread.
const getProfile = async (url) => {
const response = await fetch(url);
return response.json();
};
Go ahead and call this in your Node REPL in your terminal: getProfile("https://api.github.com/users/SallyMcGrath").then(console.log)
. It works the same as before.
🫠 Handling errors
When we use await
, we are saying, “Wait for this Promise to resolve before moving on to the next line of code.” But if the Promise doesn’t resolve, the next line of code will never run and an error will be thrown.
Let’s try this. Call getProfile
with a url that doesn’t exist: getProfile("invalid_url");
You will get a curious response:
Uncaught (in promise) TypeError...
getProfile("invalid_url")
Promise {
<pending>,
[...]
}
> Uncaught [TypeError: Failed to parse URL from invalid_url] {
[cause]: TypeError: Invalid URL
[...] {
code: 'ERR_INVALID_URL',
input: 'invalid_url'
}
}
Some lines redacted […] for clarity.
JavaScript is telling us we need to catch
the error, but how, and why?
🍱 Simplifying element creation
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>
<p data-certificate>Certificate</p>
</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.
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 = 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
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
💡 tip
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
🐕 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 should
fetch
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 our knowledge to get some data from an API. There’s a list of films stored in a JSON file in this directory. We’ll use fetch
to get the data from this API and then render it to the page.
🎯 Success criterion: You have a working app that fetches data from an API and renders it to the page.
🧠 Think back to your filterFilms project.
- Find your completed code. You’re going to iterate on this code to fetch the films from the API instead of using the data in the file.
- Update the state to start with an empty array. We can’t work with films we haven’t fetched yet!
const state = {
films: [],
};
Make a new
getFilms
function to usefetch
to get the data from the API. The URL is//curriculum.codeyourfuture.io/js3/blocks/fetch-films/data.json
Use:
fetch
to get the dataasync
/await
to make sure the function waits for the fetch to complete before trying to get the json data from the responseresponse.json()
to get the data from the response- a
try...catch
block to handle any errors that might occur
const getFilms = async () => {
try {
const response = await fetch(
"//curriculum.codeyourfuture.io/js3/blocks/fetch-films/data.json"
);
return await response.json();
} catch (error) {
console.error(error);
return [];
}
};
We’ve added a try...catch
block to handle any errors that might occur. We’ve also added await
to the fetch
and response.json()
calls. This means that the function will sensibly wait for the fetch
to complete before trying to get the json data from the response.
In our last implementation, we called the render function straight away. This time, we need to wait for the films to be fetched before we can render them. Write a new async function to initialise our app. Try to write it yourself first, then check your understanding below.
Your init
function should look something like this:
init
function should look something like this:// Initial render, which is distinct from the render function as it loads our films into memory from the API.
// Subsequent render calls do not need to call the API to get the films - we already know the films and can remember them.
async function init() {
try {
const films = await getFilms();
state.films = films;
render(filmContainer, films);
} catch (error) {
console.error(error);
}
}
The name init
is a convention. It has no special meaning in the JavaScript language.
🎁 Finally!
And let’s now call this function at the end of our script.
init();
💡 tip
👭🏾 One to one
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
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.
💽 Single datum
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
💾 ➡️ 💻 Data to 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
🔎 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 our website had light mode and dark mode, we would not have one state for “is dark mode enabled” and one state for “is light mode enabled”. We would have one piece of state: a
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 new 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
Learning Objectives
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, 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.
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. We need to handle this time problem.
Programming often involves time problems, and latency is just one of them.
🗺️ 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.
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 a local project, render this array of components in the browser.
🥎 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.
⚠️ warning
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);
}
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.
🧩 Break down the problem
Learning Objectives
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
<!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");
duration.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.
🧼 Refactoring to state+render
Learning Objectives
We are going to introduce a common pattern in writing UIs, which is to 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 last time.
];
function createFilmCard(filmData) {
// You should have an implementation of this function from last time.
}
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.
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.This is another refactoring: we didn’t change what our application does, we just moved a variable.
🧼 Simplifying element creation
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.
💡 tip
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 parameteres 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", 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.
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.
exercise
Stretch goal: Add the datetime
A <time>
element needs a datetime
attribute. Add this to the createChildElement
function to express the duration on the time element only.
PT1H52M
is the ISO 8601
format for 112
minutes.
🪃 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 our thread.
When the time runs out, our Web API sends a message to our program to let us know. This is called an
💡 tip
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 timeout() {
console.log("1");
}, 2000);
setTimeout(function timeout() {
console.log("2");
}, 500);
setTimeout(function timeout() {
console.log("3");
}, 0);
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 like clicking buttons, submitting forms, or typing text that we need to respond to. Let’s explore a common example: searching.
<label>
<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 belongs to the Promise
then
is a method available on any Promise.
- 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.
For example:
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);
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 which we can try out in our Node REPL:
const getProfile = (url) => {
return fetch(url)
.then((response) => response.json()) // This callback consumes the response 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);
}
💡 tip
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. Run this code in your Node REPL:
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
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()
.