2

I have thought experiment that derived from case where I needed to sometimes add and remove function for Output of my Angular component. This is the example

Parent component will contain only output function for child component <my-component>.

ParentComponent

class ParentComponent{
  myOutputFunction = null;

  setOutputFunction() {
    this.myOutputFunction = () => {
      // do something
    };
  }

  removeOutputFunction() {
    this.myOutputFunction = null;
  }
}

Template for ParentComponent

<my-component (someOutput)="myOutputFunction($event)"><my-component>

Child component MyComponent will have output someOutput inside. This will serve also as a question how to detect when parent decided to do removeOutputFunction() and potentially subscribe to that event and react to it.

MyComponent child component

class MyComponent {
  @Output() someOutput = new EventEmitter();

  ngOnInit(): void {
    // At this point we can see if there are any observers to someOutput
    if(this.someOutput.observers.length > 0) {
      console.log('YEY we have a set output method from ParentComponent to MyComponent`s attribute');
    } else {
      console.log('We have no attribute defined');
    }
  }
}

In ngOnInit() we can check if we have coded (someOutput)="myOutputFunction($event)".

So, the conclusion. Is there a way to listen (subscribe) to change of someOutput from parent from () => // do something value to null value.

When i log this.someOutput.observers i get array of Subscribers. Can I use this.someOutput as EventEmitter in some way to get to where this experiment is pointing to?

Thanks

Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62
  • Your question seems confusing. Can you add simple short note of actual solution you are looking for. – Pushprajsinh Chudasama Apr 07 '20 at 14:05
  • The problem here is that the child component always sees a listener if you have `(myEvent)="myHandler()"` , even if `myHandler` is null. Are you open to other approaches other than toggling the underlying event handler between null and a function? – Kurt Hamilton Apr 07 '20 at 19:49
  • @PushprajsinhChudasama i dont have an idea how to potentially solve this. Because as Kurt Hamilton said, child component always has a listener no matter what i do with `myOutputFunction`. @KurtHamilton, i am open to any suggestions as long as i can register in my child component that i have no subscribers and just remove subscription for that event in other logic in child component – Mario Petrovic Apr 08 '20 at 12:04
  • @MarioPetrovic a question - is it unwanted that the child component is re-rendered when the parent component gets rid of the function? – saglamcem Apr 08 '20 at 15:11
  • @saglamcem no, i just need a hook in child to react to parent change of that function so i can do some changes (not in the example, but my case is that i want to remove event listener on my library) – Mario Petrovic Apr 09 '20 at 11:59
  • You can pass child component with `myOutputFunction` as `@Input` and watch in child ngOnChanges for it's value (null/function). SHould be quite straight forward – Fan Cheung Apr 10 '20 at 14:06
  • This looks similar to https://stackoverflow.com/questions/45985022/conditionally-apply-click-event-in-angular-4 Generally, the idea is that `(someEvent)=""` instructs Angular to subscribe to the event emitter - no matter what `` is, or whether it's null or now. From the ChildComponent's standpoint there is always a single subscriber in this case - some internal code from Angular itself. This internal subscriber looks like `function (event) { return dispatchEvent(view, index, eventName, event); }` – amakhrov Apr 11 '20 at 22:50

4 Answers4

1

As soon as you use templating, you cannot deduce anything by looking at the number of observers. Even if you did:

<my-component (someOutput)="myOutputFunction"><my-component>

With myOutputFunction being null, number of observers would still be 1.

You could partially get there by a programmatic approach using ViewChild to conditionally setup the observer (not suggested approach, see reasons below*):

@ViewChild(MyComponent, { static: true }) comp: MyComponent;

  myOutputFunction = null;

  setOutputFunction() {
    this.myOutputFunction = () => {
      console.log('something');
    };
    this.comp.someOutput.subscribe(this.myOutputFunction);
  }

See demo: https://stackblitz.com/edit/angular-output-programmatic

