10

How to call parent component's function when child component observed input changes?

The below is HTML structure.

# app.comopnent.html
<form>
  <textbox>
    <input type="text">
  </textbox>
</form>

# textbox.component.html
<div class="textbox-wrapper">
  <ng-content>
</div>

Restrictions are like following.

  • TextboxComponent has ng-content and need to project input element to it.
  • Emit an event in TextboxComponent when input element is inputted something.
  • Don't wanna make input element to have more attributes, e.g. <input type="text" (input)="event()">.

I was writing code, but cannot find a solution...

# input.directive.ts
@Directive({ selector: 'input', ... })
export class InputDirective {
  ngOnChanges(): void {
    // ngOnChanges() can observe only properties defined from @Input Decorator...
  }
}

# textbox.component.ts
@Component({ selector: 'textbox', ... })
export class TextboxComponent {
  @ContentChildren(InputDirective) inputs: QueryList<InputDirective>;
  ngAfterContentInit(): void {
    this.inputs.changes.subscribe((): void => {
      // QueryList has a changes property, it can observe changes when the component add/remove.
      // But cannot observe input changes...
    });
  }
}
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
Yohsuke Inoda
  • 521
  • 1
  • 6
  • 20
  • Why don't you want to use an output binding, like ? How can you be sure that other devs (or even you) only add input elements to the TextboxComponent's markup? – rrjohnson85 Apr 23 '16 at 01:26
  • Because I should append an output binding to `input` element and it's verbose if I wanna use it. But I can use `host: { (input): 'event()' }` property in InputDirective instead. The biggest problem is that TextboxComponent using `ng-content` can detect when InputDirective were inputted anything. – Yohsuke Inoda Apr 23 '16 at 06:40

5 Answers5

11

The input event is bubbling and can be listened on the parent component

<div class="textbox-wrapper" (input)="inputChanged($event)">
  <ng-content></ng-content>
</div> 

Plunker example

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
6

In ngAfterViewInit(), find the element(s) of interest, then imperatively add event listener(s). The code below assumes only one input:

@Component({
    selector: 'textbox',
    template: `<h3>textbox value: {{inputValue}}</h3>
      <div class="textbox-wrapper">
        <ng-content></ng-content>
      </div>`,
})
export class TextboxComp {
  inputValue:string;
  removeListenerFunc: Function;
  constructor(private _elRef:ElementRef, private _renderer:Renderer) {}
  ngAfterContentInit() {
    let inputElement = this._elRef.nativeElement.querySelector('input');
    this.removeListenerFunc = this._renderer.listen(
      inputElement, 'input', 
      event => this.inputValue = event.target.value)
  }
  ngOnDestroy() {
    this.removeListenerFunc();
  }
}

Plunker

This answer is essentially an imperative approach, in contrast to Günter's declarative approach. This approach may be easier to extend if you have multiple inputs.


There doesn't seem to be a way to use @ContentChild() (or @ContentChildren()) to find DOM elements in the user-supplied template (i.e, the ng-content content)... something like @ContentChild(input) doesn't seem to exist. Hence the reason I use querySelector().

In this blog post, http://angularjs.blogspot.co.at/2016/04/5-rookie-mistakes-to-avoid-with-angular.html, Kara suggests defining a Directive (say InputItem) with an input selector and then using @ContentChildren(InputItem) inputs: QueryList<InputItem>;. Then we don't need to use querySelector(). However, I don't particularly like this approach because the user of the TextboxComponent has to somehow know to also include InputItem in the directives array (I guess some component documentation could solve the problem, but I'm still not a fan). Here's a plunker for this approach.

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
1

You could use the following scenario, wich has hostbinding property on input directive

input.directive.ts

import {Directive, HostBinding} from 'angular2/core';
import {Observer} from 'rxjs/Observer';
import {Observable} from 'rxjs/Observable';

@Directive({
  selector: 'input',
  host: {'(input)': 'onInput($event)'}
})
export class InputDirective {
  inputChange$: Observable<string>;
  private _observer: Observer<any>;
  constructor() {
    this.inputChange$ = new Observable(observer => this._observer = observer);
  }
  onInput(event) {
    this._observer.next(event.target.value);
  }
}

And then your TextBoxComponent will subscribe on Observable object that defined above in InputDirective class.

textbox.component.ts

import {Component, ContentChildren,QueryList} from 'angular2/core';
import {InputDirective} from './input.directive';
@Component({
  selector: 'textbox',
  template: `
    <div class="textbox-wrapper">
      <ng-content></ng-content>
      <div *ngFor="#change of changes">
        {{change}}
      </div>
    </div>
  `
})
export class TextboxComponent {
  private changes: Array<string> = [];
  @ContentChildren(InputDirective) inputs: QueryList<InputDirective>;

  onChange(value, index) {
    this.changes.push(`input${index}: ${value}`);
  }

  ngAfterContentInit(): void {
    this.inputs.toArray().forEach((input, index) => {
      input.inputChange$.subscribe(value => this.onChange(value, index + 1));
    });
  }
}

Here's plunker sample

yurzui
  • 205,937
  • 32
  • 433
  • 399
1

You can use Angular CDK observers https://material.angular.io/cdk/observers/api

import this module in your module

import { ObserversModule } from '@angular/cdk/observers';

then use in ng-content's parent element

<div class="projected-content-wrapper (cdkObserveContent)="contentChanged()">
 <ng-content></ng-content>
</div>
Rakhat
  • 4,783
  • 4
  • 40
  • 50
0

You could add an ngControl on your input element.

<form>
  <textbox>
    <input ngControl="test"/>
  </textbox>
</form>

This way you would be able to use NgControl within ContentChild. It tiges access to the valueChanges property you can register on to be notified of updates.

@Component({ selector: 'textbox', ... })
export class TextboxComponent {
  @ContentChild(NgControl) input: NgControl;
  ngAfterContentInit(): void {
    this.input.control.valueChanges.subscribe((): void => {
      (...)
    });
  }
}
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • Thank you. I cannot get expected to because of using `ngAfterContentInit`. I got emitted event when using `ngAfterViewChecked`, but it executed many times... – Yohsuke Inoda Apr 23 '16 at 13:08