Notes from Max Firtman's course on Frontendmasters: Vanilla JS: you might not need a framework
.
DOM essentials
- DOM - Document Object Model. Memory representation of the HTML page. The
DOM API
- browser exposes it to manipulate the DOM. Changes the DOM trigger a new render of the HTML. - DOM API is available on many objects. Most important stuff:
-
window
global object. In the 90s window was a browser. No longer the case: tabs, iframes, etc. -
document
object. The current DOM. Before it was one page - one document. Today you can have several documents in one page. -
DOM API
. One object per HTML element (with its own part of the DOM API) and other nodes in your document. - Each HTML element is represented by an object of
HTMLElement
interface or other interfaces that inherit from it. They have instance properties and methods. Changes in properties or children will trigger updates in the user interface when you release the thread. We can listen to events that happen in that event and react in consequence. Release the thread === your function ends and there is no more code to execute.
- To work with DOM elements, we can pick them from the DOM or I can create them with JS on the fly. Read content, change content, remove reference, etc. After the changes, when the thread is released, it will trigger an update of the DOM.
- You can select elements by ID (
getElementById
[older, from the 90s] orquerySelector
[newer - will get the 1st element matching a CSS selector]. They will returnnull
if no element is found), classname, name, css selector, navigating DOM structure (because it is a tree). And when selecting, you can get oneHTMLElement, a **live** HTML Element Collection (if a new element appears on the fly, it will be there.
HTMLCollection:
getElementsByTagName,
getElementsByClassNameor
getElementsByName) or a static element collection (
NodeList-
querySelectorAll()`). The ones that return multiple elements, you get an empty array if no elements were found. -
HTMLCollections
(live) don't have the modern array methods.querySelectorAll
(static) only has theforEach
. But you can get those methods usingArray.from(collection)
. - To change attributes of a DOM element we use the dot syntax:
element.hidden = false
,element.src = "whatever"
, etc. Except forclass
(should beclassList
) andfor
(should behtmlFor
), these two are reserved keywords in JS. -
element.style.color
to change the styles. The only difference is that the values will be camel case instead of kebab case:fontSize
instead offont-size
. -
element.addEventListener(event, callback)
. - You can change the
textContent
(no HTML code supported), theinnerHTML
(HTML code supported, even multiline, like so:<h1>My app</h1>
) or create new nodes using DOM APIs (const h1 = createElement("h1")
,element.appendChild(h1)
).
Project
- HTML and the DOM are not the same thing. The DOM is the HTML representation in memory.
head
andbody
are not required for HTML but they are implicit in the DOM. So you can write html without body and head tags and it will render in the browser, but if you inspect in the dev tools you will see a head and a body. So view source and inspect tab in the browser dev tools can be different (browser or extensions may inject things in the DOM). - The DOM API is not only available on the
document
object, it is also available in the HTMLElement API. Actually one of the common mistakes in vanilla js that lead to lower performance is always querying the document instead of html elements. This allows you to query only thenav
:
const nav = document.querySelector("nav")
nav.querySelector("span#badge")
script
tag should never go at the bottom of the html file. The idea that you want to have the html parsed before loading the js is an old one. If you put the script tag in the head the browser will stop parsing the html file until the js is loaded. But, we have nowasync
anddefer
attributes that can be added to thescript
tag.defer
downloads the file but continues parsing the html and the js parsing is "deferred" until the html is parsed.async
is more suitable for small scripts that need to be executed asap, for instance analytics. async will download the file and when the file is ready it will halt the html parsing to execute the script.Even though the html may have been parsed already, but the dom may not yet be ready for the js. That is why it is not recommended to start the js with something like
document.querywhatever
.window
has an event that we can listen to that happens after the DOM is created:DOMContentLoaded
.load
event: waits for everything to be loaded: video, css, images, etc. You are missing the opportunity to manipulate the DOM earlier. DOMContentLoaded happens before rendering, but it means that DOM is ready for manipulation.Binding functions to events in DOM objects:
onevent
properties andaddEventListener
.onevent
there is only place for one property, so only the last one will be fired.
function eventHandler () {
}
element.onload = eventHandler
element.onload = (event) => {
// it replaces the first handler
}
- addEventListener is using the observer design patter, so many can be subscribed to it. This is recommended today.
`
function eventHandler () {
}
element.addEventListener("load", eventHandler)
element.addEventListener("load", (event) => {
//
})
`
-
addEventListener
supports a 3rd argument with options:
function eventHandler () {
}
const options = {
once: true, // will fire the event once and it won't fire again
passive: true // https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#passive
}
element.addEventListener('load', eventHandler, options)
removeEventListeners
for when we add or remove elements and we don't want things hanging around when they are not needed anymore.Dispatch your own events and messages and anyone can listen to these events:
`
const event = new Event('mycustomname')
element.dispatchEvent(event)
`
Imports have to be the full path:
import bla from './blabla.js
with the extension, otherwise it won't work. Also addtype="modules"
in thescript
tag so you can import across files, otherwise it is all part of the global scope and importing doesn't work.For state, one pattern is to create a global variable through the window object:
`
import Store from './services/Store'
window.app = {}
app.store = Store
`
Client router with vanilla JS
- Two ways to do it:
- Remove previous page and inject new page into the DOM. Page is just HTML elements, there is no "page" concept in the DOM. i.e. we remove section1 and add section2 when moving between "pages"
- Hide previous page and show current page. All the "pages" are always in the DOM, but some have a
hidden
attribute. When changing we display or hide elements. i.e.<section id="section1">
and<section id="section2" hidden>
. It may not be scalable for small applications. - Single HTML but you use the DOM API and Web Components. And we use the History API to push new entries to the navigation history.
- History API:
history.pushState
andaddEventListener('popstate', cb)
-
popstate
won't be fired if the user clicks on an external link or changes the URL manually. Only works when working with the history API of our own app.
Web components
- Custom HTML tag. Set of standards. Custom elements, HTML Templates and Shadow DOM.
-
Custom elements
has to have a dash (-
) to be w3c future proof. A way to define reusable HTML elements with custom behaviour and functionality using JS. Lifecycle:connectedCallback()
,disconnectedCallback()
,adoptedCallback()
,attributeChangedCallback(name, oldvalue, new Value()
.Slot
is likechildren
in React. -
HTML Template
. It's the<template>
tag. It's in the DOM but it's ignored by the browser. We clone the template and we append it as a child, typically in theconnectedCallback
method of the Custom Element. The real template cannot be put in the DOM. -
shadow DOM
: private, isolated DOM tree within a web components that is separate from the main document's DOM tree. CSS declared in the main DOM won't be applied to the shadow DOM. Can be open and close.
Reactive programming
-
Proxy
.