2

Goal: Apply action to each checked element if checked, vs just the one element.

I am setting up multiple buttons (custom components) that when clicked, the border changes for the

tag within . When clicked again, the border resets.

I have 'checked' properly displaying when I click on the flash-card-check-mark (elm), however my code is only selecting the first item and ignoring the rest.

Can you point me in the right direction as to how I would go about making sure that -- any -- of the buttons being clicked will have this action applied?

(new to javascript and appreciate your insight)

Note: This is for code example (doesn't run without the component checked but shows the markup I'm using -- I would share a link but this is running locally)

document.querySelector('flash-card-check-mark').onclick = function() {

  var elem = document.querySelector('flash-card-check-mark');
  var elemContent = document.querySelector('flash-card-check-mark p');


  if (elem.getAttribute("checked") === null) {
    elemContent.style.border = "1px solid #0000";
  } else {
    elemContent.style.border = "1px solid magenta";
  }

};
<flash-card-check-mark no-default-padding="true">
  <p align="left" size="small" class="pa-2">
    <span slot="heading">Start my marketing today</span>
  </p>
</flash-card-check-mark>
<flash-card-check-mark no-default-padding="true">
  <p align="left" size="small" class="pa-2">
    <span slot="heading">Create automated customer journeys</span>
  </p>
</flash-card-check-mark>
Birdie Golden
  • 505
  • 6
  • 20
  • So `flash-card-check-mark` is a properly registered custom element that makes use of shadow DOM and slots? – connexo Apr 10 '21 at 10:25

4 Answers4

1

You have two options:

  • Apply the click handler to all elements with querySelectorAll
  • Use Event Delegation to apply a handler to the document, and then only pick clicks that were on "interesting elements".

Apply to all elements

Use querySelectorAll:

for(const elem of document.querySelectorAll('flash-card-check-mark')) {
  // better to use `element.addEventListener('click', (e) => 
  element.onclick = function(e) {

  // the element is available in the listener
  var elem = e.target; 
  var elemContent = elem.querySelector('p'); // you can querySelector an element


  if (elem.getAttribute("checked") === null) {
    elemContent.style.border = "1px solid #0000";
  } else {
    elemContent.style.border = "1px solid magenta";
  }

};

Use Event Delegation

You add a listener to the document instead and then filter the element out using e.target:

document.addEventListener('click', e => { // can also onclick
  // filter only elements you care about
  if (!e.target.matches('flash-card-check-mark')) return;  
  var elem = e.target;
  var elemContent = e.querySelector('p'); // query selector nests


  if (elem.getAttribute("checked") === null) {
    elemContent.style.border = "1px solid #0000";
  } else {
    elemContent.style.border = "1px solid magenta";
  }

};

Notes

You can improve the code style itself with the following advice:

  • Use let/const instead of var statements as they have much less confusing scoping rules.
  • Use arrow functions (() => {}) since they are shorter and have a more obvious this value.
  • Use addEventListener('click', to add event listeners instead of setting handlers with onclick. It has less confusing scoping rules and it means you don't have conflicts with other people setting handlers on the element.
  • Use properties instead of attributes. So elem.checked and not elem.getAttribute('checked') === 'checked').
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
0

If you want to select multiple element, Use querySelectorAll. Query Selector all Returns a nodeList. On that node list you can use forEach Loop

const flashCard = document.querySelectorAll("flash-card-check-mark");
const elemContent = document.querySelector("p");
function myFunction() {
  if (elemContent.getAttribute("checked") === null) {
    elemContent.style.border = "1px solid #0000";
  } else {
    elemContent.style.border = "1px solid magenta";
  }
}

flashCard.forEach(flashcard => flashcard.addEventListener("click", myFunction));
0

Maybe something like this (I've changed the colors):

[...document.querySelectorAll('flash-card-check-mark')].forEach(el => {
    el.onclick = () => {
    var elemContent = document.querySelector('flash-card-check-mark p');
    elemContent.style.border = el.getAttribute('checked') === null ? '1px solid red' : '1px solid green';
    };
});
AbsoluteBeginner
  • 2,160
  • 3
  • 11
  • 21
  • Thanks for the answer. The result is a bit odd as the borders work correctly, but the change is only applied to the first element in the list. If I click the 2nd element, the first's border changes. The 'checked' option is applied to the elm, vs elmContent (or rather, each ). – Birdie Golden Apr 09 '21 at 19:06
0

With var elemContent = document.querySelector('flash-card-check-mark p');

you always select the first matching element in the page

  • The click is behaviour of your Custom Element

  • Then the Custom Element should add and handle the click

  • If you want to select elements inside a Custom Element,
    use this.querySelect... not document.querySelect...

  • Since you use shadowDOM <slot> also read ::slotted CSS selector for nested children in shadowDOM slot for

<flash-card-check-mark>
  <span slot="heading">Start my marketing today</span>
</flash-card-check-mark>
<flash-card-check-mark checked>
  <span slot="heading">Create automated customer journeys</span>
</flash-card-check-mark>

<script>
customElements.define("flash-card-check-mark", class extends HTMLElement {
  constructor(){
    let style = `<style>:host{display:block;cursor:pointer}</style>`;
    super()
      .attachShadow({mode:"open"})
      .innerHTML = style + `<slot name="heading"></slot>`;
    this.onclick = (evt) => this.clicked();
    this.clicked();
  }
  clicked(){
    this.toggleAttribute("checked", !this.hasAttribute("checked") );
    let color = "pink";
    if (this.hasAttribute("checked")) color = "lightgreen";
    this.style.backgroundColor = color;
  }
});
</script>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49