2

In a parent component I have this code :

  <div class="app">
      <counter [init]="myValue" (change)="myValueChange($event);"></counter>
    </div>

Which registers the (change) event via template.

The inner component has :

 @Output('change')counterChange = new EventEmitter();

Which emits (on click) :

this.counterChange.emit(...)

Question:

In the parent component, how can I register the (change) event via code and not via template?

Aluan Haddad
  • 29,886
  • 8
  • 72
  • 84
Royi Namir
  • 144,742
  • 138
  • 468
  • 792

1 Answers1

1

You should subscribe to a Subject in the child component by adding the counter child component as a ViewChild.

IMPORTANT: As mentioned by @echonax an EventEmitter should never be used to subscribe to, as this class might eventually become for internal use only and is not guaranteed to be an Observable in the future, More details can be found in 'What is the proper use of an EventEmitter?'.

An example using a Subject and Subscription (untested):

app.component.ts:

import {Component, AfterViewInit, OnDestroy, ViewChild} from '@angular/core';
import {CounterComponent} from './counter.component';
import {Subscription} from 'rxjs/Subscription';

@Component({
    selector: 'my-app',
    template: `
    <div class="app">
        <counter [value]="myValue"></counter>
    </div>`
})
export class AppComponent implements AfterViewInit, OnDestroy {

    @ViewChild(CounterComponent) counter: CounterComponent;

    public myValue = 2;

    private counterSubscription: Subscription;

    ngAfterViewInit() {
        this.counterSubscription = this.counter.subject.subscribe(
            value => console.log('value', value)
        );
    }

    ngOnDestroy() {
        if (!!this.counterSubscription) {
            this.counterSubscription.unsubscribe();
        }
    }
}

counter.component.ts:

import {Component, Input} from '@angular/core';
import {Subject} from 'rxjs/Subject';

@Component({
selector: 'counter',
template: `
    <div class="counter">
    <div class="counter__container">
        <button (click)="decrement();" class="counter__button">
        -
        </button>
        <input type="text" class="counter__input" [value]="value">
        <button (click)="increment();" class="counter__button">
        +
        </button>
    </div>
    </div>`
})
export class CounterComponent {

    @Input() value = 0;

    private _subject = new Subject<number>();

    public subject = _subject.asObservable();

    increment() {
        this.value++;
        this.subject.next(this.value);
    }

    decrement() {
        this.value--;
        this.subject.next(this.value);
    }
}
Elwin Arens
  • 1,542
  • 10
  • 21
  • 1
    Your hooks are named wrong. they need to be `ngOnDestory` and `ngOnInit` – Aluan Haddad Apr 23 '17 at 07:35
  • 1
    Actually it should be `ngAfterViewInit` or the `counter` variable will be `undefined`. – Darin Dimitrov Apr 23 '17 at 07:37
  • 1
    Thanks. Can you please add a note telling the OP he should upgrade, he is using a pre-release version from like a year ago. He will have a hard time getting support if he doesn't – Aluan Haddad Apr 23 '17 at 07:40
  • @AluanHaddad Yeah I know it's just a plnkr that I've found. I already work with 4.0.2. I think it stays the same ( the general idea) – Royi Namir Apr 23 '17 at 07:42
  • @Royi many things are in fact different though, like the `directives` array is not valid. – Aluan Haddad Apr 23 '17 at 07:42
  • @AluanHaddad I know.:-) Like I've said I already work with 4.0.2 – Royi Namir Apr 23 '17 at 07:43
  • So please post questions that target that version, or something recent so that other people don't find solutions that will break. If you are already on that version, you have already experienced the pain of upgrading, so might as well use recent samples. – Aluan Haddad Apr 23 '17 at 07:44
  • 1
    @AluanHaddad Fair enough. I will remove the plnkr - since that specific question can be answered without code. – Royi Namir Apr 23 '17 at 07:55
  • 1
    @Royi I know this answer solves your problem but you shouldn't manually subscribe to an `EventEmitter`. It's an abstraction for rxjs `Subject` and Angular is not guaranteeing us that `EventEmitter` will continue being an `Observable`: http://stackoverflow.com/q/36076700/5706293. Use a `Subject` for bidirectional communication: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service – eko Apr 23 '17 at 07:58
  • @echonax Interesting. Thank you for your clarification. – Royi Namir Apr 23 '17 at 07:59
  • 1
    Thanks for the feedback everyone. I've updated my answer. I'm a bit baffled by the EventEmitter comment, considering it's TypeScript and they could've chosen not to expose the Observable API. – Elwin Arens Apr 23 '17 at 08:23
  • 1
    @ElwinArens Since the component is supposed to provide an Angular `EventEmitter` for communication with other Directives, I don't see any way around using an `EventEmitter`. You could _additionally_ expose an RxJS `Observable` directly for outside-of-a-template access but that would break the abstraction and make maintenance harder. The linked question has more to do with services. I think `EventEmitter` is the right choice here since it is an `@Output()`. That said, this Angular API is bad, just bad. No one should have to think about this. – Aluan Haddad Apr 23 '17 at 08:32
  • Now that you are using a `Subject`, something you definitely should _not_ expose, and have removed `@Output()`, you have broken the contract in the OP. It is no longer possible to consume this in both ways. At that point, as a service should be used instead or you should go back to `@Output()` and `EventEmitter`. If you do need to expose a `Subject` then expose it as an `Observable` using `asObservable` to prevent consumers from pushing values into it – Aluan Haddad Apr 23 '17 at 08:43
  • One way around this could be to expose your own `Output` decorator factory wrapping the angular one and ensuring that it always returns a `Subscribable`, of course this will break anyone using AOT because AOT changes the meaning of decorator syntax, violating the TS/ES behavior in the process. The whole thing is a damned mess. – Aluan Haddad Apr 23 '17 at 08:53
  • @AluanHaddad Can you please elaborate on your last comment ? why should AOT change the meaning of a decorator if angular already uses @input/@output and many other decorators. Can you please elaborate ( maybe example?) Because I'm using webpack with AOT compilation -with many decorators and I don't see any problem. I might be missing something.what did you mean ? – Royi Namir Apr 23 '17 at 13:44
  • I mean that framework decorators are no longer executed at runtime if you use AOT. This is a spec violation. It changes the meaning of code, for example, by breaking code that assumes the decorator would actually execute such as a wrapper. It is a semantic change that is against ECMAScript behavior that TypeScript implements. – Aluan Haddad Apr 23 '17 at 17:37
  • @Royi Long story short, AOT uses an anonymous language, forked from TypeScript. This language is no longer a superset of JavaScript. It is also not a subset because it uses the `@` character to mean something different. – Aluan Haddad Apr 23 '17 at 18:22