Nested web-components and event handling - javascript

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.

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

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.

Related

Can I decouple Classes by adding logic to the Controller in MVC?

I have hopefully explained my problem below by writing a short simplified example with the same conceptual question as my actual larger application. My question is centred around ways in which I can de-couple classes from each other. Whilst I accept no application can be free of coupling completely and modules have to interact with each other, I am trying to establish best ways in which I can keep each class as a separate unit so that if changes are made in one class it won't affect another. I want to keep changes simple.
Below I have outlined an example to the regular problem I face. As an example I have centred this example around a simple game where we can add a player named 'Kevin' everytime we press enter. Then everytime we click a button we add a score of 10 to every players running score and print the updated score of the current player to the screen.
Like most things I have used the MVC pattern to achieve this small example game. The Model has a Player class with data and methods. One of the methods adds 10 to the players score.
I also have a PlayerCircle class in the Model which returns things like the current player as-well as changing which players turn it is.
Lastly in the Model I have a Game Class which imports PlayerCircle. PlayerCircle is then a property of the Game class. Its the only property at the moment but as the game grows, further classes will be added e.g. Dice class
The controller module calls methods from the Model and passes things into view much like how an MVC pattern should work.
This is my PlayerCircle class
export class PlayersCircle {
constructor() {
this.players = [];
this.playersTurn = 0;
}
addPlayer(name) {
const player = new Player(name);
this.players.push(player);
}
changePlayer() {
this.playersTurn++;
}
getPlayer() {
return this.players[this.playersTurn];
}
increaseAllScores() {
this.players.forEach((player) => {
player.add10();
});
}
getAllPlayers() {
return this.players;
}
}
This is my player class
export class Player {
constructor(name) {
this.name = name;
this.score = 0;
}
add10() {
this.score += 10;
}
getScore() {
return this.score;
}
}
This is my game class
import { PlayersCircle } from "./PlayersCircle.js";
export class Game {
constructor() {
this.playersCircle = new PlayersCircle();
}
}
Now I have set up the classes for this very simple game, I now write this very simple function from the controller which adds a player to the game.
import { Game } from "./model/Game.js";
import { viewShowScore } from "./view/viewShowScore.js";
const game = new Game();
const addPlayer = () => {
game.playersCircle.addPlayer("kevin");
};
document.querySelector("body").addEventListener("keypress", addPlayer);
Ok so now I want to add 10 points to all the players in the game. Lets say I have fired the keypress 5 times and have 5 players. This is where I often struggle to establish the best Architectural Plan going forward with an Application.
The two options I normally have are
Call the add10 method on each player from the playerCircle class :-
This will enable me to use my controller just to call methods which change data or get data to the render the state into the view. This keeps my controller short and concise.However it means that the PlayersCircle class is now tightly coupled with the Player Class as one of the PlayerCircle methods uses a method of the Player class ( the add10 method) which means any changes in the Player Class on the add10 method will now break the PlayerCircle class.
My function from the controller to add 10 point to each player looks like this:
const increaseAllScores = () => {
game.playersCircle.increaseAllScores();
const currentPlayer = game.playersCircle.getPlayer();
viewShowScore(currentPlayer.getScore()); // function from view prints 10
};
document.querySelector("button").addEventListener("click", increaseAllScores);
Return all the players to the controller and then from inside the controller module run a loop on the returned array which holds the players-:
This will enable my PlayersCircle class to be completely independent from the Player class and just control the state of who'es turn it is, return all the players at once or separately. It won't have to concern itself with the inner methods of the Player class. The Player class as the game grows will have many methods. If I want to call a method on more than one player at a time then (like the add10 method) then the playersCircle class is very tightly coupled and changes from both classes will almost have to simultaneously occur.
However this would mean that my controller module isn't now just calling methods from the Model and passing results into the view but is now containing loops. Whilst I prefer this way whenever I see examples of MVC the controller is full of simple methods calls from the Model to the view.
The function inside the controller now looks like this:
const increaseAllScores = () => {
const allPlayers = game.playersCircle.getAllPlayers();
allPlayers.forEach((player) => {
player.add10();
});
const currentPlayer = game.playersCircle.getPlayer();
viewShowScore(currentPlayer.getScore()); // function from view prints 10
};
Both Pieces of code give the same result. However for long term code management when the game increases in complexity and may have new features in the future, which one is better than the other and why - option 1 which calls the add10 from the playersCircle targeting every player or option 2 which runs a loop from the controller targeting every player which is returned from the playerCircle class ?

Angular Template Interpolation Slow to Respond

