Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to observe input element changes in ng-content

Tags:

angular

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...
    });
  }
}
like image 991
Yohsuke Inoda Avatar asked Apr 23 '16 01:04

Yohsuke Inoda


People also ask

How do you style content that was projected using NG-content?

If you want to style the projected content within <ng-content>, you can do so using :host and ::ng-deep to apply styling to all nested elements within the <contact> component.

How do you use Ngcontent?

The ng-content is used when we want to insert the content dynamically inside the component that helps to increase component reusability. Using ng-content we can pass content inside the component selector and when angular parses that content that appears at the place of ng-content.

Can we style ng-content?

ng-content is an important concept to create reusable and flexible components. With the help of ng-content content can be projected by a parent component into a predefined slot. In some cases, you want to apply some styles to the projected content.


4 Answers

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

like image 142
Günter Zöchbauer Avatar answered Oct 19 '22 23:10

Günter Zöchbauer


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>
like image 36
Rakhat Avatar answered Oct 19 '22 22:10

Rakhat


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.

like image 45
Mark Rajcok Avatar answered Oct 19 '22 23:10

Mark Rajcok


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

like image 21
yurzui Avatar answered Oct 19 '22 23:10

yurzui