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 Asynchronous execution is running code in a different order than it was written. To understand asynchrony we first need to be clear about synchronous execution 🧶 🧶 synchronous execution Synchronous execution is running 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.

const search = document.getElementById("search");
search.addEventListener("input", handleInput);

When we called addEventListener it didn’t immediately call handleInput.

But here’s a new idea: eventListeners are part of the Event API. They are not part of the JavaScript language! 🤯 This means you can’t use them in a Node REPL. But they are implemented in web browsers. The core of JavaScript (e.g. strings and functions) is the same everywhere, but different contexts may add extra APIs.

When you set an eventListener you are really sending a call to a Web API and asking it do something for you.

const search = document.getElementById("search");
search.addEventListener("input", handleInput);

The callback handleInput does not run until the user types. With fetch, the callback function does not run until the data arrives. In both cases, we are waiting for something to happen before we can run our code.

We use a function as a way of wrapping up the code that needs to be run later on. This means we can tell the browser what to do when we’re done waiting.

👉🏽 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)]

🃏 Reusable components

Learning Objectives

Recall our sub-goal:

🎯 Sub-goal: Build a film card component

Now that we have made a card work for one particular film, we can re-use that code to render any film object in the user interface with a general component. To do this, we wrap up our code inside a JavaScript function. JavaScript functions reuse code: so we can implement reusable UI components using functions.

We could use either our createChildElement implementation or our <template> implementation - making a component function works the same for either. As an example, we will use the <template> implementation:

const film = {
  title: "Killing of Flower Moon",
  director: "Martin Scorsese",
  times: ["15:35"],
  certificate: "15",
  duration: 112,
};

const template = document.getElementById("film-card");
const createFilmCard = (film) => {
  const card = template.content.cloneNode(true);
  // Now we are querying our cloned fragment, not the entire page.
  card.querySelector("h3").textContent = film.title;
  card.querySelector("[data-director]").textContent = `Director: ${film.director}`;
  card.querySelector("time").textContent = `${film.duration} minutes`;
  card.querySelector("[data-certificate]").textContent = `Certificate: ${film.certificate}`;
  // Return the card, rather than directly appending it to the page
  return card;
};
const filmCard = createFilmCard(film);

// Remember we need to append the card to the DOM for it to appear.
document.body.append(filmCard);

Exercise: Use destructuring

Refactor the createFilmCard function to use object destructuring for the film parameters.

🌐 Requesting from a server side API

Learning Objectives

So now we have these pieces of our giant concept map

  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.

Let’s pick our film display exercise back up. Before we had a list of films hard-coded in our state. We’re going to replace the films array with data fetched from a server.

// Begin with an empty state
const state = {
  films: [],
  searchTerm: "",
};

const endpoint = "https://programming.codeyourfuture.io/dummy-apis/films.json";

const fetchFilms = async () => {
  const response = await fetch(endpoint);
  return await response.json();
}; // Our async function returns a Promise

fetchFilms().then((films) => {
   // When the fetchFilms Promise resolves, this callback will be called.
  state.films = films;
  render();
});

fetch returns a Promise; the Promise fulfils itself with a response; the response contains our data.

Next we will dig into Promises, async, await, and then, and complete our concept map.

🍬 async/await

Learning Objectives

These two blocks of code do exactly the same thing:

const getProfile = async (url) => {
  const response = await fetch(url);
  const data = await response.json();
  const htmlUrl = data.html_url;
  console.log(htmlUrl);
}

getProfile("https://api.github.com/users/SallyMcGrath");
const getProfile = (url) => {
  return fetch(url)
    .then((response) => response.json())
    .then((data) => data.html_url)
    .then((htmlUrl) => console.log(htmlUrl));
};
getProfile("https://api.github.com/users/SallyMcGrath");

Async/await is 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 async and await 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. Save this in a file and run it with node:

const getProfile = async (url) => url;

console.log(getProfile("hello")); // Logs a Promise.

getProfile("hello").then((value) => console.log(value)); // Logs a value

Even though the function above doesn’t have a time problem, the fact that we define the function as an async function means it returns a Promise.

But let’s do something more interesting - let’s actually solve a time problem.

const getProfile = async (url) => {
  // the async keyword tells us this function handles a time problem
};

