简体   繁体   中英

Nested web-components and event handling

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)
    })
  }

In the <memory-card> :

  • Create with CustomEvent() and dispatch a custom event with dispatchEvent()

In the <memory-game> :

  • Listen to your custom event with addEventListener()

Because the cards are nested in the game, the event will bubble naturally to the container.

This way the 2 custom elements will stay loosley coupled.

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM