Remove Output attribute value from Angular component

Viewed 913

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

4 Answers

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.

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 {}

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>

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

...
Related