How can I call a specific public method of any component in Angular, like how `ngOnInit` works

197 Views Asked by At

In my project, I have many components. And I have an event handler. When a particular event occurs, I want to call a particular method inside the component by it's name (if it exists in any of the component). Please find the below example:

@Component({
...
})
export class MyComponent {

   public onMyEvent() {
     // do stuff on that event
   }
}

Here I want the onMyEvent() to be called whenever an event occurs as below:

export class MyEventHandler {
   private myEvent: Subject;
   
   public registerEvents() {
      this.myEvent.subcribe(() => {
        // call "onMyEvent" function of all components that exists in the view
      });
   }

}

The scenario is similar to how ngOnInit(), ngAfterViewInit().. works. When you implement OnInit, AfterViewInit interfaces, you are defining a contract that the component is guaranteed to have the specified method that is ready to be called at those times. I want to do the same way.

This can be kind of achieved using ComponentRef. But only a parant component can call it's child component and is very much dependent on the HTML structure. I want it to be as seamless as ngOnInit .

1

There are 1 best solutions below

2
On

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:

  1. A set of components which may/may not implement a method onMyEvent
  2. 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()