21

Is there a way to detect changes in ng-content?

@Component({
    selector: 'example',
    template: `<ng-content></ng-content>`
})
export class Example {}

@Component({
    selector: 'test',
    template: `
        <example>
            <div *ngFor="let el of elements">{{el}}</div>
        </example>`
})
export class Test {
    elements = [1, 2, 3];

    ngOnInit() {
        setInterval(() => this.elements[0] += 10, 3000);
    }
}

I would like to get some information in Example class when my ng-content will change.

Here is plunker

George
  • 6,630
  • 2
  • 29
  • 36
Stwosch
  • 602
  • 1
  • 7
  • 20
  • Your plunker is working fine for me. I guess I'm not sure what you're looking for. – SimplyComplexable Nov 15 '17 at 17:32
  • I'm looking for a way to detect changes each time when my ng-content will change. – Stwosch Nov 16 '17 at 10:35
  • Related/duplicate - https://stackoverflow.com/questions/42962394/angular-2-how-to-detect-changes-in-an-array-input-property ... Your `elements` array always points to the same object in memory, regardless whether its contents change, so you cannot just use `onChange()`. Use `IterableDiffers()` as explained in the linked answer. – mc01 Mar 14 '18 at 21:15
  • @mc01 totally unrelated to the question you linked to. Here we're talking about changes to ng-content, not inputs. – AsGoodAsItGets Oct 16 '20 at 09:54

6 Answers6

23

The easiest solution is to use the cdkObserveContent directive. First, you must add to the imports of the module that owns the component ObserversModule

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

@NgModule({
  imports: [
    /* ...other modules you use... */
    ObserversModule
  ],

/* ... */
})
export class MyModule { }

Then in your component's template:

<div (cdkObserveContent)="onContentChange($event)">
  <ng-content></ng-content>
</div>

The event will trigger each time the content inside changes somehow and the value passed to the function is an array with all the details about what changed. The value of the data that has changed can be found in target.data.

In your component.ts:

onContentChange(changes: MutationRecord[]) {
   // logs everything that changed
   changes.forEach(change => console.log(change.target.data));
}

Also, you can use as a service that gives you an Observable<MutationRecord[]>

Dylanbob211
  • 1,064
  • 1
  • 10
  • 15
8

My solution for a project is to monitor the content's innerHTML. This example is from a tooltip that shows template content. I don't really like this solution, and would be happy to have a different/better one offered. This will emit whenever there are changes.

/// tooltip.component.html
<div class="tooltip" [class.hidden]="hidden">
    <div #content>
        <ng-content></ng-content>
    </div>
</div>

 

///tooltip.component.js
import { AfterContentChecked, AfterContentInit, Component, ElementRef, EventEmitter, Output, ViewChild } from "@angular/core";

@Component({
    selector: "example",
    templateUrl: "./tooltip.component.html",
    styleUrls: ["./tooltip.component.scss"]
})
export class TooltipComponent implements AfterContentInit, AfterContentChecked {    
    @ViewChild("content") contentWrapper: ElementRef;
    content = "";
    @Output() public readonly contentChanged = new EventEmitter<string>();

    ngAfterContentInit(): void {
        this.content = this.contentWrapper.nativeElement.innerHTML;
        this.contentChanged.emit(this.content);
    }

    ngAfterContentChecked(): void {
        const c = this.contentWrapper.nativeElement.innerHTML;
        if (c !== this.content) {
            this.content = c;
            this.contentChanged.emit(this.content);
        }
    }
}
Will Shaver
  • 12,471
  • 5
  • 49
  • 64
6

To detect changes in ng-content assuming you're adding a ItemComponent.

The code would be:

import { QueryList, ContentChildren, AfterViewInit } from '@angular/core';

export class TestComponent implements AfterViewInit  {

    @ContentChildren(ItemComponent)
    public itemList:QueryList<ItemComponent>;


    public ngAfterViewInit() : void {
        this.itemList.changes.subscribe(() => {
            // put your logi here
        });
    }

}

Notes:
According the ContentChildren documentation you have to wait ngAfterViewInit to access the QueryList so you can not use it on ngOnInit.

See the QueryList documentation to see all the other properties that this class provides you.

borracciaBlu
  • 4,017
  • 3
  • 33
  • 41
2

Use AfterContentChecked interface:

constructor(private cd: ChangeDetectorRef) { 
}
ngAfterContentChecked():void{
  this.cd.markForCheck();
}

for more information take a look at https://angular.io/api/core/AfterContentChecked

Eqbal Sajadi
  • 97
  • 1
  • 4
0

I've come to a solution but I think this might not fill well for everyone needs. First of all treat nested components childs (after all, ng-content is nothing more than that) is very easy on React world and I think that it would be very nice to have a more robust solution for Angular too.

My solution is based on Material Angular Icon, the code you'll find here: https://github.com/angular/material2/blob/master/src/lib/icon/icon.ts

Following MatIcon approach, I wish to create an icon component where the name of desired icon is passed by ng-content like arrow-top.

Therefore I wish to observe whenever ng-content changes so that I can update the span classname.

The solutions is simply linking the data present into ng-content to an @Input followed by a getter.

Thus, whenever the ng-content changes Angular will update its component input too.

Hope this looks clear.

@Component({
 selector: 'fa-icons',
 template: `
 <span #templateRef class="template">
  <ng-content></ng-content>
 </span>

 <span [className]="iconName" [ngStyle]="ngStyle">
 </span>
 `,
 styles: [`
   :host > span.template {
     display: none;
   }
 `]
})
export class FaIconsComponent {
 @ViewChild('templateRef') templateRef: ElementRef;
 @Input() name: String;
 @Input() ngStyle: StyleSheetList;
 @Input()
 get iconName() {
   const text = this.templateRef.nativeElement.textContent.trim();
   return `fa-${text}`;
  }
}
-1

If you are trying to complete something every time bound data updates you can use the ngOnChanges() lifecycle hook. Check out the docs, for more info.

SimplyComplexable
  • 1,116
  • 11
  • 19