We use the await operator to say “don’t move on until this is done”. Importantly, we are not actually waiting for a Promise to resolve. We are scheduling a callback that will be called when the Promise resolves. But this allows us to write code that looks like it’s happening in time order (as if we are waiting), without actually blocking our main thread.

const getProfile = async (url) => {
  const response = await fetch(url);
  return response.json();
};

getProfile("https://api.github.com/users/SallyMcGrath")
  .then((response) => console.log(response))

Save this to a file and run with with node. It works the same as before.

🍱 Creating elements with <template>

Learning Objectives

Using <template> tags

We could simplify this code with a different technique for creating elements.

Until now, we have only seen one way to create elements: document.createElement. The DOM has another way of creating elements - we can copy existing elements and then change them.

HTML has a useful tag designed to help make this easy, the <template> tag. When you add a <template> element to a page, it doesn’t get displayed when the page loads. It is an inert fragment of future HTML.

We can copy any DOM node, not just <template> tags. For this problem, we will use a <template> tag because it is designed for this purpose.

When we copy an element, its children get copied. This means we can write our template card as HTML:

<template id="film-card">
  <section>
    <h3>Film title</h3>
    <p data-director>Director</p>
    <time>Duration</time>
    <data data-certificate>Certificate</data>
  </section>
</template>

This is our template card. Place it in the body of your html. It doesn’t show up! Template HTML is like a wireframe; it’s just a plan. We can use this template to create a card for any film object. We will clone (copy) this template and populate it with data. Replace the contents of your <script> tag with this:

const film = {
  title: "Killing of Flower Moon",
  director: "Martin Scorsese",
  times: ["15:35"],
  certificate: "15",
  duration: 112,
};

const card = document.getElementById("film-card").content.cloneNode(true);
// Now we are querying our cloned fragment, not the entire page.
card.querySelector("h3").textContent = film.title;
card.querySelector("[data-director]").textContent = `Director: ${film.director}`;
card.querySelector("time").textContent = `${film.duration} minutes`;
card.querySelector("[data-certificate]").textContent = `Certificate: ${film.certificate}`;

document.body.append(card);

This code will produce the same DOM elements in the page as the two other versions of the code we’ve seen (the verbose version, and the version using createChildElement).

The first two approaches (the verbose version, and the createChildElement version) did so by calling the same DOM functions as each other.

This approach uses different DOM functions. But it has the same effect.

Exercise: Consider the trade-offs

We’ve now seen two different ways of simplifying our function: extracting a function, or using a template tag.

Both have advantages and disadvantages.

Think of at least two trade-offs involved. What is better about the “extract a function” solution? What is better about the template tag solution? Could we combine them?

Share your ideas about trade-offs in a thread in Slack.

🎱 Rendering based on state

Learning Objectives

For now, we have set the initial value of the searchTerm state to “Pirate”. This means that our render function should only create cards for films which contain the word “Pirate” in their title. But right now, our render function creates cards for all of the films.

In our render function, we must filter our list down to the films that match our search term. This does not require us to introduce new state. We can derive a filtered list from our existing state.

Filter function

We can use the higher order array function .filter() to return a new array of films that include the search term:

const filteredFilms = state.films.filter((film) =>
  film.title.includes(state.searchTerm)
);

We can change our render function to always do this. If searchTerm is empty, our filter function will return all the films:

function render() {
  const filteredFilms = state.films.filter((film) =>
    film.title.includes(state.searchTerm)
  );
  const filmCards = filteredFilms.map(createFilmCard);
  document.body.append(...filmCards);
}
  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

💡 Things to consider

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: Make search more user friendly

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 look again at our code for fetching film data:

const endpoint = "https://programming.codeyourfuture.io/dummy-apis/films.json";

const fetchFilms = async () => {
  const response = await fetch(endpoint);
  return await response.json();
};

fetchFilms().then((films) => {
   // When the fetchFilms Promise resolves, this callback will be called.
  state.films = films;
  render();
});

We are defining fetchFilms: an async function - a function which returns a Promise.

When we call fetchFilms, what we get is an unresolved Promise.

What fetchFilms does is fetch a URL (with our call to fetch itself returning a Promise resolving to a Response). When the Promise from fetch resolves, fetchFilms reads the body of the Response (a string), and parses is as JSON. The Promise returned by fetchFilms then resolves with the result of parsing the string as JSON.

When the Promise from fetchFilms resolves, our next callback is called: We update our state, and call render().

