Projizieren von Inhalten in Angular oder verlorene ng-Inhaltsdokumentation

Wenn sie Angular lernen, übersehen sie oft ein Konzept wie die Projektion von Inhalten oder schenken ihnen nicht genügend Aufmerksamkeit. Dies ist ein sehr leistungsfähiges Tool zum Erstellen flexibler und wiederverwendbarer Komponenten. In der Dokumentation werden jedoch nur einige Absätze im Abschnitt Lifecycle-Hooks erwähnt . Versuchen wir, diese Lücke zu schließen.



Projizieren von Inhalten mit ng-content


Die Inhaltsprojektion ist eine Möglichkeit, HTML-Inhalte von außerhalb einer Komponente zu importieren und an einer bestimmten Stelle in eine Komponentenvorlage einzufügen. (kostenlose Übersetzung der Dokumentation)
Die Definition ist ziemlich kompliziert, aber tatsächlich ist alles viel einfacher. Wir haben eine Art Komponente, und alles, was sich zwischen den öffnenden und schließenden Tags befindet, ist Inhalt.

<app-parent>
    <!-- content -->
    I'm content!
    <!-- content -->
</app-parent>

Mit Angular können Sie beliebigen HTML-Code (Inhalt) mithilfe eines Elements in die Vorlage dieser Komponente einbetten ng-content.

Versuchen wir anhand eines Beispiels herauszufinden, warum dies erforderlich ist und wie es funktioniert. Angenommen, wir haben eine einfache Schaltflächenkomponente. Den Text dieser Schaltfläche geben wir an die Vorlage weiter input property.

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: '<button>{{text}}</button>'
})
export class ButtonComponent {
  @Input() text: string;
}

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button [text]="'Button'"></app-button>`,
})
export class AppComponent {
}

Es scheint gut auszusehen. Aber plötzlich mussten wir dem Text für einige Schaltflächen ein Symbol hinzufügen. Wir haben bereits eine Icon-Komponente. Sie müssen es nur zur Schaltflächenvorlage hinzufügen, eine Direktive anhängen und eine ngIfweitere input propertyfür die dynamische Anzeige des Symbols schreiben .

// icon.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: '☻',
})
export class IconComponent {
}

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <app-icon *ngIf="showIcon"></app-icon>
               {{text}}
             </button>`,
})
export class ButtonComponent {
  @Input() text: string;
  @Input() showIcon = true;
}

Alles arbeitet. Aber was passiert, wenn Sie die Position des Symbols relativ zum Text ändern müssen? Oder einen weiteren neuen Artikel hinzufügen? Sie müssen vorhandenen Code bearbeiten, neue Eigenschaften hinzufügen usw.

All dies kann mit vermieden werden ng-content. Es kann als Platzhalter für Inhalte betrachtet werden. Es zeigt alles an, was Sie zwischen die öffnenden und schließenden Tags der Komponente setzen.

// button.component.ts
import {Component} from '@angular/core';

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

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <app-icon></app-icon>
               Button
             </app-button>`,
})
export class AppComponent {
}

Stackblitz-Code

Wenn wir nun eine Symbolschaltfläche benötigen, setzen wir einfach die Symbolkomponente zwischen die Schaltflächen-Tags. Sie können alles und was auch immer hinzufügen. Ist das nicht der Himmel? Unsere Knopfkomponente ist flexibel und schön geworden.

Welche Rolle spielt das select-Attribut für ng-content?


Manchmal müssen wir einen Inhalt an einer bestimmten Stelle relativ zum Rest des Inhalts anordnen. In diesem Fall können wir ein Attribut verwenden select, das einen Selektor akzeptiert ( .some-class, some-tag, [some-attr]).

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
               <ng-content></ng-content>
               <div>
                 <ng-content select="app-icon"></ng-content>
               </div>
             </button>`,
})
export class ButtonComponent {
}

Stackblitz-Code

Jetzt wird das Symbol immer unten angezeigt, unabhängig vom Rest des Inhalts. Perfecto!

Was ist ngProjectAs?


