🏠 HTML Elements

By Robert Laing

Making a website dynamic boils down to manipulating HTML Elements.

As per tradition with JavaScript, this has turned into a cluttered mess of nearly synonymous things, so 80% of “DHTML” needs to be thrown away to dig up the good stuff.

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>

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.

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

To keep things manageable, I try to use Element properties and methods first before searching its relatives for a solution.

Elements of Geometry

Once imported into our JavaScript program, we can proceed to alter an element’s HTML and CSS attributes. Especially in card games, I typically want to change its co-ordinates as a card gets moved with a card for finger, and then give it a new position once it has been placed. What I’ve found a bit of a challenge is, as is common in solitaire card games, to create columns or rows of overlapping card images.

By calling getBoundingClientRect on an element, we get 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
}

Empty containers have no size

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.

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