24

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

4
  • Your plunker is working fine for me. I guess I'm not sure what you're looking for. Commented Nov 15, 2017 at 17:32
  • I'm looking for a way to detect changes each time when my ng-content will change. Commented Nov 16, 2017 at 10:35
  • Related/duplicate - stackoverflow.com/questions/42962394/… ... 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. Commented Mar 14, 2018 at 21:15
  • @mc01 totally unrelated to the question you linked to. Here we're talking about changes to ng-content, not inputs. Commented Oct 16, 2020 at 9:54

7 Answers 7

28

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[]>

Sign up to request clarification or add additional context in comments.

1 Comment

I think this is the best solution in 2020. ngAfterContentChecked can fire too many times, it could hurt performance.
9

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); } } } 

1 Comment

This works but runs on every change anywhere in the project no matter where
8

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.

Comments

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

Comments

1

6 years later, it looks like these suggestions here are still valid.

For my edge case, a combination of these solutions worked out: I am just interested if in my custom component the developer used the default provided buttons, or custom buttons.

So I am using the code as mentioned before

(cdkObserveContent)="onContentChange($event)"

to get informed whenever the node changes and also the

@ViewChild('templateRef') templateRef: ElementRef;

to see actually, if the ng-content is used at all (contains anything). The benefit is, it even works with some attribute/content in the ng-content changes <3

Here comes a full example with signals and flow-control, ready for the PushStrategy (tested with angular18)

Together it looks like this in the .html part

<div class="my-wrapper"> @if (!numberOfCustomButtons()) { <span class="default-buttons"> <button (click)="cancelAction.emit()">Cancel</button> <button (click)="saveAction.emit()">Save</button> </span> } <span #customButtonsWrapper (cdkObserveContent)="onContentChange()" [class.hidden]="!numberOfCustomButtons()"> <ng-content/> </span> </div> 

Here comes the .ts part

@Component({ selector: "myButtonBar", templateUrl: "./myButtonBar.component.html", styleUrls: ["./myButtonBar.component.scss"], standalone: true, imports: [ ObserversModule, ... ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class MyButtonBarComponent { private myCustomButtonsWrapper: Signal<ElementRef> = viewChild<ElementRef>("customButtonsWrapper"); protected numberOfCustomButtons: WritableSignal<number> = signal<number>(0); cancelAction: OutputEmitterRef<void> = output<void>(); saveAction: OutputEmitterRef<void> = output<void>(); onContentChange(): void { this.numberOfCustomButtons.set(this.myCustomButtonsWrapper().nativeElement.children.length); } // Also initialize the default number of custom buttons to get rid of initialization issues, when custom buttons should be used ngOnInit(): void { this.numberOfCustomButtons.set(this.myCustomButtonsWrapper().nativeElement.children.length); } } 

As you don't wanted style issues in the default case, just hide the wrapper via display:none in your .scss/.css part

.hidden: { display: none;} 

Comments

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}`; } } 

Comments

-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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.