Windows 10, Chrome 96.0.4664.45 (Official Build) (64-bit)
The behavior does not occur on Firefox 94.0.1 (64-bit)
I'm seeing unexpected behavior when using Element.getElementsByClassName. I've included the full text of the HTML file that reproduces the behavior. Here's the output in console:
this.node.getElementsByClassName("divIndicator").length = 0
this.node.getElementsByClassName("indicators")[0].getElementsByClassName("divIndicator").length = 3
this.node.tBodies[0].getElementsByClassName("divIndicator").length = 3
this.node.parentNode.getElementsByClassName("divIndicator").length = 3
I've created a JS Object that maintains a reference to the root DOM node of a widget. When invoking "getElementsByClassName" from the root DOM node, the browser cannot retrieve the nodes with className "divIndicator".
However, when invoking "getElementsByClassName" from its child nodes (or its parentNode), the function correctly returns the 3 DIV elements with the className "divIndicator".
Do you see the error I made?
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
window.onload = main
function main() {
var nav = new Navigation()
document.body.appendChild(nav.node)
nav.selectNavItem(0)
}
//Navigation is more like a TabbedPane or TabbedContainer
class Navigation {
constructor() {
this.node = this.initDom()
this.node.model = this
this.contentContainers = []
}
initDom() {
//Create DOM tree
this.node = this.template()
this.initNavItems()
return this.node
}
initNavItems() {
//Turn off indicators
var indicators = this.node.getElementsByClassName("divIndicator")
for (var indicator of indicators) {
indicator.style.display = "none"
}
indicators[0].style.display = "initial"
//Attach listeners for Nav Items
var navItems = this.node.getElementsByClassName("labels")[0].getElementsByTagName("td")
for (var i=0; i < navItems.length; i++) {
navItems[i].addEventListener("click", function(self, idx) {
return function(evt) {
self.selectNavItem(idx)
}
} (this, i))
}
}
selectNavItem(idx, evt) {
//Set state of Navigation and ContentContainer (this follows the TabPane pattern)
var indicators = this.node.getElementsByClassName("divIndicator")
console.log('this.node.getElementsByClassName("divIndicator").length = ' + indicators.length)
indicators = this.node.getElementsByClassName("indicators")[0].getElementsByClassName("divIndicator")
console.log('this.node.getElementsByClassName("indicators")[0].getElementsByClassName("divIndicator").length = ' + indicators.length)
indicators = this.node.tBodies[0].getElementsByClassName("divIndicator")
console.log('this.node.tBodies[0].getElementsByClassName("divIndicator").length = ' + indicators.length)
indicators = this.node.parentNode.getElementsByClassName("divIndicator")
console.log('this.node.parentNode.getElementsByClassName("divIndicator").length = ' + indicators.length)
if (indicators.length == 0) {
console.error("Using alternative selector to retrieve divIndicator's")
indicators = this.node.tBodies[0].getElementsByClassName("divIndicator")
}
for (var i=0; i < indicators.length; i++) {
if (i == idx) {
//Show the indicator down arrow
indicators[idx].style.display = "block"
//Show the ContentContainer
if (this.contentContainers[idx])
this.contentContainers[idx].node.style.display = "block"
} else {
//Hide the indicator down arrow
indicators[i].style.display = "none"
//Hide the ContentContainer
if (this.contentContainers[i])
this.contentContainers[i].node.style.display = "none"
}
}
}
addContentContainer(cc) {
this.contentContainers.push(cc)
}
template() {
return new DOMParser().parseFromString(`
<table id="tblNavigation" class="navigation font4" width="540" height="100">
<tr class="labels">
<td>Meal</td>
<td>Food</td>
<td>Recipe</td>
</tr>
<tr>
<td colspan="3">
<div style="height: 2px; background-color: white;">
</td>
</tr>
<tr class="indicators">
<td><div class="divIndicator"><span>ᐯ</span></div></td>
<td><div class="divIndicator"><span>ᐯ</span></div></td>
<td><div class="divIndicator"><span>ᐯ</span></div></td>
</tr>
</table>
`, "text/html").body.firstChild
}
}
</script>
</head>
<body>
</body>
</html>
Update 1: Using Randy Casburn's answer, I continued trying to localize the issue. Chrome Debugger shows the deviant behavior directly after the call to document.body.appendChild(), but only if initNavItems() is called and has at least one statement in the method body. I replaced the original initDom() with a small change, and it appears to resolve the issue. However, I have no explanation why.
Replace the original initDom() method:
initDom() {
//Create DOM tree
this.node = this.template()
this.initNavItems()
return this.node
}
With this:
initDom() {
//Create DOM tree
this.node = this.template()
var self = this
setTimeout(function() { self.initNavItems() }, 0)
return this.node
}
Update 2: I've decided to abandon using DOMParser to create DOM elements from a string. There are at least 3 other methods. See this SO thread for a longer discussion of alternatives. I've chosen to use <template>, and here is the modification to my template() function
template() {
var template = document.createElement("template")
template.innerHTML = `
<div class="divContentContainer" style="display: none;">
...Other HTML ...
</div>
`
return template.content.firstElementChild
}