How to communicate between Web Components (native UI)?
Asked Answered
H

6

22

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 there any best way or other way to communicate between two web components like we do in angularjs/angular (like the message bus concept).

Currently in UI web-components, I'm using dispatchevent for publishing data and for receiving data, I'm using addeventlistener. For example, there are 2 web-components, ChatForm and ChatHistory.

// chatform webcomponent on submit text, publish chattext data 
this.dispatchEvent(new CustomEvent('chatText', {detail: chattext}));

// chathistory webcomponent, receive chattext data and append it to chat list
this.chatFormEle.addEventListener('chatText', (v) => {console.log(v.detail);});

Please let me know what other ways work for this purpose. Any good library like postaljs etc. that can easily integrate with native UI web components.

Heirloom answered 5/3, 2019 at 10:57 Comment(0)
O
27

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:

  1. 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>

  2. 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.

Ontogeny answered 6/3, 2019 at 16:7 Comment(5)
Both 1. and 2 can be rewritten to use only Events, Needs some extra work because if Form says 'ALLMYCHILDREN' it doesn't know how many responses to process; so you need some kind of timing to determine 'The last reply'. Kinda like students entering my classroom, I have no clue how many will come today or in what order. But I have a strict rule.. I wait 2 minutes after the last person entered, then I lock the door (yes with a key) ... teaches them Event Based programming :-)Crashland
Form's don't have a timing issue since their children already exist at the time the form is submitted. I included examples 1 and 2 to show the two, ONLY, exceptions to the rule. Everything else in traditional DOM elements is handled by events and accessing children's attributes and properties, or calling their functions.Ontogeny
Thanks a lot @Ontogeny for great explanation on this. I understood web-components are just like a built-in web components and their behaviour should be exactly same. I also learnt concept of Parents, attributes, properties etc. as you mentioned and tried to apply in my project. :)Heirloom
@Ontogeny let's say I have a view component that contains two child components: a list and a toolbar. Selecting an item in the list (checkbox) fires a custom event that bubbles all the way up to the parent view component. And if an item in the list is selected, the toolbar should enable some tool the user can use on the list. The options are to let the view speak to the toolbar directly, or pass the event to update a global state(think redux) where the view then listens for changes to the state and updates the toolbar. When would one be preferable over the other?Marxismleninism
If you let component A talk to Component B the you tie the two together. If you then need to swap out Component B with Component C and the interface is different then you need to change Component A to know how to talk to C. If, instead, you allow the events to be handled by the parent then the parent needs to know how to talk to C. But this is a choice of the write of the parent and not Component A. So it makes more sense to allow the parent to handle the differences instead of making A work with either B or C.Ontogeny
M
9

Working example

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 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 = () => {
      // alternative to below code
      // use this.onsend() or non 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 
// (e.g. in index.html which use above two WebComponents)
// Parent must just subscribe chat-form send event, and when
// receive message then it shoud give it to chat-history add method
// -----------------

myChatForm.addEventListener('send', e => {
  myChatHistory.add(e.detail.message)
});
body {background: white}
<h3>Hello!</h3>

<chat-form id="myChatForm"></chat-form>

<div>Type something</div>

<chat-history id="myChatHistory"></chat-history>
Matheny answered 25/2, 2020 at 17:18 Comment(0)
H
6

+1 for both other answers, Events are the best because then Components are loosly coupled


Also see: https://pm.dartus.fr/blog/a-complete-guide-on-shadow-dom-and-event-propagation/


Note that in the detail of a Custom Event you can send anything you want.

Event driven function execution:

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

Why?!

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:

  • Where can a card go to?
  • What is the best location?
  • Which card in the river 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));
    },
Hausa answered 8/3, 2019 at 8:16 Comment(0)
M
3

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)`
Marguritemargy answered 5/3, 2019 at 11:17 Comment(0)
P
0

I faced the same issue and as I couldn't find any fitting library I decided to write one on my own.

So here you go: https://www.npmjs.com/package/seawasp

SeaWasp is a WebRTC data layer which allows communication between components (or frameworks etc).

You simply import it, register a connection (aka tentacle ;) ) and you can send and receive messages.

I'm actively working on it so if you have any feedback /needed features, just tell me :).

Plovdiv answered 22/3, 2022 at 13:35 Comment(0)
S
0

For the case where the parent and child know about each other, like in a toaster example.

<toaster-host>
  <toast-msg show-for='5s'>Success</toast-msg>
</toaster-host>

Lots of options but for: Parent passing data to the child -> attributes or observedAttributes for primitives. If complex objects need to be passed either expose a function or a property aka domProperty that can be set. If a domProperty needs to react to being updated, it can be wrapped in a proxy.

Child passing data to parent -> can use events, or can query for the parent using .closest('toaster-host') and call a function or set a property. I prefer to query and call a function. Typescript helps with this type of approach.

In cases like the toaster example, the toaster-host and the toast-item will always be used together, so the argument about loose coupling is academic at best. They are different elements mainly because they have different jobs. If you wanted to swap out implementations of the toast-msg you could do that when you define the custom element, or even by changing the import statement to point to a different file.

Surpass answered 19/10, 2022 at 21:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.