0

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 .

Abhiram M
  • 3
  • 2
  • You can used service to call common or shared function. – Hardik Solanki Jan 04 '23 at 10:59
  • to build on what hardik say, you could use a service for this and subscribe to a service which you call, then you can make an interface with a function you wish to be implemented in the functions and have them subscribe to the the service, and when i say subscribe i not mean RxJS way but in the observer pattern way. – Henrik Bøgelund Lavstsen Jan 04 '23 at 11:01
  • What you want to do is viewing children components and calling a method in them. It CAN be done, but it is very anti-pattern, and the service approach mentioned above is the most maintainable and scalable solution. Try refactoring to use a service – Francisco Santorelli Jan 04 '23 at 11:30

1 Answers1

0

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()

nate-kumar
  • 1,675
  • 2
  • 8
  • 15
  • As you pointed out, a singleton service injected into required components works fine for my actual use case. But as you said, "to achieve an `ngOnInit` like experience, all components would be forced to have a subscription setup by default which is not great". I was just wondering how to actually achieve this? Even though it is bad architecturally. Just out of curiosity. – Abhiram M Jan 09 '23 at 10:50
  • The simplest way would be to create a base class (e.g. the `BaseEventHandler` implementation). Pretty quick to setup and easy to register new components using the same approach. However you suffer all the typical problems with inheritance (e.g. you can't then extend another base class if you have a new requirement). Another option (which I haven't explored) would be to extend Angular's `@Component` decorator to take a new argument, or even consider creating your own `@MyComponent` decorator which extends/mimics the Angular `@Component` decorator. – nate-kumar Jan 09 '23 at 12:25