5

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)
    })
  }
Supersharp
  • 29,002
  • 9
  • 92
  • 134
FroboZ
  • 437
  • 1
  • 6
  • 17
  • Yes and no. .... – Jonas Wilms Jan 02 '19 at 06:58
  • This is broad, provide the codes you have tried. – Aria Jan 02 '19 at 06:59
  • 1
    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. – Dmitry Jan 02 '19 at 07:45
  • @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. – FroboZ Jan 02 '19 at 07:46
  • @Dmitry Thank you, that sounds sensible, I will try this approach when I get home – FroboZ Jan 02 '19 at 07:47
  • @ChristopherKarlsson You may look this too: https://stackoverflow.com/questions/55001211/how-to-communicate-between-web-components-native-ui – Sandeep Jan 08 '20 at 09:41

3 Answers3

6

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.

Supersharp
  • 29,002
  • 9
  • 92
  • 134
  • 1
    Thank you, I was able to implement the solution in this answer. – FroboZ Jan 02 '19 at 22:43
  • 1
    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 – FroboZ Jan 02 '19 at 22:51
  • 1
    You could also use the arrow notation to avoir `bind()`: `() => this.flipCard()` – Supersharp Jan 03 '19 at 12:04
2

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

Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
1

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.

Intervalia
  • 10,248
  • 2
  • 30
  • 60
  • 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. – FroboZ Jan 02 '19 at 22:56