Nested web-components and event handling

web components example
google web components
shadow dom
web components tutorial
web components state management
web component event handler
dispatchevent
why use shadow dom

I'm writing a memory game in javascript. I have made a web-component for the cards, <memory-card> and a web-component to contain the cards and handle the game state <memory-game>. The <memory-card> class contains its image path for when its turned over, the default image to display as the back of the card, its turned state and an onclick function to handle switching between the states and the images.

The <memory-game> class has a setter that receives an array of images to generate <memory-cards> from. What would be the best method to handle updating the game state in the <memory-game> class? Should I attach an additional event listener to the <memory-card> elements there or is there a better way to solve it? I would like the <memory-card> elements to only handle their own functionality as they do now, ie changing images depending on state when clicked.

memory-game.js

class memoryGame extends HTMLElement {
  constructor () {
    super()
    this.root = this.attachShadow({ mode: 'open' })
    this.cards = []
    this.turnedCards = 0
  }

  flipCard () {
    if (this.turnedCards < 2) {
      this.turnedCards++
    } else {
      this.turnedCards = 0
      this.cards.forEach(card => {
        card.flipCard(true)
      })
    }
  }

  set images (paths) {
    paths.forEach(path => {
      const card = document.createElement('memory-card')
      card.image = path
      this.cards.push(card)
    })
  }

  connectedCallback () {
    this.cards.forEach(card => {
      this.root.append(card)
    })
  }
}

customElements.define('memory-game', memoryGame)

memory-card.js

class memoryCard extends HTMLElement {
  constructor () {
    super()
    this.root = this.attachShadow({ mode: 'open' })
    // set default states
    this.turned = false
    this.path = 'image/0.png'
    this.root.innerHTML = `<img src="${this.path}"/>`
    this.img = this.root.querySelector('img')
  }

  set image (path) {
    this.path = path
  }

  flipCard (turnToBack = false) {
    if (this.turned || turnToBack) {
      this.turned = false
      this.img.setAttribute('src', 'image/0.png')
    } else {
      this.turned = true
      this.img.setAttribute('src', this.path)
    }    
  }

  connectedCallback () {
    this.addEventListener('click', this.flipCard())
  }
}

customElements.define('memory-card', memoryCard)

implementing the custom event after Supersharp's answer

memory-card.js (extract)

connectedCallback () {
    this.addEventListener('click', (e) => {
      this.flipCard()
      const event = new CustomEvent('flippedCard')
      this.dispatchEvent(event)
    })
  }

memory-game.js (extract)

  set images (paths) {
    paths.forEach(path => {
      const card = document.createElement('memory-card')
      card.addEventListener('flippedCard', this.flipCard.bind(this))
      card.image = path
      this.cards.push(card)
    })
  }

Handle Events in Lightning Web Components Unit, Just as you nest HTML elements inside each other, Lightning web components—​which are custom HTML elements—can be nested inside other Lightning web  Communicate between Nested Lightning Web Components. This is one of most simplest and common use case, where parent LWC wants to react on event produced by child LWC. In above image, Model 3 is child (nested component) of Tesla. In above, Model 3 component, custom event modelclick is raised using class CustomEvent.

It would be very helpful to see some of your existing code to know what you have tried. But without it you ca do what @Supersharp has proposed, or you can have the <memory-game> class handle all events.

If you go this way then your code for <memory-card> would listen for click events on the entire field. It would check to see if you clicked on a card that is still face down and, if so, tell the card to flip. (Either through setting a property or an attribute, or through calling a function on the <memory-card> element.)

All of the rest of the logic would exist in the <memory-game> class to determine if the two selected cards are the same and assign points, etc.

If you want the cards to handle the click event then you would have that code generate a new CustomEvent to indicate that the card had flipped. Probably including the coordinates of the card within the grid and the type of card that is being flipped.

The <memory-game> class would then listen for the flipped event and act upon that information.

However you do this isn't really a problem. It is just how you want to code it and how tied together you want the code. If you never plan to use this code in any other games, then it does not matter as much.

Shadow DOM and events, Outer target: USER-CARD – document event handler gets shadow host as Please note that in case of nested components, one shadow DOM  MindSphere Web Components support standard CustomEvents. For general information refer to this link. MindSphere Web Components data is provided in the detail property of events. Each MindSphere Web Component has a unique data model which serves as interface description. For example the Asset View provides the selectedAssetChanged event.

Supersharps answer is not 100% correct.

click events bubble up the DOM, but CustomEvents (inside shadowDOM) do not

Why firing a defined event with dispatchEvent doesn't obey the bubbling behavior of events?

So you have to add the bubbles:true yourself:

[yoursender].dispatchEvent(new CustomEvent([youreventName], {
                                                  bubbles: true,
                                                  detail: [yourdata]
                                                }));

more: https://javascript.info/dispatch-events

note: detail can be a function: How to communicate between Web Components (native UI)?

For an Eventbased programming challenge
   this.cards.forEach(card => {
    card.flipCard(true)
  })

First of all that this.cards is not required, as all cards are available in [...this.children]

!! Remember, in JavaScript Objects are passed by reference, so your this.cards is pointing to the exact same DOM children

You have a dependency here, the Game needs to know about the .flipCard method in Card.

► Make your Memory Game send ONE Event which is received by EVERY card

hint: every card needs to 'listen' at Game DOM level to receive a bubbling Event

in my code that whole loop is:

game.emit('allCards','close');

Cards are responsible to listen for the correct EventListener (attached to card.parentNode)

That way it does not matter how many (or What ever) cards there are in your game

The DOM is your data-structure

If your Game no longer cares about how many or what DOM children it has, and it doesn't do any bookkeeping of elements it already has, shuffling becomes a piece of cake:

  shuffle() {
    console.log('► Shuffle DOM children');
    let game = this,
        cards = [...game.children],//create Array from a NodeList
        idx = cards.length;
    while (idx--) game.insertBefore(rand(cards), rand(cards));//swap 2 random DOM elements
  }

My global rand function, producing a random value from an Array OR a number

  rand = x => Array.isArray(x) ? x[rand(x.length)] : 0 | x * Math.random(),
Extra challenge

If you get your Event based programming right, then creating a Memory Game with three matching cards is another piece of cake

.. or 4 ... or N matching cards

Shadow DOM v1: Self-Contained Web Components, Handling focus. If you recall from shadow DOM's event model, events that are fired inside shadow DOM are adjusted to look like they come from  Event bubbling is a term you might have come across on your JavaScript travels. It relates to the order in which event handlers are called when one element is nested inside a second element, and

Custom Element Best Practices | Web Fundamentals, Custom elements let you construct your own HTML tags. When a browser does not support custom elements, the nested Dispatch events in response to internal component activity. A custom element should handle this scenario by checking if any properties have already been set on its instance. An event object is passed as an argument (optional) to the handler which contains all the information related to the event (in our case, mousedown) on the window. Open the developer tools (Inspect Element) on this page and copy paste the following code in the console panel and hit enter.

A practical introduction to web components · Devbridge, See how to use web components to make apps more predictable and to both introduce a required attribute and a blur event listener for the input field. It creates an empty space, where nested content between component  Components of Event Handling . Event handling has three main components, Events : An event is a change in state of an object. Events Source : Event source is an object that generates an event. Listeners : A listener is an object that listens to the event. A listener gets notified when an event occurs.

Events, Lightning web components dispatch standard DOM events. Components can also of code you need to write. To handle an event, define a method in the component's JavaScript class. example-my-component is nested in example-​app . When an event happens – the most nested element where it happens gets labeled as the “target element” ( event.target ). Then the event moves down from the document root to event.target, calling handlers assigned with addEventListener (, true) on the way ( true is a shorthand for {capture: true} ).

Comments
  • Yes and no. ....
  • This is broad, provide the codes you have tried.
  • I'd do something like this: create a handleCardFlip(cardNumber, isOpen) method on the "game controller" (or whatever you have). bind this method to the game controller. Pass this method ot cards when you create them. Call this method from inside cards.
  • @Aria I'm trying to make this a broad best practice question, I don't have access to my code at the moment but I'm not necessarily looking for a code answer either I want to know how I should think about handling events in nested web-components, the answer should be applicable to any scenario where you want a web component to respond to changes in one of its nested components.
  • @Dmitry Thank you, that sounds sensible, I will try this approach when I get home
  • Thank you, I was able to implement the solution in this answer.
  • My implementation: In memory-card.js i added const event = new CustomEvent('flippedCard') this.dispatchEvent(event) to the pre-existing click eventlistener in memory-game.js i added card.addEventListener('flippedCard', this.flipCard.bind(this)) to set images after creating the memory-card element
  • You could also use the arrow notation to avoir bind(): () => this.flipCard()
  • Thank you for your answer Intervalia it further explains the logic suggested by @Supersharp and was very helpful. I added my code to my question in case you have a suggestion for a better implementation.