39 Component Deep Dive

39.1 Von Modulen zu Standalone Components

In älteren Angular-Versionen (vor Angular 14) war das NgModule-Konzept das zentrale Organisationsprinzip einer Angular-Anwendung. Module dienten als Container, die zusammengehörige Komponenten, Direktiven, Pipes und Services gruppierten. Diese Herangehensweise wurde aus gutem Grund entwickelt - sie ermöglichte effizientes Lazy Loading und half, die Anwendung zu strukturieren.

Mit der Einführung von Standalone Components in Angular 14 und deren vollständiger Unterstützung in Angular 15 hat sich dieser Ansatz grundlegend verändert. Standalone Components repräsentieren einen Paradigmenwechsel in der Art und Weise, wie Angular-Anwendungen strukturiert werden.

39.1.1 Die Evolution: Von NgModule zu Standalone

Betrachten wir die traditionelle modulbasierte Herangehensweise:

// feature.module.ts
@NgModule({
  declarations: [
    FeatureComponent,
    HelperDirective,
    FormatPipe
  ],
  imports: [
    CommonModule,
    SharedModule
  ],
  providers: [
    FeatureService
  ],
  exports: [
    FeatureComponent
  ]
})
export class FeatureModule { }

// feature.component.ts
@Component({
  selector: 'app-feature',
  templateUrl: './feature.component.html'
})
export class FeatureComponent { }

In diesem Modell musste jede Komponente in einem Modul deklariert werden. Das Modul definierte dann, welche anderen Module importiert werden, welche Services als Provider bereitgestellt werden und welche Komponenten nach außen exportiert werden sollten.

Die neue Standalone-Herangehensweise sieht deutlich anders aus:

// feature.component.ts
@Component({
  selector: 'app-feature',
  templateUrl: './feature.component.html',
  standalone: true,
  imports: [
    CommonModule,
    SharedComponent,
    HelperDirective,
    FormatPipe
  ],
  providers: [
    FeatureService
  ]
})
export class FeatureComponent { }

Der Unterschied ist signifikant: Die Komponente selbst deklariert nun alle ihre Abhängigkeiten direkt, ohne den Umweg über ein umgebendes Modul.

39.2 Der Component Decorator im Detail

Der @Component-Decorator ist das Herzstück jeder Angular-Komponente. Mit der Einführung von Standalone Components haben sich einige wichtige Parameter hinzugesellt:

@Component({
  // Grundlegende Eigenschaften
  selector: 'app-example',            // Der HTML-Selector zum Einbinden der Komponente
  templateUrl: './example.component.html', // Pfad zum HTML-Template
  styleUrls: ['./example.component.css'],  // Pfade zu CSS-Dateien
  
  // Standalone-spezifische Eigenschaften
  standalone: true,                   // Markiert die Komponente als standalone
  imports: [...],                     // Importierte Abhängigkeiten
  
  // Dependency Injection
  providers: [...],                   // Lokale Service-Provider
  viewProviders: [...],               // Provider, die nur für die View dieser Komponente gelten
  
  // Weitere wichtige Eigenschaften
  changeDetection: ChangeDetectionStrategy.OnPush, // Change-Detection-Strategie
  encapsulation: ViewEncapsulation.Emulated        // Style-Encapsulation
})
export class ExampleComponent { }

39.2.1 Fokus: standalone und imports

39.2.1.1 Der standalone: true Parameter

Die standalone: true Eigenschaft ist der Schlüssel zur modulfreien Angular-Welt. Sie signalisiert Angular:

  1. Diese Komponente benötigt kein umgebendes NgModule
  2. Die Komponente verwaltet ihre eigenen Abhängigkeiten
  3. Sie kann direkt in imports anderer Standalone-Komponenten oder in der bootstrapComponent verwendet werden
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  standalone: true,
  imports: [
    // Hier werden alle benötigten Abhängigkeiten importiert
  ]
})
export class AppComponent { }

39.2.1.2 Der imports Parameter

Der imports Parameter ist das Herzstück des Standalone-Konzepts. Er definiert, welche anderen Komponenten, Direktiven oder Pipes innerhalb dieser Komponente verwendet werden können:

@Component({
  selector: 'app-feature',
  templateUrl: './feature.component.html',
  standalone: true,
  imports: [
    // Angular-eigene Module
    CommonModule,    // Für ngIf, ngFor etc.
    RouterModule,    // Für Router-Direktiven
    ReactiveFormsModule, // Für Formulare
    
    // Andere Standalone-Komponenten
    HeaderComponent,
    FooterComponent,
    
    // Standalone-Direktiven
    HighlightDirective,
    
    // Standalone-Pipes
    FormatDatePipe
  ]
})
export class FeatureComponent { }

Die imports definieren den “Sichtbarkeitsbereich” innerhalb des Templates. Nur was hier importiert wurde, kann im Template verwendet werden.

39.2.2 Dependency Injection und Provider

