2

We are trying to build a drag and drop interface where after a drop, we want to communicate up the component tree that an element has been dropped. We need to communicate to the original LWC component in order to do some post-drop processing.

The component structure currently is

  1. Level 0 - Builder
  2. Component A 1 - Level 1 - Components
  3. Component A 2 - Level 1.1 - Fields (draggable element)
  4. Component B 1 - Level 1 - Canvas
  5. Component B 2 - Level 1 - Page
  6. Component B 3 - Level 1.1 Grid Item
  7. Component B 4 - Level 1.2 Element (drop section)

The problem is communicating from B4 Canvas Element back to Level 0 - Builder and up again to A2 Fields.

We think of 2 ways of handling this -

  • 1 - build a communication bus where when Level 5 Canvas Element registers a drop, it communicates with new CustomEvent() back to the parent component, which in terms catches the event and re-emits another new CustomEvent() to it's parent and so on down to Level 1 Builder and use Parent to Child communication up to A2 Fields. This approach seems to work but is "ugly" and requires that all components in the tree implement the "catch event - emit event" function

  • 2 - using window.postMessage() when B4 Canvas Element registers a drop and then registering a listener on the components that need to monitor for drop through window.addEventListener("message", this.onMessage.bind(this), false); - notice, we require bind(this) in order to access local vars in the component catching the window.postMessage()

We favor the second approach as it's cleaner and doesn't require implementing an event bus on each component to communicate the drop event, however Salesforce advise against doing .bind(this) (https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.create_lifecycle_hooks_dom) with the warning:

Don’t use addEventListener(eventName, this.handleNotification.bind(this)). It’s an anti-pattern because bind() returns a new function and thus the component can’t call removeEventListener() with the same function instance. Because the component can’t use the same function instance, the listener leaks.

We think of getting around this by keeping the this.onMessage.bind(this) function in a local variable like

connectedCallback() {
    window.addEventListener("message", this.myMessageHandler, false);
}
//Local Var
myMessageHandler = this.onMessage.bind(this);
onMessage(e){
    try {
        if (e.data) {
            //Event received
            console.log('Post Message myMessageHandler Received with data', JSON.stringify(e.data));
        }else{
            console.log('Post Message myMessageHandler Received, no data')
        }

    } catch (e) {
        console.log(e);
    }
}

disconnectedCallback(){
    window.removeEventListener("message", this.myMessageHandler);
}

This way, we can remove the listener by implementing

window.removeEventListener("message", this.myMessageHandler)

in the disconnectedCallback.

My concern is with Salesforce are telling us to not do .bind(this) but they are not giving us any alternatives.

What is the best way to handle the given scenario that is "clean" by salesforce standards? Any issues with our workaround of putting the event listener in a local variable? What is the best approach to communicate such an event between sibling components or components, multiple levels deep?

2

As long as you have a handle to the newly created function, that's okay. They're not advocating not using bind, but instead using the addEventListener(evt, this.fn.bind(this)) anti-pattern, because there's no reference to the newly created function, thus leakage. This is true for all frameworks, not just LWC specifically.

Also, another alternative:

const handler = e => this.fn(e);
addEventListener(evt, handler);

Arrow functions bind to their lexicographical this (in other words, your component controller) instead of whatever-random-thing-this-would-be. This is a perfectly safe alternative to using bind.

As an aside, if you think you might want to deal with the potential repercussions, there's an alternative event method you could use, namely a composed bubble event. This event traverses through all the shadow boundary layers and can reach the document root. There's a caution about events with the same name potentially firing handlers not meant for it, so some caution is advised, but possible if you control the entire hierarchy.

this.dispatchEvent(new CustomEvent('notify', { bubbles: true, composed: true, detail: payload }));

Read the documentation to see if this might be right for your use case.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.