🏠 HTML Elements

By Robert Laing

Class hierarchy of HTML Elements

As usual, I’m relying on testing to figure this out.

it("instanceof body examples", function() {
  const body = document.querySelector("body");
  expect(body instanceof EventTarget).toBe(true);
  expect(body instanceof Node).toBe(true);
  expect(body instanceof Element).toBe(true);
  expect(body instanceof HTMLElement).toBe(true);
});

The common ancestor of all webpage objects is EventTarget, which allows EventTarget.addEventListener(type, callback) as I’ve elaborated on in callbacks and pointer events.

Most webpage objects are Nodes. An exception is the global window object which is its own Window class.

it("instanceof window examples", function() {
  expect(window instanceof EventTarget).toBe(true);
  expect(window instanceof Node).toBe(false);
  expect(window instanceof Element).toBe(false);
  expect(window instanceof HTMLElement).toBe(false);
  expect(window instanceof Window).toBe(true);
});

Though document is also a member of its own Document and in turn HTMLDocument class, it has Node as an ancestor:

it("instanceof document examples", function() {
  expect(document instanceof EventTarget).toBe(true);
  expect(document instanceof Node).toBe(true);
  expect(document instanceof Element).toBe(false);
  expect(document instanceof HTMLElement).toBe(false);
  expect(document instanceof Document).toBe(true);
  expect(document instanceof HTMLDocument).toBe(true);
});

Using “img” as an example, it has it its own class HTMLImageElement which inherits from HTMLElement which inherits from Element which finally takes us back to Node as a common ancestor:

it("instanceof img examples", function() {
  const newImg = document.createElement("img");
  expect(newImg instanceof EventTarget).toBe(true);
  expect(newImg instanceof Node).toBe(true);
  expect(newImg instanceof Element).toBe(true);
  expect(newImg instanceof HTMLElement).toBe(true);
  expect(newImg instanceof HTMLImageElement).toBe(true);
});

Types of Elements

An HTML document is a tree of elements/nodes with leaves and branches.

A leaf is called a void element. Unlike branch elements which usually have children elements nested inside them, void elements such as img do not.

It’s common to code void elements with self-closing tags, but according to Mozilla:

Self-closing tags () do not exist in HTML… Self-closing tags are required in void elements in XML, XHTML, and SVG (e.g., <circle cx="50" cy="50" r="50" />).

While some browsers allow a convention of translating <div />Some text to <div>Some text</div>, to keep things simple, never use self-closing tags in HTML. Void elements are those whose content is not nested between <elementName>...</elementName>

Nodes vs Elements

An HTML Element is a subclass of Node and confusingly to me has alternative methods which are nearly synonymous to the ones it inherits.

Sibling subclasses sharing Node as their parent include document and window. Thanks to this inheritance hierarchy, there’s the puzzling choice of things like Element.querySelector(selector) vs Document.querySelector(selector).

Elements vs HTMLElements

Generally we’re dealing with HTMLElement which add further to those inherited from Element properties and methods.

Importing Elements

