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
- Level 0 - Builder
- Component A 1 - Level 1 - Components
- Component A 2 - Level 1.1 - Fields (draggable element)
- Component B 1 - Level 1 - Canvas
- Component B 2 - Level 1 - Page
- Component B 3 - Level 1.1 Grid Item
- 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?