Creating a simple toggle in html / css / js

I am having an issue getting this working:
The .active state doesn’t appear to be reassigning on click.

Any ideas?
Heads up: new to Observable & JS, so be gentle.

There are a couple of things to note.

First, in your notebook, you have:

mainTabs = { 
const element = document.getElementById('mainTabs'); // looking for 'mainTabs'
  return element;

element.addEventListener('click', onTabClick, false);
}

Nothing after the line return element; gets run, so the event listener is never added. If you move the return element; line to the end of the block and re-run the cell, then the tabs work.

However, the tabs only work when that cell is re-run and won’t work the first time the notebook is loaded. This is because document.getElementById('mainTabs') return null the first time it is run. Cells in Observable are not run from top to bottom. Instead, they are run in topological order. So if cell A depends on cell B, then cell B is run before cell A. Right now in your notebook, there is nothing explicitly saying that the cell that adds the event listener depends on the cell that creates the HTML tabs.

To fix this, we can make the dependency explicit. We can give the cell that adds the HTML a name and then reference that cell instead of document when querying the DOM:

tabs = html`<div>rest of the cell...`

Then in onTabClick, instead of document.querySelectorAll('.active'), we can do tabs.querySelectorAll('.active').

Finally, in the cell that adds the event listener, we can do const element = tabs.querySelector('#mainTabs') instead of document.getElementById('mainTabs').

Now Observable knows that the cell that adds the event listener depends on the cell that adds the HTML tabs and therefore must be run after it.

Here’s a notebook that shows the changes.

1 Like

That’s amazing, thank you.

Question:
Why do you specify tabs = html?

Again, very new to this and hacking away

In Observable, each cell has a value. You can give a cell a name and then use that name to reference the cell’s value.

For example, if you have a cell

x = 5

and then another cell

y = x * 2

The value of y will be 10.

If a cell returns HTML, then its value is a DOM element. Let’s say you have this cell:

myDiv = html`<div id="myId">Hello!</div>`

Now we can use myDiv to reference this DOM element from other cells. Rather than using document.getElementById('myId'), we can just use myDiv. We can see that they are the same:

myDiv === document.getElementById('myId') // evaluates to true

Here are some examples of referencing myDiv from other cells:

myDiv.id // evaluates to "myId"
myDiv.innerHTML // evaluates to "Hello!"
myDiv.outerHTML // evaluates to `<div id="myId">Hello!</div>`

I’ve updated this notebook with these snippets.

Referencing the HTML cell by name saves us from having to query the entire document to get it and makes the dependencies explicit. See the “Re-selecting elements” section of the Observable anti-patterns and code smells notebook for more info.

1 Like

Thanks for this lovely explanation, Dan!

1 Like

I’d like to open this up again because, perhaps, the Tom MacWright notebook (Observable anti-patterns and code smalls) referenced above needs an update for the ‘Next’ UI? Some of us will want to be manipulating the DOM in HTML cells and these cannot be given names (at least I can’t figure out how to give them names). Couldn’t HTML (and/or MarkDown) cells be given evaluation precedence? optionally? so that DOM references can be made reliable in a more intuitive manner?