After this is done, the rest of our code works exactly the same as it did before. We have our list of films in our state, so we never need to fetch the list of films again.

render works the same - it only cares that state.films is an array of films, it doesn’t care where they came from.

When we change our filter by typing, events fire and our event handler will be called back exactly the same as it did before.

👭🏾 One-to-one mappings

Learning Objectives

We can now render any one film data object in the UI. However, to fully solve this problem we must render a list of all of the film objects. For each film object, we need to render a corresponding film card in the UI. In this case, there is a 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]

Given an array named films

To create an array of card components we can iterate through the film data using a for...of loop:

const filmCards = [];
for (const item of films) {
  filmCards.push(createFilmCard(item));
}

document.body.append(...filmCards);
// invoke append using the spread operator

However, there are alternative methods for building this array of UI components.

💽 Rendering one card

Learning Objectives

🎯 Sub-goal: Build a film card component

To break down this problem, we’ll render a single datum, before doing this for the whole list. Here’s one film:

const film = {
  title: "Killing of Flower Moon",
  director: "Martin Scorsese",
  times: ["15:35"],
  certificate: "15",
  duration: 112,
};

Starting with this object, we’ll focus only on building this section of the user interface:

🖼️ Open this wireframe of single film card

single-film-display
A single film card

💾 ➡️ 💻 Rendering Data as 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

🔁 Actually re-rendering

Learning Objectives

We have seen that when we search, we’re only adding new elements, and not removing existing elements from the page.

We previously identified our strategy of clearing old elements before adding new ones. But we are not doing this.

We can clear out the existing children of an element by setting its textContent propery to the empty string:

document.body.textContent = "";

Add this to your render function before you add new elements. Try using your page. Try searching for a particular film.

Oh no, our search box is gone!

exercise

Work out why our search box is gone. Remember what we just changed, and what we were trying to do by making that change.

We removed our search box from the page because we removed everything from the entire document body.

This was not our intention - we only wanted to remove any films we had previously rendered.

A way to solve this is to introduce a container element which our render function will re-fill every time it’s called.

We should identify which elements in our page should be re-drawn every time we render, and which should always be present.

Introduce a new container, and keep any “always present” UI elements outside of it. Update your render function to clear and append to the container, not the whole body.

Remember to use semantic HTML. Your container should be an appropriate tag for the contents it will have.

🔁 Re-rendering

Learning Objectives

Now that we’ve shown we can log the search text, we can set the new value of the searchTerm state, and re-render the page.

We should have a page like this:

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Film View</title>
  </head>
  <body>
    <label>Search <input type="text" id="search" /></label>
    <template id="film-card">
      <section>
        <h3>Film title</h3>
        <p data-director>Director</p>
        <time>Duration</time>
        <data data-certificate>Certificate</data>
      </section>
    </template>
    <script>
      const state = {
        films: [
          {
            title: "Killing of Flower Moon",
            director: "Martin Scorsese",
            times: ["15:35"],
            certificate: "15",
            duration: 112,
          },
          {
            title: "Typist Artist Pirate King",
            director: "Carol Morley",
            times: ["15:00", "20:00"],
            certificate: "12A",
            duration: 108,
          },
        ],
        searchTerm: "",
      };

      const template = document.getElementById("film-card");
      const createFilmCard = (film) => {
        const card = template.content.cloneNode(true);
        // Now we are querying our cloned fragment, not the entire page.
        card.querySelector("h3").textContent = film.title;
        card.querySelector("[data-director]").textContent = `Director: ${film.director}`;
        card.querySelector("time").textContent = `${film.duration} minutes`;
        card.querySelector("[data-certificate]").textContent = `Certificate: ${film.certificate}`;
        // Return the card, rather than directly appending it to the page
        return card;
      };

      function render() {
        const filteredFilms = state.films.filter((film) =>
          film.title.includes(state.searchTerm)
        );
        const filmCards = filteredFilms.map(createFilmCard);
        document.body.append(...filmCards);
      }

      const searchBox = document.getElementById("search");

      searchBox.addEventListener("input", handleSearchInput);

      function handleSearchInput(event) {
        const searchTerm = event.target.value;
        console.log(searchTerm);
      }

      render();
    </script>
  </body>
</html>

We want to change our search input handler to update state.searchTerm and call render() again.

Implement this and try searching. What happens? Play computer to work out why what’s happening isn’t what we expected.