My Angular template needs to display a rapidly changing value, driven by mousemove events, which I am retrieving from my NgRx Store. The Store appears to be keeping up with the data changes but the resulting value displayed in the template lags behind and appears to only refresh when the mouse stops moving.
A component with approximately 300 DOM elements detects mousemove events and handles them outside of ngZone as follows:
ngAfterViewInit() {
const eventElement = this.eventDiv.nativeElement;
this.move$ = fromEvent(eventElement, 'mousemove');
this.leave$ = fromEvent(eventElement, 'mouseleave');
/*
* We are going to detect mouse move events outside of
* Angular's Zone to prevent Change Detection every time
* a mouse move event is fired.
*/
this.ngZone.runOutsideAngular(() => {
// Check we have a move$ and leave$ objects.
if (this.move$ && this.leave$) {
// Configure moveSubscription.
this.moveSubscription = this.move$.pipe(
takeUntil(this.leave$),
repeat()).subscribe((e: MouseEvent) => {
e.stopPropagation();
this.mouseMove.emit(e);
});
};
});
A parent component handles the resulting mouseMove event and still outside ngZone performs some Calculations to ascertain which element the mouse is over. Once the result has been calculated a function is called, passing in the calculated result, and within this function I dispatch an NgRx Action within ngZone using this.ngZone.run(() => { dispatch Action here }.
I can see that the Store reacts quickly to the changing data. I then have a separate component responsible for displaying the result. An Observable listens to the Selector's changing values and displays the result using interpolation.
Curiously I added an RxJs tap into the Observable declaration as follows:
public mouseoverLocationName$: Observable<string | null>;
constructor(
public store: Store<fromPilecapReducers.PilecapState>
) {
this.mouseoverLocationName$ = this.store.pipe(
select(fromPilecapSelectors.PilecapMapSelectors.selectMouseoverLocationName),
tap(locationName => {
console.log(`mouseoverLocation$: ${locationName}`);
})
);
}
The console logs out the locationName value nice and quickly. However the html element displaying the string is very slow and, as I said earlier, only appears to update when the mouse stops moving. The template code is as follows:
<h2>{{mouseoverLocationName$ | async}}</h2>
I've got to the point now where I can't see the wood for the trees! any suggestions or guidance very welcome.

when can we access the children elements of a custom component using javascript?

So I'm trying to build a custom component using vanilla javascript, which will do certain things depending on the number of children it has, meaning it has to count said children
If I have the following markup (where the custom component is called "my-component")
<my-component>
<div></div>
<!-- ...arbitrary number of child elements -->
</my-component>
And the following javascript code in the <head></head> to ensure it's loaded before the <body></body> is parsed
class MyComponent extends HTMLElement {
constructor(){
super()
this.children.length
//do stuff depending on the number of children
}
//or
connectedCallback () {
this.children.length
//do stuff depending on the numbre of children
}
}
customElements.define("my-component",MyComponent)
this.children.length will return 0 in both cases, despite the elements showing on the screen afterwards, and being able to inspect the custom element on the console and get the expected number of children with Element.children.length. I suppose that this means the children elements are not yet available at the time the constructor nor the connectedCallback are run.
Is there any way to specify in my element's class definition a function that will trigger when the children elements become available, so that I can do stuff with them? I was hoping for a "childElementsReady" callback or something similar, but I guess that it doesn't exist. I don't know if there's a really obvious way to deal with this that I'm just missing, because this seems like something that I should be able to do relatively easily
A MutationObserver is the best way to handle this. You can set one up in connectedCallback to observe changes to the Light DOM - in this case it's enough to observe childList only:
class MyElement extends HTMLElement {
constructor() {
super();
this.onMutation = this.onMutation.bind(this);
}
connectedCallback() {
// Set up observer
this.observer = new MutationObserver(this.onMutation);
// Watch the Light DOM for child node changes
this.observer.observe(this, {
childList: true
});
}
disconnectedCallback() {
// remove observer if element is no longer connected to DOM
this.observer.disconnect();
}
onMutation(mutations) {
const added = [];
// A `mutation` is passed for each new node
for (const mutation of mutations) {
// Could test for `mutation.type` here, but since we only have
// set up one observer type it will always be `childList`
added.push(...mutation.addedNodes);
}
console.log({
// filter out non element nodes (TextNodes etc.)
added: added.filter(el => el.nodeType === Node.ELEMENT_NODE),
});
}
}
customElements.define('my-element', MyElement);
Here onMutation will be called every time nodes are added to the Light DOM so you can handle any set up here.
Note that, depending on the nodes in the Light DOM, onMutation can be called more than once when the element is connected to the DOM so it's not possible to say that all the children are 'ready' at any point - instead you must handle each mutation as it comes in.
I write this reply to my own question because I found out that this is an useful way to watch for added children when you have a shadow dom, so hopefully it can help anyone in that situation, but lamplightdev's answer is the most complete one since it works both when you use a shadow dom or not, so look up his answer as well
If your custom element makes use of the shadow dom, you can do this:
class MyComponent extends HTMLElement {
childAddedCustomCallback () {
let watchedSlot = this
/*"this" here is not the custom element, but a slot that the custom
element will have embedded in its shadow root, to which this function
will be attached as an event listener in the constructor*/
let children = watchedSlot.assignedElements()
let numberOfChildren = children.length
//do stuff depending on the number of children
}
constructor(){
super()
let shadowRoot = this.attachShadow({mode:"open"})
shadowRoot.innerHTML = "<slot></slot>"
let slotToWatch = shadowRoot.querySelector("slot")
slotToWatch.addEventListener("slotchange",this.childAddedCustomCallback)
}
}
customElements.define("my-component",MyComponent)
With this, every time you add a child to the custom element, it will reflect to the unnamed slot, and the slot's event listener will trigger the callback when that happens, giving you a reliable and clear way to access the children of the element as soon as they're available
This has the downside of excecuting n times if you add n children to the custom element, e.g. if you have the following markup:
<my-component>
<div></div>
<div></div>
<div></div>
<div></div>
</my-component>
Instead of excecuting once when the last child element is added, it will excecute 4 times (one for every child), so beware of that

Event delegate - Retrieve instance of class that a DOM element is a property of

I'm learning the ways of scoping and inheritance and am having a little difficulty retrieving the instance of a class of which a DOM element is a property. There is a Parent and lets say there can be unlimited Child(ren). Is there any way of getting the instance via an event delegate? Hopefully the following will help explain:
class Parent {
constructor() {
this.element = document.createElement('div');
this.element.setAttribute('class', 'parent');
// add elements from instances
this.element.appendChild(new Child(1, this).element);
this.element.appendChild(new Child(2, this).element);
this.element.appendChild(new Child(3, this).element);
// event delegate
this.element.addEventListener('click', (e) => {
// would like to get the particular instance of the class "Child" that was clicked
});
}
something(id) {
console.log('something', id);
}
}
class Child {
constructor(id, parentReference) {
this.id = id;
this.element = document.createElement('div');
this.element.setAttribute('class', 'child');
// would like to avoid adding a click event for each and use the parent as delegate
this.element.addEventListener('click', () => {
parentReference.something(this.id)
});
}
}
Hope this makes sense, welcome any and all feedback.
Also on another note, since I'm not storing the instances anywhere, only creating and appending an element to the DOM, will the Child instance remain intact as long as the DOM element remains rendered and other properties get used then be garbage collected? Should they be stored elsewhere, like an array to be safe?
Thanks in advance!
Looks like what you're trying to do is actually Web Components : you could just make your class extend HTMLElement and benefit from the whole lifecycle which comes natively with custom elements.
For example, you could have a Parent component which holds some Child components. As Parent and Child classes would be real HTMLElements, if you register a click event listener in the Parent object, you will receive click events triggered on the childs. And the clicked child instance will be available in the target property of the click event.
class ParentElement extends HTMLElement {
constructor() {
super();
this.classList.add('parent');
// add elements from instances
this.innerHTML = `
<child-element data-sequence="1"></Child>
<child-element data-sequence="2"></Child>
<child-element data-sequence="3"></Child>
`;
// event delegate
this.addEventListener('click', (e) => {
// Child instance is available in the event itself
const child = e.target;
this.something(child.id);
});
}
something(id) {
console.log('something', id);
}
}
window.customElements.define('parent-element', Parent);
class ChildElement extends HTMLElement {
constructor() {
super()
this.id = this.dataset.sequence;
this.classList.add('child');
}
}
window.customElements.define('child-element', ChildElement);
Then you can just append the parent element into the dom :
document.body.appendChild(new ParentElement());
(Be careful, i didn't tested the previous example, this is just to give you an idea)
But maybe i get it wrong. In this case, i would suggest to avoid keeping a reference to a dom element into custom objects as it would probably be easy to get memory leaks (if you re render some portion of your page but keep references to Parent or Child instances then you'll also keep a reference to the corresponding dom node). Couldn't you go the other way around by attaching your custom objects onto dom nodes (a bit like a controller ?)
Hope this help,

How to fit these two classes together?

This is my first attempt at doing OOP and made a simple Pomodoro timer. https://codepen.io/hyrosian/project/editor/XpjOPR
There was an attempt to seperate the countdown timer from the controls. The countdown timer is working but i'm not sure how to get class Counter and class Controls somehow 'fit' together.
const counter = new Counter(DOMnode)
const controls = new Controls()
The plan was to set up the eventlisteners and handlers inside Controls. We get the values e.g. +5mins for changing the session/break length inside of it. But Controls needs access to the state inside Counter.
class Controls extends Time {
constructor() {
super()
this.session_btn = document.querySelectorAll('[data-session]')
this.break_btn = document.querySelectorAll('[data-break]')
}
handleBreak(e) {
console.log(e.target.dataset.break)
}
handleSession(e) {
console.log(e.target.dataset.session)
}
init() {
this.session_btn.forEach(btn => btn.addEventListener('click', this.handleSession))
this.break_btn.forEach(btn => btn.addEventListener('click', this.handleBreak))
}
}
What would be the proper way to fit them together?
Controls should have a reference to the Counter object, but not the other way around.
Controls can access the state by asking for (getTime, getState). If Controls needs to subscribe to some event (timer has run out) then Counter could emit such an event.
The underlying idea would be that Counter is a completely isolated object and just maintains its own state and timers, but Controls is the thing that handles events from the user (and potentially updates the DOM when needed).
This is a pretty common pattern. In this scenario, Counter would often be called the model, and Controls the Controller.
You should first create the Counter, and pass it as a constructor argument to Controls

Categories

Resources