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 asynchronous execution 🧶 🧶 asynchronous execution run code in a different order. To understand asynchrony we first need to be clear about synchronous execution 🧶 🧶 synchronous execution run code in the order it is written. .

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
Each line of code is run in order. This is synchronous execution. We do this because JavaScript is single threaded 🧶 🧶 single threaded A single thread can do one thing at a time. JavaScript is a single threaded language. .

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 eventListeners 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.

👉🏽 Visualise the Event Loop

🧠 Recap our concept map

graph LR TimeProblem[🗓️ Time Problem] --> |caused by| SingleThread[🧵 Single thread] SingleThread --> |send tasks to| ClientAPIs TimeProblem --> |solved by| Asynchrony[🛎️ Asynchrony] Asynchrony --> | delivered with | ClientAPIs{💻 Client APIs} ClientAPIs --> |like| setTimeout[(⏲️ setTimeout)] ClientAPIs --> |like| eventListener[(🦻🏾 eventListener)] ClientAPIs --> |like| fetch[(🐕 fetch)]

🃏 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

Refactor the 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

  1. 📤 we know that we can send a request using fetch()
  2. 🐕 we know that fetch is a 💻 client side 🧰 Web API
  3. 🗓️ we know that sending 📤 requests over a network takes time
  4. 🧵 we know that we should not stop our program to wait for data
  5. 🪃 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

graph LR Promise{{🤝 Promises}} --> |syntax| async{{🏃‍♂️ async}} async --> |syntax| await{{📭 await}} await --> |resolves to| Response{{📤 Response}}

Async/await is syntactic sugar 🧶 🧶 syntactic sugar A simpler, or “sweeter” way to write the same thing. The code works the same under the hood, but it’s easier to read. for Promises. We group them together: async/await, because we use them together. 🧶 🧶 use them together. We can only use 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);
}
  1. At this point in our codealong, when we open our page, what will we see?
  2. 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

Users don’t always type perfectly. How will you match their typing to the film titles? What if they type in all caps? What is the simplest thing that could possibly work?

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

Try to make your render function work even if someone searched for “pirates” or “PIRATES”.

🐕 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 client side Web API 🧶 🧶 client side Web API A client side Web API lives in the browser. They provide programmatic access to built-in browser functions from JavaScript. . Fetch will fetch our data from the server side API 🧶 🧶 server side API A server side API lives on a server. They provide programmatic access to data or functions stored on the server from JavaScript. .

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)
graph TD fetch[(🐕 fetch)] --> |sends a| Request{📤 Request} Request --> |has a latency| TimeProblem[🗓️ Time Problem] Request --> |to| ServerAPIs fetch --> |is a| ClientAPIs TimeProblem --> |caused by| SingleThread[🧵 Single thread] Callbacks{{🪃 Callbacks}} --> |run on| SingleThread SingleThread --> |handled by| EventLoop[🔁 Event Loop] EventLoop --> |queues| Callbacks SingleThread --> |send tasks to| ClientAPIs SingleThread --> |handled by| Asynchrony TimeProblem --> |solved by| Asynchrony[🛎️ Asynchrony] Asynchrony --> |delivered with| Promise{{🤝 Promises}} Asynchrony --> | delivered with | ClientAPIs Promise --> |resolve to a| Response{📤 Response} Promise --> |join the| EventLoop{{Event Loop 🔁}} Promise --> |syntax| async{{🏃‍♂️ async}} async --> |syntax| await{{📭 await}} await --> |resolves to| Response Response ---> |sequence with| then{{✔️ then}} APIs((🧰 APIs)) --> |live in your browser| ClientAPIs{💻 Client side APIs} ClientAPIs --> |like| setTimeout[(⏲️ setTimeout)] ClientAPIs --> |like| eventListener[(🦻🏾 eventListener)] APIs --> |live on the internet| ServerAPIs{🌐 Server side APIs} ServerAPIs --> |serve our| Data[(💾 Data)] Data --> |as a| Response

😵‍💫 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.

  1. 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.
  2. Update the state to start with an empty array. We can’t work with films we haven’t fetched yet!
const state = {
  films: [],
};
  1. Make a new getFilms function to use fetch to get the data from the API. The URL is //curriculum.codeyourfuture.io/js3/blocks/fetch-films/data.json

  2. Use:

  • fetch to get the data
  • async/await to make sure the function waits for the fetch to complete before trying to get the json data from the response
  • response.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:

// 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

🧧 Here’s an example implementation you can download.

👭🏾 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 one-to-one mapping 🧶 🧶 one-to-one mapping A one-to-one mapping associates every element in a set to exactly one element in another set between the data array and the UI components on the web page. Each item in the array matches a node in the UI. We can represent this diagrammatically by pairing up the data elements with their corresponding UI components:

--- title: One to one mapping between data and the UI components --- flowchart LR A[datum1] == createFilmCard(datum1) ==> B[UI component 1] C[datum2] == createFilmCard(datum2) ==> D[UI component 2]

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

single-film-display
A single film card

💾 ➡️ 💻 Data to UI

Learning Objectives

When we build user interfaces we often take data and render 🧶 🧶 render rendering is the process of building an interface from some code it in the user interface. We will model some real-life objects using data structures such as arrays and objects. However, end users don’t directly interact with data structures. Instead, they’ll interact with a rendering of these data structures through some user interface, typically a web browser. We’re going to start with some structured data and explore how we can render it on the page.