🔎 Identifying state

Learning Objectives

🕞 State: data which may change over time.

We store each piece of state in a variable. When we render in the UI, our code will look at the state in those variables. When the state changes, we render our UI again based on the new state.

“What the state used to be” or “How the state changed” isn’t something we pay attention to when we render. We always render based only on the current state.

We want to have as few pieces of state as possible. We want them to be fundamental.

Some guidelines for identifying the state for a problem:

✔️ If something can change it should be state.

In our film example, the search term can change, so it needs some state associated with it.

❌ But if something can be derived it should not be state.

In our film example, we would not store “is the search term empty” and “what is the search term” as separate pieces of state. We can work this answer out ourselves. This answer can be derived. We can answer the question “is the search term empty” by looking at the search term. We don’t need two variables: we can use one.

🖇️ If two things always change together, they should be one piece of state.

If a website allows log-in, we would not have one state for “is a user logged in” and one state for “what user is logged in”. We would have one piece of state: The currently logged in user. We would set that state to null if there is no logged in user. We can answer the question “is a user logged in” by checking if the currently logged in user is null. We don’t need a separate piece of state.

State in our example

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

  1. Our list of all films
  2. The search term

When we introduce filtering films based on the search term we will not introduce other new state. We will not store a filtered list of films in state. Our filtered list of films can be derived from our existing state.

Chaining Promises

fetch API

Let’s suppose we have a remote API hosted at the following url: “https://api-film-data.com”.

We can use applications like Postman to make requests to APIs. However, we want to make a request for the film data using JavaScript. We can use fetch to make network requests in JavaScript. Let’s take a look at how we can do this:

const filmData = fetch("https://api-film-data.com/films");

fetch is a JavaScript function. We call fetch using the url of the remote API we wish to fetch data from. Once fetch has got the data then we want to store it in a variable so we can then use it in our application. Let’s log this data:

const filmData = fetch("https://api-film-data.com/films");
console.log(filmData);

However, if we log this variable we don’t get an array of data. We get:

Promise <pending>

How the internet works

Learning Objectives

We’ve been using the internet for years, but how does it actually work? What happens when you type a URL into a browser? How does the browser know where to go? How does it know what to show? How does it know how to show it?

🆕 Introducing new state

We are introducing a new feature: being able to search for films. We have identified that this introduces one new element of state: the search term someone has asked for.

Let’s add it to our state object:

const state = {
  films: [
    {
      title: "Killing of Flower Moon",
      director: "Martin Scorsese",
      times: ["15:35"],
      certificate: "15",
      duration: 112,
    },
    {
      title: "Typist Artist Pirate King",
      director: "Carol Morley",
      times: ["15:00", "20:00"],
      certificate: "12A",
      duration: 108,
    },
  ],
  searchTerm: "",
};

We needed to pick an initial value for this state. We picked the empty string, because when someone first loads the page, they haven’t searched for anything. When someone types in the search box, we will change the value of this state, and re-render the page.

We could pick any initial value. This actually allows us to finish implementing our render function before we even introduce a search box into the page. In real life, our searchTerm state will be empty at first, but we can use different values to help us with development. We can make the page look like someone searched for “Pirate”, even before we introduce a search box in the UI.

This is because we have split up our problem into three parts:

  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 films 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 (like show the rest of the page, or respond to a user clicking in the page). We need to handle this time problem.

Programming often involves time problems, and latency is just one of them.

🗺️ Applying map to our problem

Learning Objectives

Now that we understand map, let’s ty to use it in our project.

Given the list of film data:

const films = [
  {
    title: "Killing of Flower Moon",
    director: "Martin Scorsese",
    times: ["15:35"],
    certificate: "15",
    duration: 112,
  },
  {
    title: "Typist Artist Pirate King",
    director: "Carol Morley",
    times: ["15:00", "20:00"],
    certificate: "12A",
    duration: 108,
  },
];

Use createFilmCard and map to create an array of film card components. In your local project, render this array of components in the browser.

🗺️ Using map

Learning Objectives

We want to create a new array by applying a function to each element in the starting array. Earlier, we used a for...of statement to apply the function createFilmCard to each element in the array. However, we can also build an array using the map array method. map is a 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.

🥎 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.
  4. Call our render function again.

⚠️ One thing at a time!

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);
}

