Day 2 Overview: The DOM

Day 2 Overview: The DOM

Course Project Overview: What will I be building?

App: Coffee Masters

  • Web app for a coffee shop

  • Menu of products

  • Details of each product

  • Order with Cart

  • Data in JSON

  • No JavaScript written

To start, I always thought that the DOM and the HTML file in my code editor that was served to the browser were the same thing. I mean, when I opened my console to inspect objects I saw pretty much the same HTML file I saw in my editor. However, I was wrong, since there is a difference between the DOM and the HTML file that gets loaded. The HTML file is the string representation of the DOM, but the DOM is always a structure in memory. The head and body can be implicit in an HTML file, which will then be added in the DOM, because it’s mandatory in the DOM. The browser parses our HTML code and creates the DOM from it, so they’re not the same.

The only thing mandatory in an HTML file is the <!DOCTYPE html> and <title> tags, and even without them, it still might work on some browsers apparently, lol.

<!DOCTYPE html>
<style>
    body { background: red;}
</style>
<title>My first HTML test</title>

<h1>Quick HTML code</h1>

When the browser begins parsing our HTML file, it begins creating the <body> tag in the DOM when it encounters my first visible element, something like an <h1> tag:

<!DOCTYPE html>
<title>Title</title>
<script></script>

<h1>Visible Text</h1>

Scoping querySelector

While we can use the DOM API anywhere and everywhere, we can also scope its usage to the actual HTML element I'm interested in. For example, I could look for my <span> element within my <nav> element using the DOM API:

document.querySelector('span');

-> <span id='badge' hidden></span>

But we can scope it to just the element since each HTML element also has access to the DOM API. Since I'm now using a variable, this allows me to query only within the nav, which allows me to “cache” the nav object and query it many times, which makes it more performant.

let nav = document.querySelector('nav');
nav.querySelector('span');

-> <span id='badge' hidden></span>

Adding Scripts async & defer

When the browser is parsing the HTML line by line, and it encounters a <script> tag, it will stop parsing the rest of the HTML file, download the js file in the script's src, and execute the file. When it’s done, it will continue with the rest. This can lead to slow page loads if the JS file being executed is big enough. The now deprecated solution to this, which I learned back at Hack Reactor, was to simply put the <script> tag at the bottom of the <body> tag so that it would finish rendering the HTML first, and then the JS file. However, we now have some attributes that can be applied such as defer or async which defers the script download and execution.

If you don’t know which one to use, just use defer. It’s a boolean value so no need to give it any argument. It will do the following:

  • When the browser finds the script, it will download the JS file, but will not halt the parsing of the HTML, it will do it in parallel

  • When the browser finishes parsing the HTML, it will then execute the JS file. So it will defer the execution, not the downloading of the file.

  • When to use async: more suitable for small scripts that really need to be executed as soon as possible such as analytics, pings to server, etc. It will not halt parsing, but will download the file, and instead of waiting for the HTML to finish parsing, when the JS file is finished downloading it will halt HTML parsing and execute the file.

Main Script Setup

Most of the time what we want to do is work with the page. Since I'm now using defer and the JS file will be executed once the HTML is finished parsing, I can immediately do things like document.getElementById(), etc.

However, this isn't a good practice because some browsers in theory might still be working on the creation of the DOM in memory. This is why the window object has an event that we can listen to that specifies when the DOM is ready for manipulation: window.addEventListener('DOMContentLoaded');