39.2.2.1 Was sind Provider?

Provider sind ein zentrales Konzept in Angular’s Dependency Injection (DI) System. Sie teilen Angular mit, wie Instanzen bestimmter Klassen erzeugt werden sollen, wenn sie angefordert werden.

Der häufigste Provider-Typ ist der ClassProvider, der eine Klasse direkt bereitstellt:

providers: [
  UserService, // Kurzform für { provide: UserService, useClass: UserService }
]

Es gibt aber auch andere Arten von Providern: - ValueProvider: Stellt einen festen Wert bereit - FactoryProvider: Verwendet eine Fabrikfunktion zur Erstellung - ExistingProvider: Leitet auf einen existierenden Provider weiter

39.2.2.2 Provider in Standalone Components

In einer Standalone Component können Provider direkt im providers-Array definiert werden:

@Component({
  selector: 'app-feature',
  templateUrl: './feature.component.html',
  standalone: true,
  providers: [
    FeatureService,
    { provide: CONFIG_TOKEN, useValue: { apiEndpoint: '/api' } }
  ]
})
export class FeatureComponent { }

Diese Provider sind in der gesamten Komponenten-Hierarchie unterhalb dieser Komponente verfügbar - ein wichtiger Punkt für das Verständnis des “Provider-Baums” in Angular.

39.2.2.3 Implizite vs. Explizite Provider

Ein entscheidender Unterschied zwischen verschiedenen Angular-Elementen:

Implizite Provider: - Komponenten - Direktiven - Pipes

Diese werden automatisch (implizit) im Dependency Injection System registriert, wenn sie in imports aufgeführt sind.

Explizite Provider: - Services - Tokens - Andere Werte

Diese müssen explizit im providers-Array einer Komponente oder der Bootstrap-Komponente registriert werden.

// Implizite Provider durch Import
@Component({
  standalone: true,
  imports: [
    UserComponent,       // Komponente wird implizit provided
    HighlightDirective,  // Direktive wird implizit provided
    FormatPipe           // Pipe wird implizit provided
  ]
})
export class AppComponent { }

// Explizite Provider
@Component({
  standalone: true,
  providers: [
    UserService,         // Service muss explizit provided werden
    { provide: API_URL, useValue: 'https://api.example.com' } // Token muss explizit provided werden
  ]
})
export class AppComponent { }

39.3 Der Provider-Baum: Wie Angular die Dependency Injection organisiert

Angular’s Dependency Injection System funktioniert hierarchisch, ähnlich wie die Komponenten-Hierarchie selbst. Dieses Hierarchie-Prinzip ist entscheidend für das Verständnis, wie Provider und Services in Angular funktionieren.

39.3.1 Der Aufbau des Provider-Baums

  1. Root-Ebene: Alles beginnt mit der Bootstrap-Komponente
  2. Komponenten-Ebene: Jede Komponente kann eigene Provider definieren
  3. Element-Injectors: Jede Komponenteninstanz hat ihren eigenen Injector

// bootstrap.ts
bootstrapApplication(AppComponent, {
  providers: [
    // Root-Provider, global für die gesamte Anwendung
    { provide: APP_CONFIG, useValue: { production: environment.production } },
    provideRouter(routes),
    provideAnimations()
  ]
});

// app.component.ts
@Component({
  standalone: true,
  providers: [
    // Diese Provider sind für AppComponent und alle Kind-Komponenten verfügbar
    UserService
  ]
})
export class AppComponent { }

// feature.component.ts
@Component({
  standalone: true,
  providers: [
    // Diese Provider sind nur für FeatureComponent und deren Kind-Komponenten verfügbar
    FeatureService
  ]
})
export class FeatureComponent { }

Wenn eine Komponente einen Service anfordert, sucht Angular: 1. Zuerst im eigenen Element-Injector 2. Dann im Element-Injector der Eltern-Komponente 3. So weiter nach oben bis zur Root-Komponente 4. Schließlich im Root-Injector (definiert beim Bootstrap)

Dies erklärt, warum Services “singleton” sein können oder mehrfach instanziert, je nachdem, wo sie bereitgestellt werden.

39.3.2 Der Vergleich zu Spring Boot

Diese Vorgehensweise ist in gewisser Weise mit Spring Boot vergleichbar:

Spring Boot: - Verwendet Annotation-Scanning, um Komponenten zu finden - @Component, @Service, @Controller etc. markieren Klassen für DI - Spring erstellt einen Anwendungskontext mit Beans - Hierarchische Bean-Container ermöglichen unterschiedliche Sichtbarkeiten

Angular Standalone: - Verwendet explizite Imports statt Scanning - Komponenten, Direktiven und Pipes werden automatisch registriert - Services müssen explizit als Provider definiert werden - Hierarchischer Injector-Baum mit definierten Sichtbarkeitsbereichen