🧩 Break down the problem

Learning Objectives

We already have a website for displaying film listings.

Let’s think through building this film search interface step-by-step. Write down your sequence of steps to build this interface.

Given a view of film cards and search box
When a user types in the search box
Then the view should update to show only matching films.

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:

Uncaught ReferenceError: document is not defined

make sure you are running your code in the browser and not a terminal. Node doesn’t have the DOM API. You need to use your browser console. See how to set up your html if you are stuck.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Film View</title>
  </head>
  <body>
    <script>
      const film = {
        title: "Killing of Flower Moon",
        director: "Martin Scorsese",
        times: ["15:35"],
        certificate: "15",
        duration: 112,
      };
      const filmTitle = document.createElement("h3");
      filmTitle.textContent = film.title;
      console.log(filmTitle);
    </script>
  </body>
</html>

Appending elements

To display the film card, we need to append it to another element that is already in the DOM tree. For now let’s append it to the body, because that always exists.

 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");
certificate.textContent = `Certificate: ${film.certificate}`;
card.append(certificate);

document.body.append(card);

Eventually, we will include all the information, to match the wireframe. This is a bit tedious, as we had to write lots of similar lines of code several times, but it works.

🧼 Creating elements with functions

Learning Objectives

We now have a card showing all of the information for one film. The code we have is quite repetitive and verbose. It does similar things lots of times.

Let’s look at two ways we could simplify this code. First we will explore extracting a function. Then we’ll look at using <template> tags.

Refactoring: Extracting a function

One way we can simplify this code is to refactor it.

💡 Definition: refactoring

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 parameters to the function.

We could write a function like this:

function createChildElement(parentElement, tagName, textContent) {
  const element = document.createElement(tagName);
  element.textContent = textContent;
  parentElement.append(element);
  return element;
}

And then rewrite our code to create the card like this:

const film = {
  title: "Killing of Flower Moon",
  director: "Martin Scorsese",
  times: ["15:35"],
  certificate: "15",
  duration: 112,
};

function createChildElement(parentElement, tagName, textContent) {
  const element = document.createElement(tagName);
  element.textContent = textContent;
  parentElement.append(element);
  return element;
}

const card = document.createElement("section");

createChildElement(card, "h3", film.title);

createChildElement(card, "p", `Director: ${film.director}`);

createChildElement(card, "time", `${film.duration} minutes`);

createChildElement(card, "data", `Certificate: ${film.certificate}`);

document.body.append(card);

This code does exactly the same thing as the code we had before. By introducing a function we have introduced some advantages:

  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.
  4. We’re less likely to make mistakes copying and pasting the code. In the first version of this content, we actually wrote duration.textContent = `Certificate: ${film.certificate}`; instead of certificate.textContent = `Certificate: ${film.certificate}`; because we were just copying and pasting and missed an update. The less we need to copy and paste and update code, the less likely we are to miss an update.

There are also some drawbacks to our refactoring:

  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.

🧼 Refactoring to state+render

Learning Objectives

We are going to introduce a common pattern in writing UIs, which is to define and use a function called render.

Up until now, our film website has been static: it never changes. By introducing a search input, our website is becoming dynamic: it can change. This means that we may need to re-run the code which creates our UI elements.

So before we add the new functionality to our website, we are going to 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 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 before.
];

function createFilmCard(filmData) {
  // You should have an implementation of this function from before.
}

function render() {
  const filmCards = films.map(createFilmCard);
  document.body.append(...filmCards);
}

We’re missing one thing: We’re never calling our render function! Call your render function after you define it:

const films = [
  // You have this array from last week.
];

function createFilmCard(filmData) {
  // You should have an implementation of this function from last week.
}

function render() {
  const filmCards = films.map(createFilmCard);
  document.body.append(...filmCards);
}

render();

Your application should now work exactly the same as it did before. Because we moved our code into a function, this means we can call that function again if we need to, for instance when someone searches for something.

We saw this same pattern when we made the character limit component. We called the same function on page load, and when someone typed something.

Storing our state somewhere

Up until now, we had a variable called films, and we created some cards based on that variable.

Let’s move this films variable inside an object called state, to make it clear to us what the state is in our application.

