I feel there are a few concepts running in parallel here, so forgive me if my answer is off-base. Please let me know which elements are incorrect and I can amend my answer accordingly
As I understand you want the following:
- A set of components which may/may not implement a method
onMyEvent
- A watcher (subscription) in some/all of these components which attempts to trigger this method if it exists
tl:dr to achieve an ngOnInit
like experience (something which works out of the box with no component configuration) all components would be forced to have a subscription setup by default, which is not great architecturally. A singleton service injected with subscription determined by the components themselves appears to be a much more sensible approach, but this requires setup that you don't have with lifecycle hooks and as such may not qualify as "seamless"
Point 1 is simple enough, as you have already alluded to we can define a shared interface for a number of components to implement which mandates the presence of onMyEvent
.
export interface IEventHandler {
onMyEvent: () => void
}
export class MyComponent1 implements IEventHandler {
constructor() {}
public onMyEvent() {
// do stuff on that event
}
}
export class MyComponent2 {
constructor() {}
}
export class MyComponent3 implements IEventHandler { // error: `onMyEvent` is missing
constructor() {}
}
Point 2 requires a single stream (i.e. Observable
, Subject
, EventEmitter
, etc) which is capable of being watched by multiple components
The obvious solution for this would be a service which houses this stream and can be dependency injected into any component which requires it.
export class EventHandlerService {
public myEventSubject: Subject;
}
However, I am assuming that you do not want to generate a unique service instance for each component which requires it, you instead want a singleton service which broadcasts the same event to all components. As a result, you place the responsibility of subscribing to the event with the components themselves, i.e.
export class MyComponent1 implements IEventHandler {
constructor(private eventHandlerService: EventHandlerService) {}
ngOnInit() {
this.eventHandlerService.myEventSubject.subscribe({
next: () => this.onMyEvent() // guaranteed to exist as we have implemented `IEventHandler`
})
}
public onMyEvent() {
// do stuff on that event
}
}
This requires a manual setup that you don't have with inbuilt lifecycle hooks like ngOnInit
, but the difference here is that all components require ngOnInit
whereas only some of these components require a subscription to myEventSubject
. It is this selectivity which prevents the experience from being as "seamless"
You could of course explore a base class to keep your working components slightly more "neat"
export class BaseEventHandler implements IEventHandler {
constructor(public eventHandlerService: EventHandlerService) {}
ngOnInit() {
this.eventHandlerService.myEventSubject.subscribe({
next: () => this.onMyEvent() // guaranteed to exist as we have implemented `IEventHandler`
})
}
public onMyEvent() {
// do stuff on that event
}
}
export class MyComponent1 extends BaseEventHandler {
constructor(public eventHandlerService: EventHandlerService) {
super(eventHandlerService)
}
ngOnInit() {
super.ngOnInit();
}
}
export class MyComponent2 {
constructor() {}
}
But there seems little benefit over the previous implementation, considering you are still dependent on a service and selective subscription to the service variable (in this case selective extension of the BaseEventHandler
)
You also mention ComponentRef
, i.e. presumably using ViewChild
to access a component instance and call the method in question in a fashion such as:
@ViewChild(ChildComponent) childComponentRef: ComponentRef
this.childComponentRef.onMyEvent()
However in this case, the watcher for the event would still have to reside in individual components (in this case the parent component instead of the child component) and as not all parent components would need to subscribe to the watcher, you once again would achieve this using selective subscription vs an always-available hook such as ngOnInit()