// it's better to wait for the event for manipulation since it happens
// before rendering
window.addEventListener('DOMContentLoaded', () => {
    let nav = document.querySelector('nav');
    console.log(nav);
);
💡
There is also a load event, which waits for everything to be loaded, such as images, videos, assets, and everything. If we wait for this, we're missing the opportunity to manipulate the DOM earlier.

Event Binding & Handlers

Each DOM element has a list of possible events we can listen to:

  • Basic: load, click, dblclick

  • Value: change

  • Keyboard Events: keyup, keydown, keypress

  • Mouse Events: mouseover, mouseout, etc

  • Pointer and Touch Events

  • Scroll, Focus, and more APIs

Some specific objects have special events:

  • DOMContentLoaded, popstate in window
💡
The spec’s naming pattern is to use lowercase with no word separator for event names. That’s why we can end up with names like webkitcurrentplaybacktargetiswirelesschanged

Binding functions to events in DOM objects

  • onevent (more classic): one property for every event onload onclick, key here is that everything is lowercase, in React camelCase.

  • addEventListener

💡
When using oneventname technique, only one function can be attached per event/object combination. Since it’s a property (setter, getter) it will be overwritten.

When using addEventListener() we can attach more than one event handler, since it uses the Observer Design Pattern aka Publish/Subscribe, so that we can subscribe to multiple events and fire all of them.

function eventHandler(event) {
    //do something
}

element.onload = eventHandler;
element.onload = (event) => {
    //replaces the first eventHandle
}

element.addEventListener('click', eventHandler);
element.addEventListener('load', eventHandler);
element.addEventListener('load', (event) => {
    //do something else
});
💡
This allows us to separate our concerns, knowing that all of them will fire, no matter how many times we’ve called it.

Advanced Event Handling

function eventHandler(event){
    //do something
};

const options = {
    once: true,
    passive: true
};

element.addEventListener('load', eventHandler, options);
element.removeEventListener('load', eventHandler);

In the example of someone scrolling through a site, by default, the browser will wait for the event handler to see if we're changing the DOM when we set passive: true. In this example, I'm telling the browser to safely do what it’s doing and that I won't be changing the DOM; meaning I don’t have to wait for it to finish.

Removing event listeners is only helpful when doing a SPA and adding and removing elements on the page. We don’t need to remove the event listener for DOMContentLoaded because when a user is leaving or closing the browser, we don’t need to clean up JS, it will happen automatically. To be completely honest, I don't think I fully captured what the point of this was, but I don't anticipate having to implement it anytime soon-- and if I have to I'll be sure to ask questions of more senior engineers.

Dispatching Custom Events

I can also reuse the same system I've been using with adding event listeners with my own custom events and messages:

const event = new Event('mycustomevent');
element.dispatchEvent(event);
💡
We can also broadcast events throughout the DOM. This is kind of a Context, like in React. Everyone in the DOM can subscribe via a Provider.

Helpful Shorthand Methods

One of the biggest developer fears is that vanilla JS is too verbose, however, JS is a very flexible language. The following code is one example of how we can abstract away some of the verbose-ness:

const $ = () => document.querySelector.call(this, arguments);
const $$ = () => document.querySelectorAll.call(this, arguments);
HTMLElement.prototype.on = (a,b, c) => this.addEventListener(a, b, c);
HTMLElement.prototype.off = (a, b) => this.removeEventListener(a, b);
HTMLElement.prototype.$ = (s) => this.querySelector(s);
HTMLElement.prototype.$ = (s) => this.querSelectorAll(s);

Fetching Data

Creating Initial Services

💡
What’s a service? In JS: nothing. Just a concept, a design pattern that we are defining. Can have any name, nothing exists in the DOM called a “service”. It is an object or way to offer different services to the app, such as fetching data from a network and/or a Store that stores data that is global to the app aka the state of the app.
//service/API.js
const API = {
    url: '/data/menu.json',
    fetchMenu: async () => {
        const result = await fetch(API.url);
        return await result.json();
    }
}

//services/Store.js
import API from './API.js';

const Store = {
    menu: null,
    cart: []
}

//app.js
import Store from './services/Store.js';
import API from './services/API.js';

An ECMAScript Module means that we don’t want our variables to be global, so we can export our functions from our files to be used throughout our app.

💡
Before we can being importing our functions from our other modules, we can’t simply go and import in our app.js file, we need to go into our HTML <script> tag and add the type attribute and set it to "module" or else we get the following error:

💡
In the browser, without something like Babel, we need to include file extensions for our functions to work. IE: if I’m importing API from ./API I need to add the .js or else it won’t work.

Loading Menu Data

In JS, if we don’t use modules, then everything is global, which can lead to conflicts between several variable names.

Now that we’ve set our type to module, we can import our functions just like I’m used to in something like React.

However, for our purposes, Store needs to be a global store, but it’s a module, so how can we make this change? We can use one pattern which breaks the module pattern by using the window object:

//app.js
window.app = {};
app.store = Store;

Now, every time I access app.store I'm effectively "hooking" into a global object available everywhere in my app.

So let’s start fetching data: We can include our fetchMenu() function inside of our event listener:

window.addEventListener('DOMContentLoaded', async() => {
    const menu = await API.fetchMenu();
})

However, we can do better by also adding another service that only handles working with my data model because it will increase the readability of my code.

//services/API.js
const API = {
  url: '/data/menu.json',
  fetchMenu: async () => {
    const result = await fetch(API.url);
    return await result.json();
  },
};

export default API;
//services/Store.js
const Store = {
    menu: null,
    cart: []
};

export default Store;
//services/Menu.js
import API from './API.js';

export async function loadData() {
  app.store.menu = await API.fetchMenu();
}
//app.js
import API from './services/API.js';
import Store from './services/Store.js';
import { loadData } from './services/Menu.js';

window.app = {};
app.store = Store;

window.addEventListener('DOMContentLoaded', async () => {
  loadData();
});

We now have data!!! But it's in memory. The next part will be how to render our data to our screen and then react to changes in our data-- stay tuned!