Übersicht zu Angular Signals
Mit Signals liefert Angular eine weitere Möglichkeit für ein reaktives Zustandsmanagement.
Für Umsteiger:innen aus z. B. React bietet die neue Signals-API eine gute und bekannte Möglichkeit Zustände darzustellen und zu verwalten.
Signals kann man als Wrapper um einen Wert verstehen, dessen Zustand Angular intern überwacht und den neuen Zustand vom Wert an die jeweiligen Interessenten weitergibt.
Dabei unterscheiden wir Signals in Writable und Readable Signals, d. h. in Signals, die beschrieben werden können und somit neue Werte erhalten und lediglich lesbare Signale, dessen Werte und Änderungen nur ausgelesen werden.
Writable and Readable Signals
Die Angulars Signals API bietet rein syntaktisch bereits eine Unterscheidung für die Erkennung der jeweiligen Signale.
Writable Signals werden mit Hilfe der signals
Funktion wie folgt erstellt:
1const value = signal<number>(0)
Nachdem das writable Signal erstellt wurde, stellt die API für die Veränderung des Wertes Funktionen bereit - zum Setzen mit set
, Updaten mit update
oder das Signal nach Außen als Readable mit asReadonly
.
1const value = signal<number>(0)
2
3value.set(3)
4value() // Aufruf value() liefert 3
5
6value.update(prev -> prev + 1)
7value() // Aufruf von value() liefert 4
8
9const readableSignal = value.asReadonly() // Variable ist nicht veränderbar
Am Beispiel sind die Aufrufe bzw. das Auslesen der Signal-Werte mit einem Kommentar versehen, mit dem aktuellen Wert. Dabei ist es wichtig zum Aufruf der Werte die Signal-Variable als Funktion aufzurufen, um den dahinter liegenden Wert zu erhalten.
Computed Signals
Computed Signals gehören zu den readable Signals und können als abgeleitete Werte verstanden werden, d. h. der Wert von einem computed Signal wird in Abhängigkeit von einem vorhandenen Signal oder Signals ermittelt.
1const writableSignal = signal<number>(0) 2 3const derivedSignal = computed(() => { 4 const dependedValue = writableSignal() 5 return dependedValue + 1 6}) 7 8writableSignal.set(1) // Wert wird auf 1 verändert 9 10derivedSignal() // durch Änderung wird neuer Wert ermittelt -> 2
Im obigen Beispiel habe ich bewusst die Benennung der Variablen als solche gewählt, um die Zusammenhänge besser zu verdeutlichen.
Denn bei computed Signals ist es erforderlich die abhängige Größe(n), hier writableSignal
im Funktionskörper computed
aufzurufen.
Dadurch wird Angular vermittelt, welche Abhängigkeiten zum abgeleiteten Wert derivedSignal
bestehen, um somit den Wert neu zu ermitteln, sobald sich die Abhängigkeit writableSignal
ändert.
Seiteneffekte mit effect
Für die Umsetzung von Seiteneffekten, darunter versteht man im Angular Kontext zusätzliche Operationen einer Hauptfunktion, die den Zustand der Anwendung verändern, z. B. beim Update vom Signal werden in der UI Werte geändert und angezeigt, gibt es die effect
Operation.
Anders als computed
gibt effect
keinen Wert für ein neues Signal zurück.
Die effect
Operation wird in einem sog. Injection Kontext ausgeführt, d. h. im Konstruktor der jeweiligen Klasse, die instanziiert wird.
1@Component({/*...*/}) 2export class SimpleAdditionComponent { 3 private state = signal<number>(0) 4 5 constructor() { 6 effect(() => { 7 const dependency = this.state() 8 console.log(`sideeffect of state: ${this.state()}`) 9 }) 10 } 11 12 addOne() { 13 this.state.update(prev => prev + 1) 14 } 15}
Im obigen Beispiel wird die effect
Operation in dem Injection Kontext, dem Konstruktor der Klasse aufgerufen, um den jeweiligen Seiteneffekt darzustellen, sobald sich die Abhängigkeit state
ändert.
Aus dem Anlass habe ich die Variable im Funktionskörper der effect
Operation dependency
genannt, um auf die Abhängigkeit aufmerksam zu machen.
Wichtig ist, dass die effect
Operation beim ersten Durchgang immer ausgeführt wird, sobald die Instanziierung der Klasse erfolgt.
In den nächsten Fällen wird der Seiteneffekt erst vollzogen, sollte die addOne
Methode aufgerufen werden.
Die Ausführung der effect
Operation erfolgt asynchron.
Ebenso sollte beachtet werden, dass innerhalb der effect
Operation Werte von Signalen gesetzt werden können.
Jedoch empfehle ich in einem solchen Fall auf die Abhängigkeiten zu achten, weil es im Falle einer zyklischen Abhängigkeit zu einer Endlosschleife führt.
D. h. sollte das state
Signal innerhalb der effect
Operation einen neuen Wert erhalten, folgt es zu einem erneuten Aufruf des Funktionskörpers.
input
Signal
Neben den o. a. Signals für die Darstellung und Ableitung von Zuständen gibt es die Möglichkeit Signals als Daten in eine Komponente einzubringen, von einer Eltern-Komponente in eine Kind-Komponente.
Mit dem input
Signal innerhalb der Komponente wird die frühere Variante @Input
als Annotation und Mittel zur Komponenten-Kommunikation abgelöst.
Der Vorteil liegt im Vergleich zur @Input
Variante u. a. bei der Type-Safety, d. h. die übergebenen Werte haben einen zugewiesenen Typen und TypeScript gibt Feedback, sollten Diskrepanzen vorliegen.
1@Component({/*...*/})
2export class DisplayComponent {
3 public displayValue = input<Data>() // Datentyp: Data oder undefinded
4}
5
6// im Template
7@if(displayValue()) {
8 <div>{{displayValue()}}</div>
9} @else {
10 <div>no data to display</div>
11}
Im o. a. Beispiel ist die input
Signal Variante in der Klassen-Komponente und im Template der dazugehörigen Komponente dargestellt.
Hierbei fällt auf, dass die Eingabe von Daten mittels des input
Schlüsselwortes umgesetzt und im Template als Funktionsaufruf vom Signal angezeigt werden kann.
Bei der Nutzung von Signals sollte darauf geachtet werden diese zu initiieren, denn sollte, wie im Beispiel das Signal nicht initiiert werden, enthält es zunächst den Wert undefined
.
Aus dem Anlass habe ich im Beispiel die Syntax für den Kontrollfluss im Template mit @if
und @else
genutzt, denn displayValue
soll nur angezeigt werden, wenn der Input eingegeben wurde.
Die input
API gibt uns darüber hinaus die Möglichkeit Eingaben als Pflichtangaben zu deklarieren, dafür gibt es die Punktoperation required
.
1@Component({/*...*/}) 2export class DisplayComponent { 3 public displayValue = input.required<Data>() 4} 5 6// im Template 7<div>{{displayValue()}}</div>
Ich habe hierfür das Beispiel mit der required
Punktoperation zum input
angepasst.
Beim Vergleich der Beispiele ist ersichtlich, dass die Syntax für den Kontrollfluss im Template ausfällt.
Damit wird die Nutzung der DisplayComponent
erst möglich, wenn die displayValue
als Input übergeben wird.
Aus dem Anlass braucht das Input-Signal im Klassen-Körper der DisplayComponent
keine Initiierung innerhalb der runden Klammern.
Sollte die DisplayComponent
ohne den geforderten Input genutzt werden, wird Angular einen Fehler auswerfen.
output
Signal
Im Rahmen der Komponenten-Kommunikation existiert die Möglichkeit Informationen von der Kind-Komponente an die Eltern-Komponente weiter zu leiten.
Die output
Signal API dient damit als Ersatz für die bisherige @Output
Annotation, mit gleichen Vorteilen, wie input
- die Typisierung zwischen den Komponenten sind klar und überprüfbar, im Gegenteil zur bisherigen @Output
Annotation.
1// Child-Component
2@Component({/*...*/})
3export class ListItemComponent {
4 item = input.required<Item>()
5 itemSelected = output<Item>()
6
7 selectItem(item: Item) {
8 this.itemSelected.emit(item)
9 }
10}
11
12// Template of child-component
13<li>
14 <p>{{item().name}}</p>
15 <button (onClick)="selectItem(item())">Add Item</button>
16</li>
17
18// Parent-Component
19@Component({/*...*/})
20export class ListComponent implements OnInit {
21 private itemService = inject(ItemService)
22 private itemsState = signal<Item[]>([])
23
24 public itemsSignal = this.itemsState.asReadOnly()
25
26 ngOnInit() {
27 this.itemService.getItems().subscribe((items: Item[]) => {
28 this.itemsState.set(items)
29 })
30 }
31
32 addItem(item: Item) {
33 this.itemService
34 .addItem(item)
35 .subscribe(...)
36 }
37}
38
39// Template of parent-component
40<ul>
41 @for(item of itemsSignal(); track item.id) {
42 <app-list-item [item]="item" (itemSelected)="addItem($event)" />
43 }
44</ul>
Im obigen Beispiel sind die bisherigen Themen beziehungsweise Funktionalitäten eingearbeitet und mit dem output
Signal verknüpft.
Dabei wird zunächst eine Liste von Elementen von der Elternkomponente an die Kind-Komponente mit Hilfe von input
weitergereicht und innerhalb des Templates angezeigt.
Mit Hilfe der output
API wird das gewählte Element innerhalb der selectItem
Funktion mit this.itemSelected.emit(...)
an die Elternkomponente emittiert.
Die Elternkomponente fängt das emittierte Event (itemSelected)
von der Kindkomponente mit addItem=($event)
auf und verarbeitet den Wert vom Output innerhalb der addItem
Methode.
Innerhalb der addItem
Methode wird die Auswahl des Elementes mit der API des ItemService
vorgenommen und verarbeitet.
Am konkreten Beispiel
Zur Veranschaulichung der bisherigen Themen, habe ich eine konkrete Anwendung ausgearbeitet.
Bei der Anwendung handelt es sich um eine E-Commerce Anwendung, die aus unterschiedlichen Komponenten besteht - Produkte auflistet, eine detaillierte Produktseite anzeigt und einen Warenkorb besitzt.
Es besteht die Möglichkeit zwischen der Produktauflistung, den Details und dem Warenkorb zu navigieren sowie Produkte in den Warenkorb hinzuzufügen.
Für das Beispiel nutze ich Angular (Version 19), die Komponenten Bibliothek PrimeNG und TailwindCSS.
Zur Auslieferung von den notwendigen Daten habe ich für Test-Zwecke ein Fastify Backend zur Verfügung gestellt, das sich nicht im folgenden Repository befindet.
Das Projekt befindet sich im folgenden Repositiory: Github - Angular Signals
Produktliste und Listenelemente (input, output)
Im folgenden Screenshot ist die Hauptseite und darin die Auflistung der einzelnen Produkte zu sehen.
Die Auflistung der Produkte und die einzelnen Produkt-Positionen (z. B. Wireless Bluetooth Headphones) sind als zwei Komponenten designed.
Als Eltern-Komponente (ProductOverview) dient die Auflistung, die den Service zum Einholen der Produkte nutzt. Darüber hinaus gibt die Eltern-Komponente die einzelnen Produkte weiter an die Kind-Komponente (Product-Item), zur Darstellung des jeweiligen Produktes.
1// Auszug Template Eltern-Komponente (angepasst fürs Beispiel) 2@for (product of products(); track product.id) { 3 <sgn-product-item [product]="product" (cartItemAdded)="addItemToCart($event)" /> 4} 5 6// Auszug Eltern-Komponente (angepasst fürs Beispiel) 7@Component({/*...*/}) 8export class ProductOverviewComponent { 9 private productService = inject(ProductService); 10 private cartService = inject(CartService); 11 12 private productState = signal<Product[]>([]); 13 public products = this.productState.asReadonly(); 14 15 constructor() { 16 this.productService 17 .getProducts() 18 .pipe(takeUntilDestroyed()) 19 .subscribe((products: Product[]) => { 20 this.productState.set(products); 21 }); 22 } 23 24 addItemToCart(cartItem: CartItem): void { 25 this.cartService.addItemToCart(cartItem).subscribe({/*...*/}); 26 } 27}
Im Template ist die Kind-Komponente mit dem Selektor sng-product-item
ersichtlich, den das Produkt von der Eltern-Komponente weitergereicht wird.
1@Component({
2 selector: 'sgn-product-item',
3 /*...*/
4})
5export class ProductItemComponent {
6 public product = input.required<Product>();
7 public cartItemAdded = output<CartItem>();
8
9 addCartItem(product: Product) {
10 this.cartItemAdded.emit({ product, amount: 1 });
11 }
12}
Innerhalb der Kind-Komponente wird das input
sowie output
Signal genutzt, um die Eingabe vom Produkt sowie emittieren an die Eltern-Komponente zu realisieren.
Mit der Nutzung von diesen Signals ist es gelungen, die Daten an einem Ort zu fetchen und weiterzureichen, an eine sog. Presentational-Component, die lediglich Daten präsentiert.
Warenkorb (effect, computed)
Am Beispiel vom Warenkorb werde ich eine mögliche Nutzung von effect
und computed
aufzeigen.
Wir werden innerhalb des effect
Blockes versuchen einen Service zu subscriben und die erhaltenen Daten in ein Signal, den cartSate
, zu schreiben.
Aus den Produkten von Warenkorb, die von der cartState
Variable gehalten werden, leiten wir die Summe ab und bedienen uns damit dem computed
Signal.
Im obigen Auszug ist der Warenkorb, mit den einzelnen Produkten sowie der Gesamtsumme sichtbar.
1@Component({/*...*/})
2export class CartOverviewComponent {
3 private cartService = inject(CartService);
4 private cartState = signal<Cart | null>(null);
5 public cartSignal = this.cartState.asReadonly();
6
7 public cartSum = computed(() => {
8 const cart = this.cartSignal();
9 if (cart) {
10 return this.getSumOfCart(cart);
11 } else {
12 return null;
13 }
14 });
15
16 constructor() {
17 effect(() => {
18 this.cartService.getCart().subscribe((cart) => {
19 this.cartState.set(cart);
20 });
21 });
22 }
23}
Innerhalb der CartOverviewComponent
wird im constructor
Block das effect
Signal genutzt, um die Produkte im Warenkorb zu fetchen und sie anschließend im Warenkorb-Zustand cartState
festzuhalten.
Das cartSignal
, was lediglich von Außen lesbar ist (public), wird im Template zur Veranschaulichung genutzt.
Aus dem cartState
, in dem sich alle Produkte im Warenkorb befinden, wird die Summe abgeleitet und ebenfalls im Template angezeigt.
Bei der Nutzung von effect
zum Beschreiben von Signalen sollte stets darauf geachtet werden zirkuläre Abhängigkeiten zu vermeiden.
Alternative mit resource
effect
ist eine Möglichkeit Seiteneffekte i. V. m. Services umzusetzen, jedoch nicht die von Angular Signals favorisierte Variante, auch wenn sie in der Praxis gerne genutzt wird.
Im Angular v19 Update wurde das resource
Signal eingeführt, um explizit mit asynchronen Operationen besser umzugehen.
Ich habe das obige Beispiel der CardOverviewComponent
abgeändert und den Aufruf des Service innerhalb vom resource
Block umgesetzt.
1@Component({/*...*/})
2export class CartOverviewComponent {
3 private cartService = inject(CartService);
4 public cartSignal = computed(() => this.cartResource.value());
5
6 public cartSum = computed(() => {
7 const cart = this.cartSignal();
8 if (cart) {
9 return this.getSumOfCart(cart);
10 } else {
11 return null;
12 }
13 });
14
15 public cartResource = resource({
16 loader: async () => {
17 return await firstValueFrom(this.cartService.getCart());
18 }
19 });
20}
Innerhalb vom loader
Block des resource
Signals wird der cartService
aufgerufen.
Zu beachten ist, dass der loader
Block ein Promise
als Rückgabe erwartet, weshalb ich im Beispiel firstValueFrom
der rxjs
Bibliothek nutze.
Damit wird ein Observable
in ein Promise
konvertiert und resolved, sobald der erste Wert von der Subscription angekommen ist.
An diesem kleinen Beispiel wollte ich den Wert und Nutzen von resource
aufzeigen und wie es eingesetzt werden kann.
Testen von Signals
Das Testen von Signals werde ich anhand der konkreten Beispiele mit den eingesetzten Signal APIs vorstellen und dabei auf wesentliche Aspekte eingehen.
Ich möchte in dem Rahmen darauf aufmerksam machen, dass ich beim Anlegen vom Projekt vitest
und die Angular Testing Library
nutze.
Unabhängig von der gewählten Technologie, werde ich ebenfalls die Testfälle exemplarisch mit TestBed
veranschaulichen.
Zur einheitlichen Struktur und Verständnis sind die Tests nach dem Arrange-Act-Assert
Pattern angelegt.
Testing input
, output
Im Testfall für das input
und output
Signal ziehe ich die ProductItemComponent
als Beispiel heran.
In der obigen Abbildung ist die Komponente zu sehen, die sich als Kind-Komponente in der bereits gezeigten ProductOverviewComponent
befindet.
Aufbau
Die ProductItemComponent
ist folgend aufgebaut:
1// Class 2@Component({/*...*/}) 3export class ProductItemComponent { 4 public product = input.required<Product>(); 5 public cartItemAdded = output<CartItem>(); 6 7 addCartItem(product: Product) { 8 this.cartItemAdded.emit({ product, amount: 1 }); 9 } 10} 11 12// Template 13<div class="..."> 14 {/*...*/} 15 <div class="..."> 16 <a [routerLink]="[product().id]" class="..."> 17 <p class="...">{{ product().category }}</p> 18 <p class="..." data-testid="product-name">{{ product().name }}</p> 19 </a> 20 @if (product().rating) { 21 <p-rating stars="5" [ngModel]="product().rating" readonly /> 22 } 23 </div> 24 <div class="..."> 25 <p class="...">{{ product().price | currency }}</p> 26 <p-button 27 data-testid="add-to-cart-button" 28 icon="..." 29 label="Add to cart" 30 (onClick)="addCartItem(product())" 31 ></p-button> 32 </div> 33</div>
Variante mit Angular Testing Library
Die Angular Testing Library stellt zum Testen von Komponenten mit Signals die Property inputs
zur Verfügung, die sich im Körper der render
Funktion befindet.
1// Testing input signal 2it('should display product item', async () => { 3 await render(ProductItemComponent, { 4 inputs: { 5 product: productMock 6 } 7 }); 8 9 expect(screen.getByText(productMock.name)).toBeTruthy(); 10});
Im obigen Unit-Test wird geprüft, ob das eingegebene Produkt im Template sichtbar ist.
Dabei nutze ich stichprobenartig den Selektor getByText
der Angular Testing Library und erwarte den Produktnamen im Template.
Beim Testen von dem output
Signal nutze ich einen sog. Spy, um auf das emit
vom Output zu hören.
1it('should add item to cart', async () => {
2 const { fixture } = await render(ProductItemComponent, {
3 inputs: {
4 product: productMock
5 }
6 });
7 const spy = vi.spyOn(fixture.componentInstance.cartItemAdded, 'emit');
8
9 screen.getByText('Add to cart').click();
10
11 expect(spy).toHaveBeenCalledWith({
12 amount: 1,
13 product: productMock
14 });
15});
Mittels der destructuring assignment
({fixture}
) wird das sog. fixture
zum Testen und Debugging der Komponente aus der render
Funktion ausgepackt und im spyOn
(Spionier-Funktion) verwendet, um auf das emit
des cartItemAdded
Outputs zu hören.
Variante mit TestBed
Beim Testen mit TestBed gibt es Ähnlichkeiten bei den Unit-Tests, wie im folgenden zu sehen ist:
1// Testing: input signal 2it('should display product item', () => { 3 const productTitleElement = debugElement.query( 4 By.css('[data-testid="product-name"]') 5 ).nativeElement; 6 7 expect(productTitleElement.textContent).toStrictEqual(productMock.name); 8}); 9 10// Testing: output signal 11it('should add item to cart', () => { 12 const spy = vi.spyOn(fixture.componentInstance.cartItemAdded, 'emit'); 13 const addToCartButton = debugElement.query(By.css('[data-testid="add-to-cart-button"]')); 14 15 addToCartButton.triggerEventHandler('onClick'); 16 fixture.detectChanges(); 17 18 expect(spy).toHaveBeenCalledWith({ 19 amount: 1, 20 product: productMock 21 }); 22});
Das Vorgehen bei den beiden Signals ist ähnlich zu den Tests mit der Angular Testing Library. Jedoch unterscheiden sie sich bei der Bereitstellung der Test-Komponente und der damit zusammenhängenden Konfiguration.
Die Konfiguration von der Test-Komponente erfolgt mit der TestBed Variante im beforEach
Block und sieht folgendermaßen aus:
1beforeEach(async () => {
2 await TestBed.configureTestingModule({
3 imports: [ProductItemComponent],
4 providers: [/*...*/]
5 }).compileComponents();
6
7 fixture = TestBed.createComponent(ProductItemComponent);
8
9 /* zwei optionen zum setzen vom komponenten input */
10 // Option 1
11 fixture.componentRef.setInput('product', productMock);
12
13 // Option 2
14 TestBed.runInInjectionContext(() => {
15 fixture.componentInstance.product = input(productMock);
16 });
17
18 fixture.detectChanges();
19
20 debugElement = fixture.debugElement;
21});
Innerhalb vom TestBed.configureTestingModule
wird die Komponente konfiguriert und das Fixture als Testschnittstelle zugewiesen.
Bei der Übergabe vom input
Signal gibt es zwei mögliche Optionen, die ich aufgeführt habe.
Die zweite Option bietet den Vorteil, dass die Typisierung erhalten und angezeigt wird.
Testing computed
und effect
Beim Testen von computed
und effect
wird das Beispiel der CardOverviewComponent
herangezogen.
In dem Komponenten-Test möchte ich darauf aufmerksam machen, dass es sich hierbei um einen Test mit ChangeDetection handelt, anders als beim Testen von Signals in Services.
Damit ist es nicht möglich, wie in den vorherigen Beispielen fixture.detectChanges
aufzurufen.
Um ausstehende Signals bzw. Änderungen auszuführen, steht der TestBed API die flushEffects
Funktion zu Verfügung.
In der unten dargestellten Abbildung ist die Test-Komponente CardOverviewComponent
zu sehen, mit den jeweiligen Items und der Gesamtsumme.
Aufbau
Die CardOverviewComponent
ist folgendermaßen aufgebaut:
1// Class
2@Component({/*...*/})
3export class CartOverviewComponent {
4 private cartService = inject(CartService);
5 private cartState = signal<Cart | null>(null);
6 public cartSignal = this.cartState.asReadonly();
7
8 public cartSum = computed(() => {
9 const cart = this.cartSignal();
10 if (cart) {
11 return this.getSumOfCart(cart);
12 } else {
13 return null;
14 }
15 });
16
17 constructor() {
18 effect(() => {
19 this.cartService.getCart().subscribe((cart) => {
20 this.cartState.set(cart);
21 });
22 });
23 }
24
25 private getSumOfCart(cart: Cart): number {
26 return cart.items.reduce((prev, curr) => {
27 return prev + curr.product.price;
28 }, 0);
29 }
30
31 deleteProductById(id: number) {
32 this.cartService.deleteItemFromCart(id.toString()).subscribe(() => {
33 this.cartService.getCart().subscribe((cart) => {
34 this.cartState.set(cart);
35 });
36 });
37 }
38}
Variante mit Angular Testing Library
1describe('CartOverviewComponent', () => { 2 const cartMock = CartBuilder.build(); 3 const cartServiceMock: Partial<CartService> = { 4 getCart: () => of(cartMock), 5 deleteItemFromCart: (id: string) => { 6 return of({}); 7 } 8 }; 9 10 beforeEach(async () => { 11 await render(CartOverviewComponent, { 12 providers: [{ provide: CartService, useValue: cartServiceMock }] 13 }); 14 }); 15 16 it('should display item in cart', async () => { 17 const productName = cartMock.items[0].product.name; 18 19 expect(screen.getByText(productName)).toBeTruthy; 20 }); 21 22 it('should display total of cart with one item', async () => { 23 const cartPrice = cartMock.items[0].product.price; 24 25 expect(screen.getByTestId('cart-sum')).toBeTruthy; 26 expect(screen.getByTestId('cart-sum').innerHTML).contains('total', `$${cartPrice}`); 27 }); 28});
Im obigen Ausschnitt habe ich den Test mit Hilfe der Angular Testing Library vorgenommen, dabei habe ich die render
Funktion innerhalb des beforeEach
Blocks platziert.
Alle sonstigen Tests ähneln den vorherigen Beispielen, mit der vorliegenden ChangeDetection innerhalb von Komponenten-Tests ist es nicht notwendig ausstehende Effekte auszuführen.
Variante mit TestBed
Zur Vollständigkeit folgt die Umsetzung mit TestBed:
1describe('CartOverviewComponent', () => { 2 let fixture: ComponentFixture<CartOverviewComponent>; 3 let debugElement: DebugElement; 4 5 const cartMock = CartBuilder.build(); 6 const cartServiceMock: Partial<CartService> = { 7 getCart: () => of(cartMock) 8 }; 9 10 beforeEach(async () => { 11 await TestBed.configureTestingModule({ 12 imports: [CartOverviewComponent], 13 providers: [{ provide: CartService, useValue: cartServiceMock }] 14 }).compileComponents(); 15 16 fixture = TestBed.createComponent(CartOverviewComponent); 17 debugElement = fixture.debugElement; 18 19 fixture.detectChanges(); 20 }); 21 22 it('should display item in cart', () => { 23 const cartItemName = debugElement.query(By.css('[data-testid="cart-item-name"]')).nativeElement; 24 25 expect(cartItemName.textContent).contain(cartMock.items[0].product.name); 26 }); 27 28 it('should display total of cart with one item', () => { 29 const cartPrice = cartMock.items[0].product; 30 const sum = debugElement.query(By.css('[data-testid="cart-sum"]')).nativeElement; 31 32 expect(sum).toBeTruthy; 33 expect(sum.textContent).contains('total', `$${cartPrice}`); 34 }); 35});
Fazit
Die Nutzung von Signals innerhalb der Angular Anwendung, um Inputs, Outputs sowie Seiteneffekte von Zuständen darzustellen, ist mit Hilfe der Dokumentation und der übersichtlichen Schnittstelle relativ schnell zu erlernen und anzuwenden.
Darüber hinaus finden Signals innerhalb von anderen Frameworks oder Libraries Anwendung und erleichtert aufgrund der Ähnlichkeit den Umstieg auf Angular.
Angulars Weg zur Vereinfachung und einheitlicheren Gestaltung vom Zustandsmanagement nimmt mit jedem Update mehr gestalt an und ersetzt sukzessiv die bisher bekannten Pattern mit RxJs.
Ein gutes Beispiel für die stetige Weiterentwicklung zeigt die Variante mit resource
bei der CardOverviewComponent
, um die Ressourcen für den Warenkorb vom dazugehörigen Backend-System zu laden.
Quellen und Informationen
Angular Documentation to Signals
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Blog-Autor*in
Nicat Achmedow
IT Consultant
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.