📝 Check-in ➡️ Coordinate

Learning Objectives

  1. Assemble as group
  2. Briefly discuss any particular areas of concern following the diagnose block
  3. 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 boolean 🧶 🧶 boolean true or false. On or off.

In our film example, we need two pieces of state:

  1. Our list of all films
  2. 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:

  1. 👩🏾‍🔬 Identify what state we have.
  2. ✍🏿 Define how to render the page based on that state.
  3. 🎱 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

film-cards
A grid of cards displaying film information

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 wireframe 🧶 🧶 wireframe A wireframe is a basic outline of a web page used for design purposes . This wireframe is built by reusing the same UI component 🧶 🧶 UI component A UI component is a reusable, self-contained piece of the UI. UI components are like lego blocks you can use to build websites. Most websites are made by “composing” components in this way. . Each film object is rendered as a card component. To build this user interface, we will start with data in the form of an array of objects, each with similar properties.

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

graph LR fetch[(🐕 fetch)] --> |sends a| Request{📤 Request} Request --> |has a latency| TimeProblem[🗓️ Time Problem]

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 higher order function 🧶 🧶 higher order function A higher-order function is a function that takes another function as an argument or returns a new function . In this case, it means we pass a function as an argument to 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”

--- title: One to one mapping - doubling each number in an array --- flowchart LR A[5] == double(5) ==> B[10] C[20] == double(20) ==> D[40] E[30] == double(30) ==> F[60]

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:

1
2
3
4
5
6
function double(num) {
  return num * 2;
}

const numbers = [5, 20, 30];
const doubledNums = numbers.map(double);

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 error 🧶 🧶 error An Error is a global object produced when something goes wrong. We can throw an Error manually with the throw keyword. We can use try/catch in both synchronous and asynchronous code.

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 crash. 🧶 🧶 crash. The JavaScript execution will halt with a fatal exception, causing the Node.js process to exit immediately. Any further statements will not be run.

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

Handle your errors in all cases.

🦻🏻 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 event listener 🧶 🧶 event listener An event listener waits for a specific event to occur. It runs in response to things like clicks, and key presses. We register listeners with 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:

  1. Add an input event listener to the search box
  2. In the handler, get value of input element
  3. Set the new state based on this value.

⚠️ warning

But we’re not going to do all of these at once! Stop and implement just the first two steps (adding the event listener, and getting the value), and 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

graph LR A[Render UI] --> B[User types] B --> C[Capture event] C --> D[Filter data] D --> E[Update state] E --> A
  1. 🔍 Display search box and initial list of films
  2. 🦻🏽 Listen for user typing in search box
  3. 🎞️ Capture latest string when user types
  4. 🎬 Filter films list based on search text
  5. 📺 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:

  1. 👂🏿 Listening for input
  2. 🎬 Filtering data
  3. 🆕 Re-rendering UI with the films example.

💡 tip

We clear the current film list and then add elements based on our new list.

💭 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 compose DOM elements 🧶 🧶 compose DOM elements To compose DOM elements means to combine DOM elements to form some part of the user interface. .

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, make sure you are running your code in the browser and not VSCode. 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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;

document.body.append(filmTitle);

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 refactor 🧶 🧶 refactor Refactoring is when we change how our code is structured, without changing what it does. Even though we have changed our code, it does exactly the same thing it did before. . Find your code from last week that creates the film cards and adds them to the page. Move your code into a function called 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

We don’t need to store our state in a variable called 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

To refactor means to update our code quality without changing the implementation.

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:

  1. Create a new element (sometimes with a different tag name).
  2. Set that element’s text content (always to different values).
  3. 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:

  1. Our code is smaller, which can make it easier to read and understand what it’s doing.
  2. 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.
  3. 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:

  1. 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.
  2. 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 event 🧶 🧶 event An event is a signal that something has happened. . Our API sends its message to our event loop 🧶 🧶 event loop The event loop is a JavaScript mechanism that handles asynchronous callbacks. . And what message does the event loop send? It sends a callback. It sends our call back. It tells our thread to run the code in that function.

💡 tip

A callback is our function call, sent back to us through the event loop, for us to run.

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);
graph Callbacks{{🪃 Callbacks}} --> |run on| SingleThread[🧵 Single thread] SingleThread --> |handled by| EventLoop[🔁 Event Loop] EventLoop --> |queues| Callbacks SingleThread --> |send tasks to| ClientAPIs{💻 Client APIs} ClientAPIs --> | send| Callbacks

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

graph LR Promise{{🤝 Promises}} --> |resolve to a| Response{📤 Response} Response ---> |sequence with| then{{🪆️ then}}

.then() is a method that belongs to the Promise prototype 🧶 🧶 prototype A prototype object is like a template. then is a method available on any Promise. You can think of the commands as

  1. given a request to fetch some data
  2. when the response comes back / the promise resolves to a response object
  3. 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

The 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

Users don’t always type perfectly. How will you match their typing to the film titles? What if they type in all caps? What is the simplest thing that could possibly work?

To re-render the UI, we need to update the DOM elements to match the latest state. We can do this by:

  1. Clearing existing DOM elements
  2. Mapping updated films data to new DOM elements
  3. 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

graph LR Asynchrony --> |delivered with| Promise{{🤝 Promises}} Promise --> |resolve to a| Response{📤 Response} Promise --> |join the| EventLoop{{Event Loop 🔁}}

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().