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