2

I've recently discovered Angular Elements and think that could be a very good fit for my portal application which involve many teams with different requirements.

Teams will be responsible for providing their own widgets to other teams. I need to PoC a totally decoupled typed-safe messaging bus between widgets (tile) allowing them to talk to each other.

I started from a very good demo application found on GitHub and added couple of classes. Forked project

Outer world communication is achieved by @Input messageIn and @Output messageOut from inherited BaseWidgetComponent class used by DashboardTileComponent and LazyDashboardTileComponent.

export class BaseWidgetComponent {
  private readonly _message: Subject<WidgetMessage>;

  constructor() {
    this._message = new Subject<WidgetMessage>();
    this.messageOut = new EventEmitter<WidgetMessage>();
  }

  @Input()
  set messageIn(message: WidgetMessage) {
    this._message.next(message);
  }

  @Output() messageOut: EventEmitter<WidgetMessage>;

  protected on(messageType: string): Observable<WidgetMessage> {
    return this._message.pipe(filter(x => x.type === messageType));
  }

  protected publish(messageType: string, payload: string): void {
    this.messageOut.emit({sender: this, type: messageType, payload: payload});
  }
}

DashboardComponent (container component) will be responsible to instantiate on runtime widgets and register them using the WidgetMessagingService to enable cross communication. Registration is adding event listener on the messageOut to publish new message and updating the messageIn with new message coming.

public register(widget: NgElement & WithProperties<BaseWidgetComponent>): void {
    this._message.subscribe(x => {
      widget.messageIn = x;
    });

    widget.addEventListener('messageOut', (x: CustomEvent<WidgetMessage>) => {
      this._message.next(x.detail);
    });
}

The problem with this technique, is that there's no mean to know if the widget has been destroy in order to clear all references to avoid leakage.

I tried to add an @Output('destroyed') and emit the event on the OnDestroy lifecycle hook without success.

I found some advanced code sample to intercept all events including 'disconnectedCallback' and do what I except but I wish to keep the code very simple for other devs. Angular Elements bridge

Here you can find My working implementation. You just need to add a default and a lazy widget and click on their publish buttons to see the full flow.

Basically I need to find a way to know when the widget has been disposed and call the unregister method to clear everything.

How can I do that?

Is there any better ways to achieve this communication?

mathpaquette
  • 406
  • 4
  • 16
  • Maybe add a callback as an input and call that inside your ngOnDestroy hook? – pascalpuetz Nov 10 '18 at 21:17
  • yeah potentially could work but I feel this is not a very Angular way to do it. Do you think I can use @ViewChildren to get added/removed nodes? – mathpaquette Nov 10 '18 at 21:23
  • 1
    If the outer application is an angular application/angular element as well, then it should be possible with ViewChild/ViewChildren. That would shift the responsibility though, since the outer component then needs to observe the elements. By passing in a callback it's the child component that tells the outer application that it is destroyed. – pascalpuetz Nov 10 '18 at 21:42
  • ViewChild/ViewChildren don't work with elements added to the DOM dynamically like I do. – mathpaquette Nov 10 '18 at 22:54
  • I can't believe I need to create my own NgElementStrategy to do that. – mathpaquette Nov 11 '18 at 00:35
  • Why makes you think that `ngOnDestroy` is not a Angular way to destroy or unsubscribe ? – Sunil Singh Nov 11 '18 at 03:55
  • ngOnDestroy is totally fine but I just don't want to pass a callback as @Input parameter which is a little weak. – mathpaquette Nov 11 '18 at 04:15
  • 1
    The problem here is that ngOnDestroy is called after the element has been removed from the dom. So @Output will not work. This is reported as a bug here https://github.com/angular/angular/issues/14252 – pascalpuetz Nov 11 '18 at 09:48
  • @pascalpuetz maybe the cleanest way to do it for now is to pass the complete MessagingService instance via Input and handle the registration/unregistration with ngInit() and ngOnDestroy() – mathpaquette Nov 11 '18 at 14:03
  • 1
    To be honest, that is up to preference. I'd use the callback variant since the component would not require to know the API of the Service. If you ever decided to change the service/ it's API, you would not have to touch your components since they're only calling a callback. – pascalpuetz Nov 11 '18 at 14:11

0 Answers0