const state = {
  films: [
    {
      title: "Killing of Flower Moon",
      director: "Martin Scorsese",
      times: ["15:35"],
      certificate: "15",
      duration: 112,
    },
    {
      title: "Typist Artist Pirate King",
      director: "Carol Morley",
      times: ["15:00", "20:00"],
      certificate: "12A",
      duration: 108,
    },
  ],
};

Each time we need to store more information we should think: Is this a piece of state, or is this something we’re deriving from existing state? Whenever something in our state changes, we will tell our UI just to show “whatever is in the state” by calling the render function. In this way, we simplify our UI code by making it a function of the state.

💡 Tip

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 more clearly.

Make sure to update any references to the films variable you may have had before to instead reference state.films.

This is another refactoring: we didn’t change what our application does, we just moved a variable.

🪃 Callbacks

Learning Objectives

Consider this visualisation of an asynchronous program:

👉🏽 Code running out of order and off the thread

When we call setTimeout we send a function call to a client side Web API. The code isn’t executing in our single thread any more, so we can run the next line. The countdown is happening, but it’s not happening in code we control.

When the time runs out, the Web API sends a message to our program to let us know. This is called an event 🧶 🧶 event An event is a signal that something has happened. . The 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.

💡 Our call is back

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 timeout1() {
  console.log("1");
}, 2000);
setTimeout(function timeout2() {
  console.log("2");
}, 500);
setTimeout(function timeout3() {
  console.log("3");
}, 0);
console.log("4");
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 by doing things like clicking buttons, submitting forms, or typing text. We need to respond to these events. Let’s explore a common example: searching.

<label>
  Search <input type="search" id="q" name="q" placeholder="Search term" /> 🔍
</label>

When a user types text into a search box, we want to capture their input and use it to filter and redisplay search results. This means the state of the application changes as the user types. We need to react to this change by updating the UI.

We’ll explore these ideas today. Code along with the examples in this lesson.

🪆 .then()

Learning Objectives

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

.then() is a method that all Promises have. You can interpret this code:

const url = "https://api.github.com/users/SallyMcGrath";
const callback = (response) => response.json(); // .json() is an instance method that exists for all Response objects.
fetch(url).then(callback);
  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.

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 in a file, call it, and try running the file with node:

const getProfile = (url) => {
  return fetch(url)
    .then((response) => response.json()) // This callback consumes the response string and parses it as JSON into an object.
    .then((data) => data.html_url) // This callback takes the object and gets one property of it.
    .then((htmlUrl) => console.log(htmlUrl)); // This callback logs that property.
};
getProfile("https://api.github.com/users/SallyMcGrath");

So then returns a new Promise, and you can call then again on the new object. You can chain Promises in ever more complex dependent steps. This is called Promise chaining.

It’s important to understand some of what is happening with Promises and then. But for the most part, you will not be writing code in this style.

🪞 Re-rendering the UI

Learning Objectives

With state updated from user input, we can re-render:

const render = (films) => {
  // Clear existing DOM elements
  // Map films to DOM elements
};

function handleInput(event) {
  // capture search term
  const { searchTerm } = event.target;
  // Filter films on search term
  filteredFilms = films.filter((film) => film.title.includes(searchTerm));
  // Set new state
  state.films = filteredFilms;
  // Re-render UI with updated films
  render(state.films);
}

💡 Things to consider

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. Put this code in a file and run it with node:

const url = "https://api.github.com/users/SallyMcGrath"; // try your own username
const response = fetch(url);
console.log(response);
Your Promise should look like this:
Promise {
  Response {
    [Symbol(realm)]: null,
    [Symbol(state)]: {
      aborted: false,
      rangeRequested: false,
      timingAllowPassed: true,
      requestIncludesCredentials: true,
      type: 'default',
      status: 200,
      timingInfo: [Object],
      cacheState: '',
      statusText: 'OK',
      headersList: [HeadersList],
      urlList: [Array],
      body: [Object]
    },
    [Symbol(headers)]: HeadersList {
      cookies: null,
      [Symbol(headers map)]: [Map],
      [Symbol(headers map sorted)]: null
    }
  },
  [Symbol(async_id_symbol)]: 54,
  [Symbol(trigger_async_id_symbol)]: 30
}

The response variable in this code is not labelling the data. It’s labelling a Promise.

A promise is exactly what it sounds like: a promise to do something. You can use this promise object to sequence your code. You can say, “When the data comes back, then do this.”

You will explore Promises in more detail as you build more complex applications. For now, let’s move on to .then().