How-To-Anleitungen Angular: praktische Integrationen, Statusverwaltung und erweiterte Reactive Forms
Schritt-für-Schritt-Anleitungen gehören zu den nützlichsten Inhalten für Entwickler, da sie echte Probleme lösen: eine Bibliothek installieren, ein Projekt richtig konfigurieren, eine Statusverwaltungsstrategie wählen, Erstellen Sie komplexe Formulare und verhindern Sie schwer erkennbare Fehler.
In diesem Artikel sehen wir einen praktischen Weg, um besser mit dem modernen Angular zu arbeiten: Tailwind CSS-Integration, Verwendung von Chart.js, Animationen, Zustandsverwaltung mit Signals, NgRx und Akita, und Erstellen fortschrittlicher reaktionsfähiger Formulare mit synchronen und asynchronen Validierungen.
1. Angular
ProjektvoraussetzungenBevor Sie Bibliotheken integrieren oder komplexe Architekturen implementieren, beginnen Sie am besten mit einem sauberen Fundament. Wir werden ein modernes Angular-Projekt verwenden, vorzugsweise mit eigenständigen Komponenten, Reactive Forms und TypeScript.
Erstellen des Projekts
npm install -g @angular/cli
ng new angular-how-to-guide
cd angular-how-to-guide
ng serve
Wenn Sie an einem bestehenden Projekt arbeiten, stellen Sie zumindest sicher, dass Sie Folgendes haben:
- Angular wurde auf eine aktuelle Version aktualisiert.
- Node.js in einer Angular-kompatiblen Version.
- TypeScript korrekt konfiguriert.
- Eine klare Struktur für Komponenten, Dienstleistungen, Modelle und Geschäfte.
Empfohlene Struktur
src/
app/
core/
services/
guards/
interceptors/
shared/
components/
pipes/
validators/
features/
dashboard/
products/
profile/
state/
signals/
ngrx/
app.config.ts
app.routes.ts
Gerade wenn das Projekt wächst, hilft eine geordnete Struktur. Durch die Trennung von Core, Shared, Features und State können Sie verwirrende Abhängigkeiten und übermäßig große Komponenten vermeiden.
2. So integrieren Sie Tailwind CSS in Angular
Tailwind CSS ist eine Utility-First-Bibliothek, mit der Sie moderne Schnittstellen erstellen können, ohne große Mengen schreiben zu müssen von benutzerdefiniertem CSS. In Angular ist es sehr nützlich für Dashboards, Admin-Panels, Landingpages und wiederverwendbare Komponenten.
Installation
npm install tailwindcss @tailwindcss/postcss postcss --save-dev
Erstellen oder aktualisieren Sie die PostCSS-Konfigurationsdatei.
{
"plugins": {
"@tailwindcss/postcss": {}
}
}
Importieren Sie dann Tailwind in die globale Stildatei, zum Beispiel src/styles.css.
@import "tailwindcss";
Beispiel einer Winkelkomponente mit Rückenwind
import { Component } from '@angular/core';
@Component({
selector: 'app-pricing-card',
standalone: true,
template: `
<section class="mx-auto max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<p class="text-sm font-medium uppercase tracking-wide text-indigo-600">Piano Pro</p>
<h2 class="mt-2 text-3xl font-bold text-slate-900">€29/mese</h2>
<p class="mt-3 text-slate-600">
Ideale per team che vogliono creare applicazioni Angular più velocemente.
</p>
<ul class="mt-6 space-y-2 text-sm text-slate-700">
<li>Componenti riutilizzabili</li>
<li>Dashboard responsive</li>
<li>Supporto a form complessi</li>
</ul>
<button class="mt-6 w-full rounded-xl bg-indigo-600 px-4 py-3 font-semibold text-white hover:bg-indigo-700">
Inizia ora
</button>
</section>
`
})
export class PricingCardComponent {}
Best Practices mit Tailwind in Angular
- Vermeiden Sie riesige Vorlagen voller sich wiederholender Klassen.
- Extrahieren Sie wiederverwendbare Komponenten, wenn eine Benutzeroberfläche wiederholt wird.
- Verwenden Sie bedingte Klassen mit
[class.class-name]oder[ngClass]. - Definieren Sie konsistente Token-Designs für Farben, Abstände und Typografie.
- Benutzen Sie Rückenwind nicht als Ausrede, um die Barrierefreiheit zu ignorieren.
Beispiel mit bedingten Klassen
<button
type="button"
class="rounded-lg px-4 py-2 font-semibold transition"
[class.bg-green-600]="isActive"
[class.text-white]="isActive"
[class.bg-slate-200]="!isActive"
[class.text-slate-700]="!isActive">
Stato elemento
</button>
Diese Technik ist nützlich für Registerkarten, aktive Schaltflächen, Filter, ausgewählte Elemente und Validierungsstatus.
3. So integrieren Sie Chart.js in Angular
Chart.js ist eine weit verbreitete Bibliothek für Linien-, Balken-, Radar-, Donut- und statistische Dashboard-Diagramme. In Angular können Sie es direkt oder über spezielle Wrapper verwenden. Hier sehen wir eine direkte Integration, einfach und kontrollierbar.
Installation
npm install chart.js
Erstellen der Grafikkomponente
ng generate component features/dashboard/revenue-chart --standalone
TypeScript-Code der Komponente
import {
AfterViewInit,
Component,
ElementRef,
OnDestroy,
ViewChild
} from '@angular/core';
import {
Chart,
ChartConfiguration,
registerables
} from 'chart.js';
Chart.register(...registerables);
@Component({
selector: 'app-revenue-chart',
standalone: true,
template: `
<div class="h-80 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm">
<h2 class="mb-4 text-lg font-semibold text-slate-900">Ricavi mensili</h2>
<canvas #canvas></canvas>
</div>
`
})
export class RevenueChartComponent implements AfterViewInit, OnDestroy {
@ViewChild('canvas')
canvas!: ElementRef<HTMLCanvasElement>;
private chart?: Chart;
ngAfterViewInit(): void {
const config: ChartConfiguration = {
type: 'line',
data: {
labels: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu'],
datasets: [
{
label: 'Ricavi',
data: [12000, 15000, 14000, 21000, 26000, 31000],
borderColor: '#4f46e5',
backgroundColor: 'rgba(79, 70, 229, 0.15)',
tension: 0.35,
fill: true
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true
}
},
scales: {
y: {
beginAtZero: true
}
}
}
};
this.chart = new Chart(this.canvas.nativeElement, config);
}
ngOnDestroy(): void {
this.chart?.destroy();
}
}
Warum das Diagramm zerstören?
Wenn eine Angular-Komponente von der Seite entfernt wird, hinterlässt Chart.js möglicherweise Listener oder Objekte im Speicher.
Der Aufruf von destroy() in ngOnDestroy() vermeidet Speicherverluste, insbesondere in Dashboards mit vielen Diagrammen.
Daten dynamisch aktualisieren
updateChart(values: number[]): void {
if (!this.chart) {
return;
}
this.chart.data.datasets[0].data = values;
this.chart.update();
}
In einem realen Projekt stammen die Werte häufig von einem HTTP-Dienst, einem globalen Speicher oder einem Signal.
Praktische Ratschläge
- Verwenden Sie für jeden komplexen Graphen separate Komponenten.
- Chart.js nicht initialisieren, bevor die Leinwand verfügbar ist.
- Diagramm immer zerstören in
ngOnDestroy(). - Für SSR oder Angular Universal führen Sie Chart.js nur browserseitig aus.
- Normalisieren Sie die Daten, bevor Sie sie an das Diagramm übergeben.
4. So fügen Sie Animationen in Angular
hinzuAnimationen verbessern die Wahrnehmung der Benutzeroberfläche, müssen jedoch mit Bedacht eingesetzt werden. Das Ziel besteht nicht darin, die Seite zu „schmücken“, sondern dem Benutzer zu helfen, zu verstehen, was passiert: Öffnen von Panels, Erscheinen von Benachrichtigungen, Übergänge zwischen Zuständen und Feedback zu Formularen.
Installieren des Angular Animations-Pakets
npm install @angular/animations
Konfiguration in app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAnimations } from '@angular/platform-browser/animations';
export const appConfig: ApplicationConfig = {
providers: [
provideAnimations()
]
};
Beispiel einer Fade- und Slide-Animation
import { Component } from '@angular/core';
import {
animate,
style,
transition,
trigger
} from '@angular/animations';
@Component({
selector: 'app-notification',
standalone: true,
animations: [
trigger('fadeSlide', [
transition(':enter', [
style({
opacity: 0,
transform: 'translateY(12px)'
}),
animate(
'180ms ease-out',
style({
opacity: 1,
transform: 'translateY(0)'
})
)
]),
transition(':leave', [
animate(
'140ms ease-in',
style({
opacity: 0,
transform: 'translateY(12px)'
})
)
])
])
],
template: `
@if (visible) {
<div
@fadeSlide
class="rounded-xl border border-green-200 bg-green-50 p-4 text-green-800">
Operazione completata con successo.
</div>
}
<button
class="mt-4 rounded-lg bg-green-600 px-4 py-2 text-white"
type="button"
(click)="visible = !visible">
Mostra/Nascondi
</button>
`
})
export class NotificationComponent {
visible = true;
}
Wann sollte eine externe Bibliothek wie GSAP verwendet werden?
Angular Animations ist für viele Anwendungsschnittstellen ausreichend. Eine Bibliothek wie GSAP ist nützlich, wenn Sie komplexe Zeitleisten, koordinierte Animationen usw. benötigen. sehr dynamische Landingpages oder ausgefeilte Mikrointeraktionen.
npm install gsap
Wichtige Regel: Vermeiden Sie die Animation schwerer Eigenschaften wie width, height oder top
wenn Sie transform und opacity verwenden können, die im Allgemeinen effizienter sind.
5. Zustandsverwaltung in Angular: Signale, NgRx und Akita
State Management ist eines der wichtigsten Themen in Angular. Auf den ersten Blick mag ein Dienst mit einigen Variablen ausreichend erscheinen, aber wenn die App wächst, wird er notwendig Wählen Sie einen strukturierteren Ansatz.
Schneller Vergleich
| Lösung | Wann man es verwendet | Vorteile | Nachteile |
|---|---|---|---|
| Signale | Einfacher/mittlerer lokaler oder gemeinsamer Zustand | Moderne Syntax, einfache Reaktionsfähigkeit, weniger Boilerplate | Erfordert architektonische Disziplin bei großen Projekten |
| NgRx | Unternehmensanwendungen, komplexe Abläufe, Überprüfbarkeit | Vorhersehbare Muster, DevTools, Effekte, Selektoren | Mehr Boilerplate und Lernkurve |
| Akita | Leanerer Shop, Entity-Store, Teams wollen weniger Standardprodukte | Pragmatische API, bequeme Entitätsverwaltung | Ökosystem weniger zentral als NgRx |
Faustregel
- Verwenden Sie Signale für den lokalen Status, den UI-Status und kleine bis mittlere Funktionen.
- Verwenden Sie NgRx, wenn Sie über viele asynchrone Streams, Rollen, Berechtigungen, Caching und erweitertes Debugging verfügen.
- Verwenden Sie Akita, wenn Sie einen strukturierten, aber weniger ausführlichen Speicher als NgRx wünschen.
- Führen Sie keinen globalen Store ein, wenn das Problem mit einem einfachen Service gelöst werden kann.
6. Praktisches Tutorial: Speichern mit Winkelsignalen
Mit Signalen lassen sich Blindwerte auf einfache Weise darstellen. Sie eignen sich besonders gut zum Erstellen kleiner Feature-Stores, ohne externe Bibliotheken einführen zu müssen.
Lassen Sie uns einen Todo Store erstellen
import { Injectable, computed, signal } from '@angular/core';
export interface Todo {
id: number;
title: string;
done: boolean;
}
export type TodoFilter = 'all' | 'active' | 'done';
@Injectable({
providedIn: 'root'
})
export class TodoStore {
private readonly todosState = signal<Todo[]>([]);
readonly filter = signal<TodoFilter>('all');
readonly todos = this.todosState.asReadonly();
readonly activeCount = computed(() => {
return this.todosState().filter(todo => !todo.done).length;
});
readonly completedCount = computed(() => {
return this.todosState().filter(todo => todo.done).length;
});
readonly visibleTodos = computed(() => {
const filter = this.filter();
const todos = this.todosState();
if (filter === 'active') {
return todos.filter(todo => !todo.done);
}
if (filter === 'done') {
return todos.filter(todo => todo.done);
}
return todos;
});
addTodo(title: string): void {
const cleanTitle = title.trim();
if (!cleanTitle) {
return;
}
const todo: Todo = {
id: Date.now(),
title: cleanTitle,
done: false
};
this.todosState.update(current => [todo, ...current]);
}
toggleTodo(id: number): void {
this.todosState.update(current => {
return current.map(todo => {
if (todo.id !== id) {
return todo;
}
return {
...todo,
done: !todo.done
};
});
});
}
removeTodo(id: number): void {
this.todosState.update(current => {
return current.filter(todo => todo.id !== id);
});
}
clearCompleted(): void {
this.todosState.update(current => {
return current.filter(todo => !todo.done);
});
}
}
Komponente, die den Store nutzt
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { TodoStore } from './todo.store';
@Component({
selector: 'app-todos',
standalone: true,
imports: [FormsModule],
template: `
<section class="mx-auto max-w-2xl rounded-2xl bg-white p-6 shadow-sm">
<h2 class="text-2xl font-bold text-slate-900">Todo con Signals</h2>
<div class="mt-4 flex gap-2">
<input
class="flex-1 rounded-lg border border-slate-300 px-3 py-2"
placeholder="Nuova attività"
[(ngModel)]="title">
<button
type="button"
class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
(click)="add()">
Aggiungi
</button>
</div>
<div class="mt-4 flex gap-2">
<button type="button" (click)="store.filter.set('all')">Tutti</button>
<button type="button" (click)="store.filter.set('active')">Attivi</button>
<button type="button" (click)="store.filter.set('done')">Completati</button>
</div>
<ul class="mt-6 space-y-2">
@for (todo of store.visibleTodos(); track todo.id) {
<li class="flex items-center justify-between rounded-lg border border-slate-200 p-3">
<label class="flex items-center gap-2">
<input
type="checkbox"
[checked]="todo.done"
(change)="store.toggleTodo(todo.id)">
<span [class.line-through]="todo.done">
{{ todo.title }}
</span>
</label>
<button
type="button"
class="text-red-600"
(click)="store.removeTodo(todo.id)">
Elimina
</button>
</li>
}
</ul>
<p class="mt-4 text-sm text-slate-600">
Attivi: {{ store.activeCount() }} -
Completati: {{ store.completedCount() }}
</p>
</section>
`
})
export class TodosComponent {
readonly store = inject(TodoStore);
title = '';
add(): void {
this.store.addTodo(this.title);
this.title = '';
}
}
Vorteile des Signals-Ansatzes
- Der Code ist lesbar.
- Kein Handlungs-, Minderungs- oder Wirkungsbedarf für einfache Fälle.
- Mit
computed()abgeleitete Werte sind sehr praktisch. - Der Zustand bleibt im Dienst gekapselt.
- Es ist einfach, Store-Methoden zu testen.
Grenze zum Erinnern
Signale bedeuten nicht „automatische Architektur“. Wenn das Projekt stark wächst, müssen Sie noch Konventionen definieren: Benennung, Diensttrennung, Persistenz, Caching, Fehlerbehandlung und Kommunikation mit APIs.
7. Praktisches Tutorial: State Management mit NgRx
NgRx implementiert ein Redux-inspiriertes Muster. Es ist geeignet, wenn Sie einen vorhersehbaren, nachvollziehbaren und leicht zu debuggenden Zustand wünschen. Es wird häufig in Unternehmensanwendungen verwendet.
Installation
ng add @ngrx/store
ng add @ngrx/effects
ng add @ngrx/store-devtools
Modelldefinition
export interface Product {
id: number;
name: string;
price: number;
}
export interface ProductsState {
items: Product[];
loading: boolean;
error: string | null;
}
export const initialProductsState: ProductsState = {
items: [],
loading: false,
error: null
};
Aktionen
import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { Product } from './product.model';
export const ProductsActions = createActionGroup({
source: 'Products',
events: {
'Load Products': emptyProps(),
'Load Products Success': props<{ products: Product[] }>(),
'Load Products Failure': props<{ error: string }>()
}
});
Reduzierer
import { createReducer, on } from '@ngrx/store';
import { ProductsActions } from './products.actions';
import { initialProductsState } from './product.model';
export const productsReducer = createReducer(
initialProductsState,
on(ProductsActions.loadProducts, state => ({
...state,
loading: true,
error: null
})),
on(ProductsActions.loadProductsSuccess, (state, { products }) => ({
...state,
items: products,
loading: false
})),
on(ProductsActions.loadProductsFailure, (state, { error }) => ({
...state,
loading: false,
error
}))
);
Selektoren
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { ProductsState } from './product.model';
export const selectProductsState =
createFeatureSelector<ProductsState>('products');
export const selectProducts =
createSelector(
selectProductsState,
state => state.items
);
export const selectProductsLoading =
createSelector(
selectProductsState,
state => state.loading
);
export const selectProductsError =
createSelector(
selectProductsState,
state => state.error
);
Effekte
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, of, switchMap } from 'rxjs';
import { ProductsActions } from './products.actions';
import { ProductsApiService } from './products-api.service';
@Injectable()
export class ProductsEffects {
private readonly actions$ = inject(Actions);
private readonly api = inject(ProductsApiService);
loadProducts$ = createEffect(() => {
return this.actions$.pipe(
ofType(ProductsActions.loadProducts),
switchMap(() => {
return this.api.getProducts().pipe(
map(products => ProductsActions.loadProductsSuccess({ products })),
catchError(() => {
return of(
ProductsActions.loadProductsFailure({
error: 'Errore durante il caricamento dei prodotti.'
})
);
})
);
})
);
});
}
Konfiguration speichern
import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideEffects } from '@ngrx/effects';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { productsReducer } from './state/products.reducer';
import { ProductsEffects } from './state/products.effects';
export const appConfig: ApplicationConfig = {
providers: [
provideStore({
products: productsReducer
}),
provideEffects([
ProductsEffects
]),
provideStoreDevtools({
maxAge: 25
})
]
};
Winkelkomponente mit NgRx
import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { ProductsActions } from './state/products.actions';
import {
selectProducts,
selectProductsError,
selectProductsLoading
} from './state/products.selectors';
@Component({
selector: 'app-products-page',
standalone: true,
template: `
<section class="space-y-4">
<h2 class="text-2xl font-bold">Prodotti</h2>
@if (loading()) {
<p>Caricamento...</p>
}
@if (error()) {
<p class="text-red-600">{{ error() }}</p>
}
<ul class="grid gap-4 md:grid-cols-3">
@for (product of products(); track product.id) {
<li class="rounded-xl border border-slate-200 p-4">
<h3 class="font-semibold">{{ product.name }}</h3>
<p>€ {{ product.price }}</p>
</li>
}
</ul>
</section>
`
})
export class ProductsPageComponent {
private readonly store = inject(Store);
readonly products = this.store.selectSignal(selectProducts);
readonly loading = this.store.selectSignal(selectProductsLoading);
readonly error = this.store.selectSignal(selectProductsError);
constructor() {
this.store.dispatch(ProductsActions.loadProducts());
}
}
Wenn NgRx die richtige Wahl ist
- Sie haben viele Funktionen, die Daten teilen.
- Sie haben komplexe asynchrone Abläufe.
- Sie benötigen DevTools und Zeitreise-Debugging.
- Das Team muss sehr klaren Mustern folgen.
- Der Staat muss vorhersehbar und zentralisiert sein.
8. Wann sollte Akita
verwendet werden?Akita ist eine pragmatischere und weniger ausführliche Zustandsverwaltungslösung als NgRx. Dies ist nützlich, wenn Sie einen strukturierten Speicher wünschen, ohne jedoch viele Aktionen und Reduzierungen schreiben zu müssen.
Installation
npm install @datorama/akita
Konzeptionelles Beispiel eines Ladens
import { Injectable } from '@angular/core';
import {
EntityState,
EntityStore,
StoreConfig
} from '@datorama/akita';
export interface CartItem {
id: number;
name: string;
quantity: number;
}
export interface CartState extends EntityState<CartItem, number> {}
@Injectable({
providedIn: 'root'
})
@StoreConfig({
name: 'cart'
})
export class CartStore extends EntityStore<CartState> {
constructor() {
super();
}
}
Service zum Ändern des Status
import { Injectable, inject } from '@angular/core';
import { CartStore, CartItem } from './cart.store';
@Injectable({
providedIn: 'root'
})
export class CartService {
private readonly cartStore = inject(CartStore);
addItem(item: CartItem): void {
this.cartStore.add(item);
}
updateQuantity(id: number, quantity: number): void {
this.cartStore.update(id, {
quantity
});
}
removeItem(id: number): void {
this.cartStore.remove(id);
}
clearCart(): void {
this.cartStore.reset();
}
}
Praktische Wahl
Wenn Ihr Team bereits mit NgRx vertraut ist und an Unternehmensanwendungen arbeitet, bleibt NgRx eine sehr gute Wahl. Wenn Sie jedoch ein Geschäft wünschen, das schneller umzusetzen und weniger feierlich ist, kann Akita eine gute Alternative sein. Bei neuen und kleinen bis mittleren Anwendungen sollten Sie zunächst Signale in Betracht ziehen.
9. Erweiterte reaktive Formen in Angular
Reaktive Formulare sind eines der leistungsstärksten Tools von Angular. Sie ermöglichen Ihnen, komplexe Formulare zu erstellen, Eingaben zu validieren, dynamische Zustände zu verwalten, FormArrays zu erstellen, feldübergreifende Validierungen und asynchrone Prüfungen, die mit APIs verknüpft sind.
Praktisches Szenario
Erstellen wir ein Profilformular mit:
- Kontodaten;
- E-Mail mit asynchroner Validierung;
- Passwort mit benutzerdefiniertem Validator;
- Passwort bestätigen;
- dynamische Liste von Fähigkeiten mit
FormArray; - Fehlermeldungen in der Vorlage löschen.
Benutzerdefinierte Validatoren
import {
AbstractControl,
AsyncValidatorFn,
ValidationErrors,
ValidatorFn
} from '@angular/forms';
import {
catchError,
map,
of,
switchMap,
timer
} from 'rxjs';
import { UsersApiService } from './users-api.service';
export function passwordStrengthValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = String(control.value || '');
const hasMinLength = value.length >= 8;
const hasUppercase = /[A-Z]/.test(value);
const hasNumber = /[0-9]/.test(value);
const hasSpecial = /[^A-Za-z0-9]/.test(value);
const valid =
hasMinLength &&
hasUppercase &&
hasNumber &&
hasSpecial;
if (valid) {
return null;
}
return {
weakPassword: {
hasMinLength,
hasUppercase,
hasNumber,
hasSpecial
}
};
};
}
export function passwordsMatchValidator(
passwordKey: string,
confirmPasswordKey: string
): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const password = group.get(passwordKey)?.value;
const confirmPassword = group.get(confirmPasswordKey)?.value;
if (password === confirmPassword) {
return null;
}
return {
passwordsMismatch: true
};
};
}
export function uniqueEmailValidator(
usersApi: UsersApiService
): AsyncValidatorFn {
return (control: AbstractControl) => {
const email = String(control.value || '').trim();
if (!email) {
return of(null);
}
return timer(400).pipe(
switchMap(() => usersApi.emailExists(email)),
map(exists => {
return exists ? { emailTaken: true } : null;
}),
catchError(() => {
return of(null);
})
);
};
}
Beispiel-API-Dienst
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UsersApiService {
private readonly http = inject(HttpClient);
emailExists(email: string): Observable<boolean> {
return this.http.get<boolean>(
`/api/users/email-exists?email=${encodeURIComponent(email)}`
);
}
}
Komponente mit komplexer reaktiver Form
import { Component, inject } from '@angular/core';
import {
FormArray,
NonNullableFormBuilder,
ReactiveFormsModule,
Validators
} from '@angular/forms';
import {
passwordStrengthValidator,
passwordsMatchValidator,
uniqueEmailValidator
} from './profile.validators';
import { UsersApiService } from './users-api.service';
@Component({
selector: 'app-profile-form',
standalone: true,
imports: [
ReactiveFormsModule
],
templateUrl: './profile-form.component.html'
})
export class ProfileFormComponent {
private readonly fb = inject(NonNullableFormBuilder);
private readonly usersApi = inject(UsersApiService);
readonly form = this.fb.group({
account: this.fb.group(
{
name: [
'',
[
Validators.required,
Validators.minLength(2)
]
],
email: [
'',
{
validators: [
Validators.required,
Validators.email
],
asyncValidators: [
uniqueEmailValidator(this.usersApi)
],
updateOn: 'blur'
}
],
password: [
'',
[
Validators.required,
passwordStrengthValidator()
]
],
confirmPassword: [
'',
[
Validators.required
]
]
},
{
validators: [
passwordsMatchValidator('password', 'confirmPassword')
]
}
),
profile: this.fb.group({
role: ['frontend'],
bio: ['']
}),
skills: this.fb.array([
this.createSkill()
])
});
get skills(): FormArray {
return this.form.controls.skills;
}
createSkill() {
return this.fb.group({
name: [
'',
[
Validators.required
]
],
years: [
0,
[
Validators.required,
Validators.min(0),
Validators.max(40)
]
]
});
}
addSkill(): void {
this.skills.push(this.createSkill());
}
removeSkill(index: number): void {
if (this.skills.length === 1) {
return;
}
this.skills.removeAt(index);
}
submit(): void {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
console.log('Payload form:', this.form.getRawValue());
}
}
HTML-Formularvorlage
<form
class="mx-auto max-w-3xl space-y-8 rounded-2xl bg-white p-6 shadow-sm"
[formGroup]="form"
(ngSubmit)="submit()">
<section formGroupName="account" class="space-y-4">
<h2 class="text-xl font-bold text-slate-900">Account</h2>
<div>
<label class="block text-sm font-medium text-slate-700">
Nome
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="text"
formControlName="name">
@if (form.controls.account.controls.name.touched &&
form.controls.account.controls.name.hasError('required')) {
<p class="mt-1 text-sm text-red-600">
Il nome è obbligatorio.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Email
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="email"
formControlName="email">
@if (form.controls.account.controls.email.pending) {
<p class="mt-1 text-sm text-slate-500">
Verifica email in corso...
</p>
}
@if (form.controls.account.controls.email.hasError('emailTaken')) {
<p class="mt-1 text-sm text-red-600">
Questa email è già registrata.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Password
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="password"
formControlName="password">
@if (form.controls.account.controls.password.hasError('weakPassword')) {
<p class="mt-1 text-sm text-red-600">
La password deve avere almeno 8 caratteri, una maiuscola,
un numero e un carattere speciale.
</p>
}
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Conferma password
</label>
<input
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
type="password"
formControlName="confirmPassword">
@if (form.controls.account.hasError('passwordsMismatch')) {
<p class="mt-1 text-sm text-red-600">
Le password non coincidono.
</p>
}
</div>
</section>
<section formGroupName="profile" class="space-y-4">
<h2 class="text-xl font-bold text-slate-900">Profilo</h2>
<div>
<label class="block text-sm font-medium text-slate-700">
Ruolo
</label>
<select
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
formControlName="role">
<option value="frontend">Frontend Developer</option>
<option value="backend">Backend Developer</option>
<option value="fullstack">Full Stack Developer</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">
Bio
</label>
<textarea
class="mt-1 w-full rounded-lg border border-slate-300 px-3 py-2"
rows="4"
formControlName="bio"></textarea>
</div>
</section>
<section class="space-y-4">
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold text-slate-900">
Competenze
</h2>
<button
type="button"
class="rounded-lg bg-indigo-600 px-4 py-2 text-white"
(click)="addSkill()">
Aggiungi competenza
</button>
</div>
<div formArrayName="skills" class="space-y-3">
@for (skill of skills.controls; track $index) {
<div
class="grid gap-3 rounded-xl border border-slate-200 p-4 md:grid-cols-[1fr_140px_auto]"
[formGroupName]="$index">
<input
class="rounded-lg border border-slate-300 px-3 py-2"
type="text"
placeholder="Competenza"
formControlName="name">
<input
class="rounded-lg border border-slate-300 px-3 py-2"
type="number"
placeholder="Anni"
formControlName="years">
<button
type="button"
class="rounded-lg border border-red-300 px-4 py-2 text-red-600"
(click)="removeSkill($index)">
Rimuovi
</button>
</div>
}
</div>
</section>
<button
type="submit"
class="w-full rounded-xl bg-slate-900 px-5 py-3 font-semibold text-white disabled:opacity-50"
[disabled]="form.pending">
Salva profilo
</button>
</form>
Häufige Fehler in reaktiven Formularen
- Zu viel Logik in die Vorlage einfügen.
- Verwenden Sie
markAllAsTouched()nicht beim Senden. - Führen Sie bei jedem Tastendruck asynchrone Aufrufe aus, ohne
updateOn: 'blur'oder entprellen. - Unterteilen Sie Validatoren nicht in dedizierte Dateien.
- Behandeln Sie nicht den
ausstehenden-Status asynchroner Validierungen. - Formulare in großen Projekten nicht richtig eingeben.
Best Practices
- Verwenden Sie reaktive Formulare für komplexe und dynamische Formulare.
- Verwenden Sie vorlagengesteuerte Formulare nur für sehr einfache Formulare.
- Benutzerdefinierte Validatoren extrahieren.
- Fehlermeldungen erst nach
berührtanzeigen oder absenden. - Senden deaktivieren, während das Formular
ausstehend. ist
- Normalisieren Sie die Nutzlast, bevor Sie sie an das Backend senden.
10. Testen Sie Store und Formular
Ein praktisches Tutorial ist ohne Tests nicht vollständig. Sie müssen nicht jedes Detail des Frameworks testen, aber es ist wichtig, Ihre benutzerdefinierte Logik zu testen: Speicher, Validatoren und Datentransformation.
Testen eines Passwort-Validators
import { FormControl } from '@angular/forms';
import { passwordStrengthValidator } from './profile.validators';
describe('passwordStrengthValidator', () => {
it('dovrebbe invalidare una password debole', () => {
const control = new FormControl('abc');
const validator = passwordStrengthValidator();
const result = validator(control);
expect(result).toEqual(
jasmine.objectContaining({
weakPassword: jasmine.any(Object)
})
);
});
it('dovrebbe accettare una password forte', () => {
const control = new FormControl('Angular2026!');
const validator = passwordStrengthValidator();
const result = validator(control);
expect(result).toBeNull();
});
});
Speichertests mit Signalen
import { TodoStore } from './todo.store';
describe('TodoStore', () => {
it('dovrebbe aggiungere una todo', () => {
const store = new TodoStore();
store.addTodo('Studiare Angular Signals');
expect(store.todos().length).toBe(1);
expect(store.todos()[0].title).toBe('Studiare Angular Signals');
});
it('dovrebbe filtrare le todo completate', () => {
const store = new TodoStore();
store.addTodo('Prima attività');
const id = store.todos()[0].id;
store.toggleTodo(id);
store.filter.set('done');
expect(store.visibleTodos().length).toBe(1);
expect(store.visibleTodos()[0].done).toBeTrue();
});
});
11. Leistung und Wartung
In Angular sind Leistung und Wartung verknüpft. Es wird schwierig, eine schnelle, aber chaotische App weiterzuentwickeln. Andererseits lässt sich eine gut organisierte App tendenziell auch einfacher optimieren.
Praktische Ratschläge
- Features in kleine Komponenten aufteilen.
- Verwenden Sie Lazy Loading für umfangreiche Seiten und Bereiche.
- Vermeiden Sie komplexe Logik in Vorlagen.
- Verwenden Sie
Trackin Schleifen@für. - Listener, Diagramme und Abonnements zerstören, die nicht automatisch verwaltet werden.
- Legen Sie nicht alles in den globalen Laden.
- Verwenden Sie berechnete Werte, anstatt Daten in der Vorlage neu zu berechnen.
- Vermeiden Sie zu aggressive asynchrone Validierungen.
12. Abschließende Checkliste für ein solides Angular-Projekt
- Hat das Projekt eine klare Struktur?
- Sind die Komponenten klein und fokussiert?
- Sind externe Bibliotheken in Komponenten oder Services gekapselt?
- Wird Chart.js korrekt zerstört?
- Wird Rückenwind konsequent eingesetzt?
- Ist der Zustand lokal, wenn möglich, und global, nur wenn nötig?
- Verwenden komplexe Formulare reaktive Formulare?
- Werden benutzerdefinierte Validatoren getestet?
- Sind die Formularfehler für den Benutzer verständlich?
- Verfügen asynchrone Aufrufe über eine Fehlerbehandlung?
- Ist der Code auch für einen neuen Entwickler lesbar?
FAQ
Bessere Signale oder NgRx?
Es kommt auf die Komplexität an. Signale eignen sich hervorragend für den einfachen oder mittleren Status. NgRx eignet sich am besten für große Anwendungen mit komplexen Abläufen, erweitertem Debugging und großen Teams.
Kann ich Signals und NgRx zusammen verwenden?
Ja. Sie können NgRx für den globalen Status und Signale für den lokalen UI-Status, Filter, Panels usw. verwenden. Registerkarte und abgeleitete Werte in Komponenten.
Ist Tailwind CSS für Unternehmensanwendungen geeignet?
Ja, wenn klare Konventionen eingehalten werden. Es ist wichtig, gemeinsame Komponenten zu erstellen und Klassen nicht überall ohne Kriterien zu duplizieren.
Ist Chart.js für komplexe Dashboards ausreichend?
Für viele Dashboards ja. Wenn Sie extrem fortschrittliche Grafiken, benutzerdefinierte Interaktionen oder große Datenmengen benötigen, Sie könnten über spezialisiertere Bibliotheken nachdenken.
Wann sollte man Reactive Forms verwenden?
Verwenden Sie reaktive Formulare, wenn Sie über komplexe Validierungen, dynamische Formulare, FormArrays usw. verfügen. voneinander abhängige Kontrollen oder asynchrone Validierungen.
Wie gehe ich mit Fehlern in Formularen um?
Fehler werden erst angezeigt, nachdem das Feld berührt wurde oder nach dem Absenden. Vermeiden Sie, dass zu viele Fehler angezeigt werden, während der Benutzer noch tippt.
Verlangsamen asynchrone Validierungen die App?
Sie können es verlangsamen, wenn jeder Charakter sie erledigt.
Verwenden Sie updateOn: 'blur', entprellen Sie und führen Sie bei Bedarf eine Zwischenspeicherung durch.
Ist Akita immer noch eine gute Wahl?
Dies kann in Projekten der Fall sein, die es bereits verwenden, oder in Teams, die einen einfacheren Ansatz bevorzugen. Bewerten Sie bei neuen Projekten sorgfältig Signale und NgRx, bevor Sie eine externe Bibliothek einführen.
Ist es besser, Wrapper-Bibliotheken für Chart.js zu verwenden?
Es kommt darauf an. Ein Wrapper kann die Integration vereinfachen, aber seine direkte Verwendung bietet mehr Kontrolle. In einfachen Fällen reicht oft die direkte Integration aus.
Wie vermeidet man zu große Winkelbauteile?
Extrahieren Sie Logik in Dienste, Geschäfte oder Validatoren. Teilen Sie die Benutzeroberfläche in kleinere Komponenten auf und nutzen Sie bei Bedarf Eingabe/Ausgabe oder gemeinsame Dienste.