I\'m trying to use native web components for one of my UI project and for this project, I\'m not using any frameworks or libraries like Polymer etc. I would like to know is
Custom Events is the best solution if you want to deal with loosely coupled custom elements.
On the contrary if one custom element know the other by its reference, it can invoke its custom property or method:
//in chatForm element
chatHistory.attachedForm = this
chatHistory.addMessage( message )
chatHistory.api.addMessage( message )
In the last example above communication is done through a dedecated object exposed via the api
property.
You could also use a mix of Events (in one way) and Methods (in the other way) depending on how custom elements are linked.
Lastly in some situations where messages are basic, you could communicate (string) data via HTML attributes:
chatHistory.setAttributes( 'chat', 'active' )
chatHistory.dataset.username = `$(this.name)`
+1 for both other answers, Events are the best because then Components are loosly coupled
Note that in the detail
of a Custom Event you can send anything you want.
So I use (psuedo code):
Elements that define a Solitaire/Freecell game:
-> game Element
-> pile Element
-> slot Element
-> card element
-> pile Element
-> slot Element
-> empty
When a card (dragged by the user) needs to be moved to another pile,
it sends an Event (bubbling up the DOM to the game element)
//triggered by .dragend Event
card.say(___FINDSLOT___, {
id,
reply: slot => card.move(slot)
});
Note: reply
is a function definition
Because all piles where told to listen for ___FINDSLOT___
Events at the game element ...
pile.on(game, ___FINDSLOT___, evt => {
let foundslot = pile.free(evt.detail.id);
if (foundslot.length) evt.detail.reply(foundslot[0]);
});
Only the one pile matching the evt.detail.id
responds:
!!! by executing the function card
sent in evt.detail.reply
And getting technical: The function executes in pile
scope!
(the above code is pseudo code!)
Might seem complex;
The important part is that the pile
element is NOT coupled to the .move()
method in the card
element.
The only coupling is the name of the Event: ___FINDSLOT___
!!!
That means card
is always in control, and the same Event(Name) can be used for:
pile
makes a Full-House?In my E-lements code pile
isn't coupled to evt.detail.id
either,
CustomEvents only send functions
.say()
and .on()
are my custom methods (on every element) for dispatchEvent
and addEventListener
I now have a handfull of E-lements that can be used to create any card game
No need for any libraries, write your own 'Message Bus'
My element.on()
method is only a few lines of code wrapped around the addEventListener
function, so they can easily be removed:
$Element_addEventListener(
name,
func,
options = {}
) {
let BigBrotherFunc = evt => { // wrap every Listener function
if (evt.detail && evt.detail.reply) {
el.warn(`can catch ALL replies '${evt.type}' here`, evt);
}
func(evt);
}
el.addEventListener(name, BigBrotherFunc, options);
return [name, () => el.removeEventListener(name, BigBrotherFunc)];
},
on(
//!! no parameter defintions, because function uses ...arguments
) {
let args = [...arguments]; // get arguments array
let target = el; // default target is current element
if (args[0] instanceof HTMLElement) target = args.shift(); // if first element is another element, take it out the args array
args[0] = ___eventName(args[0]) || args[0]; // proces eventNR
$Element_ListenersArray.push(target.$Element_addEventListener(...args));
},
.say( )
is a oneliner:
say(
eventNR,
detail, //todo some default something here ??
options = {
detail,
bubbles: 1, // event bubbles UP the DOM
composed: 1, // !!! required so Event bubbles through the shadowDOM boundaries
}
) {
el.dispatchEvent(new CustomEvent(___eventName(eventNR) || eventNR, options));
},
In your parent code (html/css) you should subscribe to events emited by <chat-form>
and send event data to <chat-history>
by execute its methods (add
in below working example)
// WEB COMPONENT 1: chat-form
customElements.define('chat-form', class extends HTMLElement {
connectedCallback() {
this.innerHTML = `Form<br><input id="msg" value="abc"/>
<button id="btn">send</button>`;
btn.onclick = () => {
// can: this.onsend() or not recommended: eval(this.getAttribute('onsend'))
this.dispatchEvent(new CustomEvent('send',{detail: {message: msg.value} }))
msg.value = '';
}
}
})
// WEB COMPONENT 2: chat-history
customElements.define('chat-history', class extends HTMLElement {
add(msg) {
let s = ""
this.messages = [...(this.messages || []), msg];
for (let m of this.messages) s += `<li>${m}</li>`
this.innerHTML = `<div><br>History<ul>${s}</ul></div>`
}
})
// -----------------
// PARENT CODE
// which subscribe chat-form send event,
// receive message and set it to chat-history
// -----------------
myChatForm.addEventListener('send', e => {
myChatHistory.add(e.detail.message)
});
body {background: white}
<chat-form id="myChatForm"></chat-form>
<chat-history id="myChatHistory"></chat-history>
If you look at Web Components as being like built in components like <div>
and <audio>
then you can answer your own question. The components do not talk to each other.
Once you start allowing components to talk directly to each other then you don't really have components you have a system that is tied together and you can not use Component A without Component B. This is tied too tightly together.
Instead, inside the parent code that owns the two components, you add code that allows you to receive events from component A and call functions or set parameters in Component B, and the other way around.
Having said that there are two exceptions to this rule with built in components:
The <label>
tag: It uses the for
attribute to take in an ID of another component and, if set and valid, then it passes focus on to the other component when you click on the <label>
The <form>
tag: This looks for form elements that are children to gather the data needed to post the form.
But both of these are still not TIED to anything. The <label>
is told the recipient of the focus
event and only passes it along if the ID is set and valid or to the first form element as a child. And the <form>
element does not care what child elements exist or how many it just goes through all of its descendants finding elements that are form elements and grabs their value
property.
But as a general rule you should avoid having one sibling component talk directly to another sibling. The methods of cross communications in the two examples above are probably the only exceptions.
Instead your parent code should listen for events and call functions or set properties.
Yes, you can wrap that functionality in an new, parent, component, but please save yourself a ton of grief and avoid spaghetti code.
As a general rule I never allow siblings elements to talk to each other and the only way they can talk to their parents is through events. Parents can talk directly to their children through attributes, properties and functions. But it should be avoided in all other conditions.