How to call parent component\'s function when child component observed input changes?
The below is HTML structure.
# app.comopnent.html
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>
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
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.
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 => {
(...)
});
}
}
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