Das selecty- Attribut verarbeitet ng-contentTags, die sich auf der ersten Verschachtelungsebene der übergeordneten Komponente befinden. Aber was passiert, wenn wir die Verschachtelungsebene für die Symbolkomponente erhöhen, indem wir sie in ein Tag einschließen?

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <ng-container>
                 <app-icon></app-icon>
               </ng-container>
               Button
             </app-button>`
})
export class AppComponent {}

Wir werden sehen, dass es selectnicht funktioniert, als ob es überhaupt nicht existiert. Dies liegt daran, dass <ng-content select="...">nur auf der ersten Ebene der Verschachtelung des Inhalts der Eltern gesucht wird. Es gibt ein Attribut, um dieses Problem zu lösen ngProjectAs. Es nimmt den Selektor auf und "maskiert" den gesamten DOM-Knoten darunter.

// app.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-root',
  template: `<app-button>
               <ng-container ngProjectAs="app-icon">
                 <app-icon></app-icon>
               </ng-container>
               Button
             </app-button>`
})
export class AppComponent {}

Stackblitz-Code

Fall * ngIf + ng-Inhalt


Lassen Sie uns einen weiteren interessanten Fall untersuchen. Angenommen, wir müssen auf die Schaltfläche zum Ausblenden / Anzeigen des Symbols klicken. Fügen Sie der Schaltflächenkomponentenklasse, die für die Anzeige des Symbols verantwortlich ist, eine boolesche Eigenschaft hinzu, ändern Sie sie, indem Sie auf die Schaltfläche klicken und sie aufhängen ngIf.

// button.component.ts
import {Component, Input} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button (click)="toggleIcon()">
               <ng-content></ng-content>
               <div *ngIf="showIcon">
                 <ng-content select="app-icon"></ng-content>
               </div>
             </button>`,
})
export class ButtonComponent {
  showIcon = true;

  toggleIcon() {
    this.showIcon = !this.showIcon;
  }
}

Das Symbol wird ausgeblendet / durch Klicken angezeigt. Fein! Fügen wir jedoch einige Protokolle für Hooks OnInitund OnDestroyfür die Symbolkomponente hinzu. Es ist bekannt, dass eine Direktive, ngIfwenn eine Bedingung geändert wird, ein Element vollständig entfernt / erstellt und OnDestroy/ OnInitjedes Mal entsprechend funktionieren muss.

// icon.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-icon',
  template: '☻',
})
export class IconComponent implements OnInit, OnDestroy {
  ngOnInit() {
    console.log('app-icon init');
  }

  ngOnDestroy() {
    console.log('app-icon destroy')
  }
}

Code auf Stackblitz

Ein paar Mal klicken wir auf die Schaltfläche, stellen sicher, dass das Symbol verschwindet und dann angezeigt wird. Dann gehen wir zur Entwicklerkonsole in der Hoffnung, unsere begehrten Protokolle zu sehen, aber ... das sind sie nicht!

Es gibt nur ein Protokoll zum Erstellen einer Komponente. Es stellt sich heraus, dass unsere Symbolkomponente nie gelöscht, sondern einfach ausgeblendet wird. Warum passiert dies?

ng-content erstellt keine neuen Inhalte, sondern projiziert einfach vorhandene. Daher ist die Komponente, in der der Inhalt deklariert ist, für das Erstellen und Löschen verantwortlich. Für mich war es ein völlig nicht offensichtlicher Moment. Lassen Sie uns unsere Lösung so reparieren, dass sie zunächst wie erwartet funktioniert.

// button.component.ts
import {Component} from '@angular/core';

@Component({
  selector: 'app-button',
  template: `<button>
                <ng-content></ng-content>
                <ng-content select="app-icon"></ng-content>
             </button>`,
})
export class ButtonComponent {
}

// app.component.ts
import { Component } from "@angular/core";

@Component({
  selector: 'app-root',
  template: `<app-button (click)="toggleIcon()">
              <div *ngIf="showIcon" ngProjectAs="app-icon">
                <app-icon></app-icon>
              </div>
              Button
            </app-button>`,
})
export class AppComponent {
  showIcon = true;

  toggleIcon() {
    this.showIcon = !this.showIcon;
  }
}

Code auf Stackblitz

Nachdem wir die Protokolle geöffnet haben, können wir sehen, dass die Komponente des Symbols wie gewünscht erstellt und gelöscht wird.

Anstelle einer Schlussfolgerung


Ich hoffe, dieser Artikel hat Ihnen ein bisschen bei der Projektion von Inhalten in Angular geholfen.
Es ist für mich kategorisch unverständlich, warum die offizielle Dokumentation dieses Thema ignorierte. Das Angular-Repository hängt das Problem seit 2017 sogar daran. Anscheinend hat das Angular-Team wichtigere Dinge zu tun.

All Articles