However, imho you're going about this in the wrong way. It seems you want child component to (conditionally) produce some result iff someone is listening.

  • Using an EventEmitter to deduce this is wrong; if you want behaviour in child component to depend on myOutputFunction, pass an @Input() to child component - simplest, pass myOutputFunction itself and let child component invoke it. Or alternatively:
  • In general, this sounds like a use case for an observable: If (and only if) someone subscribes to it, something should be produced. The consumer is the parent, the producer is the child. Subject to the rescue.
  • Such patterns sounds unrelated to a component; consider instead putting the observable/subject in a service.

You're probably already aware of all this, so I'll leave it at: No, don't base logic on observers of an EventEmitter. *Even if you go the programmatic approach you've broken some basic abstractions; correct use of - and behaviour from - child component will require consumer has intricate knowledge of child component logic.

corolla
  • 5,386
  • 1
  • 23
  • 21
  • Thanks for your answer. Although this is a solution, i think its too hacky, because when you start tampering with angular abstraction of subscriptions by tapping into component with `ViewChild` and change subscription methods. Also, using an input wont give child -> parent emission. – Mario Petrovic Apr 15 '20 at 10:58
  • 1
    @MarioPetrovic, I'll clarify that using viewchild is _not_ a suggested approach – corolla Apr 15 '20 at 15:48
1

You can't remove the output attribute dynamically, but I think this solution can help you.

If you want to set values from parent to child you should use @Input in the child component.

@Output is used to notify something from child to parent.

Solution 1

If you want the children to only emit values when the parent has a function, tell the child component when someone is listening.

Let's assume that your emitted values are of type string

Child Component

class MyComponent {
  @Output() someOutput: EventEmitter<string> = new EventEmitter();
  @Input() isParentListening = false;

  ngOnInit(): void {
    if (this.isParentListening) {
      // Parent wants to receive data
      this.someOutput.emit("Yay! Parent is listening");
    } else {
      // Parent is not listening
      console.log("My parent doesn't take care of me :(");
    }
  }
}

Parent Component

<my-component
  [isParentListening]="listenChildComponent"
  (someOutput)="myOutputFunction($event)"
></my-component>
class ParentComponent {
  listenChildComponent = true;

  myOutputFunction = (value: string) => {
    // do something
  };
}

Solution 2

Another solution is keeping the child component clean and unaware of this logic, and the parent decides if it has to do something or not.

Child Component

class MyComponent {
  @Output() someOutput: EventEmitter<string> = new EventEmitter();

  ngOnInit(): void {
    // Output always as if someone were listening
    this.someOutput.emit("Yay! Someone is listening (I hope)");
  }
}

Parent Component

The parent simply doesn't implement any logic when that output event occurs.

<my-component (someOutput)="()=>{}"></my-component>
class ParentComponent {}
adrisons
  • 3,443
  • 3
  • 32
  • 48
  • 1
    Thanks for this comprehensive answer. I hoped i would not have to fall to output/input solution where input is looked to change of the output function. This is because i dont want to take care of 2 values (boolean and function). But as i can see, my thought experiment is just not doable in Angular. Definitely input/output is the only way to go with it, because i will listen to input change and then react to that change in child component. In my case i use OpenLayers component that does hover emission and i want to stop it sometimes. Thanks very much on your time – Mario Petrovic Apr 15 '20 at 11:03
0

Why not use Input?

class MyComponent {
  @Input() func = null;

  ngOnInit(): void {
    if (func)
       func();
    else
       console.log("I'm child")
  }
}

In your parent

   <app-child [func]="functionInParent"></app-child>

   functionInParent()
   {
         console.log("function in parent") 
   }

   //OR
   <app-child></app-child>
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • Using @Input will not emit changes to parent component. And also doing it in `ngOnInit()` will only do it on child initialization. I appreciate your effort, this answer missed the point of listening on change and being able to emit change to the parent – Mario Petrovic Apr 15 '20 at 10:51
-2

That's a very interesting idea. but just to be sure that you are getting it the right time you should listen to the 'newListener' event and do your logic there. check this link as reference https://nodejs.org/api/events.html#events_event_newlistener

....
this.someOutput.once('newListener', (event, listener) => {
    ... // you have a listener
})

...