how to listen to any of multiple elements?

I’ve been reading through the MDN documentation to try to figure out how to effect a change in image when any one of several buttons are clicked.

Document.querySelectorAll()
-and-
Document.getElementsByClassName()
both look like they should work, but I haven’t succeeded with either :frowning:

I am probably making a simple error somewhere. I can listen for one element at a time with Document.getElementById(), but it seems silly to write the same function individually for each element

Does anyone have a minute to look? :pray: Here’s my notebook:

Off-topic, but you should avoid querying and mutating the global document in Observable because it won’t play nicely with data flow.

To avoid the querying the global document, be specific about where you want to find your element. Change this:

document.getElementById("link1")

To this:

page_navigation.querySelector("#link1")

To avoid the mutation, such as assigning the onclick listener in one cell to a DOM element defined in another cell, combine the cells.

page_navigation = {
  const page_navigation = html`<nav>…</nav>`;
  page_navigation.querySelector("#link").onclick = …;
  return page_navigation;
}
1 Like

A more on-topic answer: why not define the section you want to show as a view? I.e., instead of

page_navigation = html`<nav>…</nav>`;

You’d say

viewof section = radio(["Section 1", "Section 2", "section 3"])

using one of Jeremy’s radio inputs?

Then, if you want to pick a random image whenever the section changes, you simply need to reference section to trigger re-evaluation. For example:

section, html`<img src=${image_list[Math.floor(Math.random() * image_list.length)]}>`

If you want to style it to look like tabs instead of radio buttons, then you could define a custom view that behaves the same way.

2 Likes

Thanks @mbostock for your time and guidance!
Both your off- and on-topic responses are helpful and informative, thank you!

In this particular case, I am trying to learn how to write this for “vanilla” JavaScript, and using Observable to help me better see and understand how the functions work. For this reason, I have been trying to shy away from using JS to create the DOM element, and instead using JS to add additional functionality on top of the tachyons.css page frame written in HTML.

I remain perplexed by this particular problem, as everything looks to me like it should be working.

After setting up the navigation component and giving each element a unique id and common class names, I can see that both .querySelectorAll() and .getElementsByClassName have correctly returned matches:

page_navigation.getElementsByClassName("link").length // returns 3 - the number of nav elements I've defined with this class name
page_navigation.querySelectorAll("#link1, #link2, #link3").length // returns 3

So selection is working, and I get a NodeList that acts like an Array (and can be converted into it). The function I am trying to execute for an .onclick event is effective where I specify a single value using .getElementById. but not for the multiple values returned with .querySelector().

That is, this will work:

attempt_using_id = page_navigation.querySelector("#link1").onclick = function(event) {
  let random_image = image_list[Math.floor(Math.random()*Object.keys(image_list).length)]
  image_view.querySelector("#image").src = random_image
  }

But nearly the same thing with another element selection will not work.

Neither

page_navigation.getElementsByClassName("link").onclick = function(event) {
  let random_image = image_list[Math.floor(Math.random()*Object.keys(image_list).length)]
  document.getElementById('image').src = random_image
  }

nor

page_navigation.querySelectorAll("#link1, #link2, #link3").onclick = function(event) {
  let random_image = image_list[Math.floor(Math.random()*Object.keys(image_list).length)]
  document.getElementById('image').src = random_image
  }

These different outcomes for what appear to be similar constructs is hard to understand, and regrettably I’m not quite grasping it through trial-and-error + reading. :frowning: Fortunately, there’s an easy workaroud (just repeating the working function for as many elements I wish), but this isn’t a satisfying solution. I suspect that, once I obtain the `NodeList`, there’s something more I must do. I am currently reading about forEach and also trying to write for loops, but haven’t quite gotten it. :confused:

Thanks again for your time, help and guidance! I definitely appreciate learning from this that I can specify a cell’s name to focus where I am looking for elements (and thereby creating an Observable dependency). I also appreciate your suggestions for how to accomplish the same effect in a different, more Observable way using views. I hope it’s OK that I am asking more basic JS-related questions. Very early on, Fabian indicated that I should embrace your invitation to discuss:

…common questions, examples of techniques, and general discussion about data science, visualization, programming, and more. [ref]

I got it! :tada:

When I first put this together, I used just <img > to create an image element. While this threw me off initially, it also led to the answer (also something Fabian was showing me in another thread): the <img> element wasn’t working with id as I was expecting. Wrapping the <img> in <div> allowed me to access it with .querySelectorAll().

It took a while, but I also figured out how to loop through each <div> and assign an .onclick event to it:

page_navigation.querySelectorAll("#link1, #link2, #link3").forEach(div => 
    div.onclick = (e) => {
let random_image = image_list[Math.floor(Math.random()*Object.keys(image_list).length)]
  image_view.querySelector("#image").src = random_image
    })

Now it works! :slight_smile: The function returns ‘undefined’, but the behaviors are in effect!

Thanks for the time and encouragement!

2 Likes