Der Hauptunterschied liegt im “expliziten” Ansatz von Angular gegenüber dem “impliziten” Scanning-Ansatz von Spring Boot. Angular erfordert, dass Abhängigkeiten explizit importiert werden, während Spring Boot Komponenten durch Classpath-Scanning findet.

39.4 Praktisches Beispiel: Aufbau einer modularen Anwendung mit Standalone Components

Betrachten wir ein vollständiges Beispiel einer Feature-Komponente mit Standalone-Ansatz:

// user.service.ts
@Injectable()
export class UserService {
  getUsers() {
    return of([
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]);
  }
}

// highlight.directive.ts
@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input() highlightColor = 'yellow';
  
  constructor(private el: ElementRef) {}
  
  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.highlightColor);
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(null);
  }
  
  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

// format-date.pipe.ts
@Pipe({
  name: 'formatDate',
  standalone: true
})
export class FormatDatePipe implements PipeTransform {
  transform(value: Date): string {
    return new Intl.DateTimeFormat('de-DE').format(value);
  }
}

// user-list.component.ts
@Component({
  selector: 'app-user-list',
  template: `
    <h2 appHighlight>Benutzerliste</h2>
    <p>Letztes Update: {{ lastUpdate | formatDate }}</p>
    <ul>
      <li *ngFor="let user of users">{{ user.name }}</li>
    </ul>
  `,
  standalone: true,
  imports: [
    CommonModule,        // Für ngFor
    HighlightDirective,  // Unsere Standalone-Direktive
    FormatDatePipe       // Unsere Standalone-Pipe
  ],
  providers: [
    UserService          // Der Service wird explizit bereitgestellt
  ]
})
export class UserListComponent implements OnInit {
  users: any[] = [];
  lastUpdate = new Date();
  
  constructor(private userService: UserService) {}
  
  ngOnInit() {
    this.userService.getUsers().subscribe(users => {
      this.users = users;
    });
  }
}

// app.component.ts
@Component({
  selector: 'app-root',
  template: `
    <h1>Meine Anwendung</h1>
    <app-user-list></app-user-list>
  `,
  standalone: true,
  imports: [
    UserListComponent    // Wir importieren unsere Feature-Komponente
  ]
})
export class AppComponent {}

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    // Globale Provider hier
    provideRouter([
      { path: '', component: AppComponent }
    ])
  ]
});

In diesem Beispiel: 1. Wir haben eine vollständige Anwendung ohne ein einziges NgModule 2. Jede Komponente, Direktive und Pipe ist standalone 3. Die Abhängigkeiten werden explizit importiert, wo sie benötigt werden 4. Services werden explizit als Provider bereitgestellt 5. Die Anwendung beginnt mit bootstrapApplication anstelle von NgModule-Bootstrap

39.5 Gegenüberstellung: Modularer Ansatz vs. Standalone

Zum Abschluss eine direkte Gegenüberstellung beider Ansätze:

39.5.1 Modularer Ansatz (traditionell)

// user.module.ts
@NgModule({
  declarations: [
    UserListComponent,
    HighlightDirective,
    FormatDatePipe
  ],
  imports: [
    CommonModule
  ],
  providers: [
    UserService
  ],
  exports: [
    UserListComponent
  ]
})
export class UserModule { }

// app.module.ts
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    UserModule
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

39.5.2 Standalone Ansatz (modern)

// user-list.component.ts (und andere Komponenten)
@Component({
  selector: 'app-user-list',
  templateUrl: './user-list.component.html',
  standalone: true,
  imports: [
    CommonModule,
    HighlightDirective,
    FormatDatePipe
  ],
  providers: [
    UserService
  ]
})
export class UserListComponent { }

// app.component.ts
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  standalone: true,
  imports: [
    UserListComponent
  ]
})
export class AppComponent { }

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    // Globale Provider
  ]
});

39.6 Vorteile des Standalone-Ansatzes

  1. Vereinfachte Code-Basis: Keine separaten Module mehr erforderlich.
  2. Verbesserte Entwicklererfahrung: Alle Abhängigkeiten sind dort deklariert, wo sie verwendet werden.
  3. Bessere Tree-Shaking-Möglichkeiten: Nur tatsächlich verwendete Komponenten und Dienste werden gebündelt.
  4. Leichtere Wartbarkeit: Der Import-Pfad ist klarer und direkter nachvollziehbar.
  5. Reduzierte Boilerplate: Weniger Code für die gleiche Funktionalität.

39.7 Herausforderungen und Übergangsphase

Die Umstellung auf Standalone Components kann in bestehenden Projekten herausfordernd sein. Angular bietet daher Hilfsmittel für den Übergang:

// Einbinden von NgModule-basierten Komponenten in Standalone
@Component({
  standalone: true,
  imports: [
    // Import eines gesamten Moduls
    importProvidersFrom(LegacyModule)
  ]
})
export class NewComponent { }

// Umwandlung eines NgModule in Standalone-Komponenten
const providers = convertProviders(LegacyModule);