You can safely ignore the dozens of getElementBy… and generalize this to two operators provided you understand basic CSS selectors:

  1. querySelector(selector) for a single element. I invariably use ID (prefixed with a hash symbol in CSS selector notation, ie “#IDname”) of which each is supposed to be unique in an HTML document. Even though ID is what I nearly always use, I still prefer querySelector to getElementById(“id”) since I prefer to remember a general function to lots of superfluous crap.
  2. querySelectorAll(selectors) for an array of elements. Typically this would be used to group elements sharing a class (prefixed with a dot in CSS selector notation), but thanks to slightly more advanced CSS selector knowledge, this can be done for IDs provided a good naming convention is adopted.

Converting a NodeList to an array

One gotcha with querySelectorAll is it returns some historical relic called a NodeList which needs to be converted into an array literal by either wrapping it in Array.from() or using spread syntax.

Array.from(document.querySelectorAll(`img[id*="_"]`))
.forEach(function (child) {
  child.remove();
});

[...document.querySelectorAll(`img[id*="_"]`)]
.forEach(function (child) {
  child.remove();
});

Avoid parents and children

Though I typically want to use the container of an image rather than the image itself as my drag and drop target so as to get a larger tap target, I got tripped up by using parentElement because if there’s a typo in the html file — or somebody else (eg a future you) edits the html file and changes the element hierarchy — your JavaScript code breaks.

I typically have an image and some text in a span element inside each grid cell of a game, and after initially creating an elaborate function to test if a player had tapped on the div or img or span, I found if I simply appended Img or Text to cellName, I could simply get the target ID like so:

/** @function */
function getId(element) {
  return element.id.replace("Img","").replace("Text","");
}

A common thing I have to do is clear all the card images in cells containing a group of cards, and here tried to avoid using children, but ultimately found that easier than using substring matching selectors with querySelectorAll.

The pattern I settled on looks like this:

[...DOM[col].children]
.filter((el) => el.localName === "img")
.forEach(function (child) {
  child.remove();
});

Matching ID substrings

Several elements can be returned from their IDs by querySelectorAll by using a CSS attribute selector which allow pattern matching.

This notation follows the convention of grep and sed of using ^ for matches starting with a given substring, eg:

img[id^="skyscraper"]

and $ means ending with a given substring:

a[href$=".org"]

and * means the substring appears anywhere

img[id*="hand"]

MDN has more examples.

Elements of Geometry

Though there’s Element.clientLeft, HTMLElement.offsetLeft… I’ve found these dangerous to use.

When an element has display: inline, clientLeft returns 0 regardless of the element’s border. — MDN

I’ve found it hard to predict if an element will return zero or not, so found getBoundingClientRect much safer. It returns an object-literal like this:

{
  "x":724.2000122070312,
  "y":13.199996948242188,
  "width":137,
  "height":377.20001220703125,
  "top":13.199996948242188,
  "right":861.2000122070312,
  "bottom":390.40000915527344,
  "left":724.2000122070312
}

Something that tripped me up was using getBoundingClientRect on an empty div or other container element gives zero width and height unless width has been set in the stylesheet. Setting max-width doesn’t fix this, but min-width does.

Where are mainly need this info is to position numbers giving the size of decks etc over card images.

function cardText(id, idTxt, val) {
  DOM[idTxt].innerText = val;
  const rectC = DOM[id].getBoundingClientRect();
  const rectT = DOM[idTxt].getBoundingClientRect();
  DOM[idTxt].style.left = `${rectC.left + 0.5 * (rectC.width - rectT.width)}px`;
  DOM[idTxt].style.top = `${rectC.top}px`;
}

Top Down Column

function createImg(col, src, idx) {
  const newImg = document.createElement("img");
  newImg.style.position = "absolute";
  newImg.src = `./cards/${src}`;
  newImg.id = `${col}_${idx}`;
  newImg.width = DOM.hand1Img.width;
  newImg.height = DOM.hand1Img.height;
  newImg.style["z-index"] = idx;
  newImg.style.left = `${DOM[col].offsetLeft + 4}px`;
  newImg.style.top = `${DOM[col].offsetTop + (0.2 * idx * newImg.height)}px`;
  DOM[col].append(newImg);
}

Bottom Up Column

function createImg(col, src, idx) {
  const newImg = document.createElement("img");
  newImg.style.position = "absolute";
  newImg.src = `./cards/${src}`;
  newImg.id = `${col}_${idx}`;
  newImg.width = DOM.hand1Img.width;
  newImg.height = DOM.hand1Img.height;
  newImg.style["z-index"] = idx;
  newImg.style.left = `${DOM[col].offsetLeft + 4}px`;
  newImg.style.top = `${DOM[col].offsetTop + (0.2 * idx * newImg.height)}px`;
  DOM[col].append(newImg);
}