Reputation: 437
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)
})
}
Upvotes: 5
Views: 5920
Reputation: 21301
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)?
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
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(),
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
Upvotes: 2
Reputation: 10975
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.
Upvotes: 1
Reputation: 31229
In the <memory-card>
:
CustomEvent()
and dispatch a custom event with dispatchEvent()
In the <memory-game>
:
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.
Upvotes: 6