Skip to content

Commit a69a2bd

Browse files
committed
binding example added
1 parent d820c68 commit a69a2bd

6 files changed

Lines changed: 352 additions & 39 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# Component binding
2+
3+
Refactor `app/banner/banner.component.ts`
4+
5+
```ts
6+
import { Component, signal } from "@angular/core";
7+
8+
@Component({
9+
selector: "app-banner",
10+
imports: [],
11+
template: `<h1>{{ title() }}</h1>`,
12+
styles: `
13+
h1 { color: green; font-size: 350%; }
14+
`,
15+
})
16+
export class BannerComponent {
17+
title = signal("Test demos");
18+
}
19+
```
20+
21+
### Query for the `<h1>`
22+
23+
Update `banner.component.spec.ts`
24+
25+
```ts
26+
import { ComponentFixture, TestBed } from "@angular/core/testing";
27+
import { BannerComponent } from "./banner.component";
28+
29+
describe("BannerComponent", () => {
30+
let component: BannerComponent;
31+
let fixture: ComponentFixture<BannerComponent>;
32+
let h1: HTMLElement;
33+
34+
beforeEach(() => {
35+
TestBed.configureTestingModule({ imports: [BannerComponent] });
36+
fixture = TestBed.createComponent(BannerComponent);
37+
component = fixture.componentInstance;
38+
h1 = fixture.nativeElement.querySelector("h1");
39+
});
40+
});
41+
```
42+
43+
### `createComponent()` does not bind data
44+
45+
We would like to see if the data is displayed.
46+
47+
Update `banner.component.spec.ts`
48+
49+
```ts
50+
it("should display original title", () => {
51+
expect(h1.textContent).toContain(component.title());
52+
});
53+
```
54+
55+
```bash
56+
npx ng test --include app/banner/banner.component.spec.ts
57+
```
58+
59+
The test fails with
60+
61+
```
62+
expected '' to contain 'Test demos'.
63+
```
64+
65+
Binding happens when Angular performs **change detection**. The `TestBed.createComponent` does not trigger change detection by default.
66+
67+
### `detectChanges()`
68+
69+
You can tell the `TestBed` to perform data binding by calling `fixture.detectChanges()`.
70+
71+
Update `banner.component.spec.ts`
72+
73+
```diff
74+
....
75+
it('should display original title', () => {
76+
+ fixture.detectChanges();
77+
expect(h1.textContent).toContain(component.title());
78+
});
79+
....
80+
```
81+
82+
Delayed change detection is intentional and useful. It gives the tester an opportunity to inspect and change the state of the component before Angular initiates data binding and calls lifecycle hooks.
83+
84+
Update `banner.component.spec.ts`
85+
86+
```ts
87+
it("should display a different test title", () => {
88+
component.title.set("Other");
89+
fixture.detectChanges();
90+
expect(h1.textContent).toContain("Other");
91+
});
92+
```
93+
94+
### Automatic change detection
95+
96+
We can make that the test environment runs the test detection automatically.
97+
98+
That's possible by configuring the `TestBed` with the `ComponentFixtureAutoDetect` provider. First import it from the testing utility library:
99+
100+
```ts
101+
import { ComponentFixtureAutoDetect } from "@angular/core/testing";
102+
```
103+
104+
```ts
105+
TestBed.configureTestingModule({
106+
providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }],
107+
});
108+
```
109+
110+
> NOTE: You can also use the `fixture.autoDetectChanges()` function instead if you only want to enable automatic change detection after making updates to the state of the fixture's component. In addition, automatic change detection is on by default when using provideExperimentalZonelessChangeDetection and turning it off is not recommended.
111+
112+
Create `banner-detect-changes.component.spec.ts`
113+
114+
```ts
115+
import {
116+
ComponentFixture,
117+
TestBed,
118+
ComponentFixtureAutoDetect,
119+
} from "@angular/core/testing";
120+
import { BannerComponent } from "./banner.component";
121+
122+
describe("BannerComponent", () => {
123+
let component: BannerComponent;
124+
let fixture: ComponentFixture<BannerComponent>;
125+
let h1: HTMLElement;
126+
127+
beforeEach(() => {
128+
TestBed.configureTestingModule({
129+
providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }],
130+
imports: [BannerComponent],
131+
});
132+
fixture = TestBed.createComponent(BannerComponent);
133+
component = fixture.componentInstance;
134+
h1 = fixture.nativeElement.querySelector("h1");
135+
});
136+
137+
it("should display original title", () => {
138+
expect(h1.textContent).toContain(component.title());
139+
});
140+
141+
it("should still see original title after comp.title change", async () => {
142+
const oldTitle = component.title();
143+
const newTitle = "Other";
144+
component.title.set(newTitle);
145+
expect(h1.textContent).toContain(oldTitle);
146+
await fixture.whenStable();
147+
expect(h1.textContent).toContain(newTitle);
148+
});
149+
150+
it("should sisplay updated title after detectChanges", () => {
151+
component.title.set("Other");
152+
fixture.detectChanges();
153+
expect(h1.textContent).toContain(component.title());
154+
});
155+
});
156+
```
157+
158+
The first test shows the benefit of automatic change detection.
159+
160+
The second and third test reveal an important limitation. The Angular testing environment does not run change detection synchronously when updates happen inside the test case that changed the component's title. The test must call await fixture.whenStable to wait for another of change detection.
161+
162+
```bash
163+
npx ng test --include app/banner/banner-detect-changes.component.spec.ts
164+
```
165+
166+
> NOTE: Angular does not know about direct updates to values that are not signals. The easiest way to ensure that change detection will be scheduled is to use signals for values read in the template.
167+
168+
### Change an innput value with `dispatchEvent()`
169+
170+
To simulate user input, find the input element and set its value property.
171+
172+
But there is an essential, intermediate step.
173+
174+
Angular doesn't know that you set the input element's value property. It won't read that property until you raise the element's input event by calling `dispatchEvent()`.
175+
176+
```bash
177+
npx ng g c display --inline-style --inline-template
178+
```
179+
180+
Update `app/display/display.component.ts`
181+
182+
```ts
183+
import { Component, signal } from "@angular/core";
184+
185+
@Component({
186+
selector: "app-display",
187+
imports: [],
188+
template: `
189+
<input (input)="onChange($event)" />
190+
<span>{{ myInput() }}</span>
191+
`,
192+
styles: ``,
193+
})
194+
export class DisplayComponent {
195+
myInput = signal("");
196+
onChange(event: Event) {
197+
const inputValue = (event.target as HTMLInputElement).value;
198+
this.myInput.set(inputValue);
199+
}
200+
}
201+
```
202+
203+
Update `app/display/display.component.spec.ts`
204+
205+
```ts
206+
import { ComponentFixture, TestBed } from "@angular/core/testing";
207+
208+
import { DisplayComponent } from "./display.component";
209+
210+
describe("DisplayComponent", () => {
211+
let component: DisplayComponent;
212+
let fixture: ComponentFixture<DisplayComponent>;
213+
214+
beforeEach(async () => {
215+
await TestBed.configureTestingModule({
216+
imports: [DisplayComponent],
217+
}).compileComponents();
218+
219+
fixture = TestBed.createComponent(DisplayComponent);
220+
component = fixture.componentInstance;
221+
fixture.autoDetectChanges();
222+
});
223+
224+
it("should display the input entry", async () => {
225+
const input: HTMLInputElement =
226+
fixture.nativeElement.querySelector("input");
227+
const display: HTMLElement = fixture.nativeElement.querySelector("span");
228+
229+
input.value = "Jane";
230+
input.dispatchEvent(new Event("input"));
231+
232+
await fixture.whenStable();
233+
234+
expect(display.textContent).toBe("Jane");
235+
});
236+
});
237+
```
238+
239+
```bash
240+
npx ng test --include app/display/display.component.spec.ts
241+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {
2+
ComponentFixture,
3+
TestBed,
4+
ComponentFixtureAutoDetect,
5+
} from '@angular/core/testing';
6+
import { BannerComponent } from './banner.component';
7+
8+
describe('BannerComponent', () => {
9+
let component: BannerComponent;
10+
let fixture: ComponentFixture<BannerComponent>;
11+
let h1: HTMLElement;
12+
13+
beforeEach(() => {
14+
TestBed.configureTestingModule({
15+
providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }],
16+
imports: [BannerComponent],
17+
});
18+
fixture = TestBed.createComponent(BannerComponent);
19+
component = fixture.componentInstance;
20+
h1 = fixture.nativeElement.querySelector('h1');
21+
});
22+
23+
it('should display original title', () => {
24+
expect(h1.textContent).toContain(component.title());
25+
});
26+
27+
it('should still see original title after comp.title change', async () => {
28+
const oldTitle = component.title();
29+
const newTitle = 'Other';
30+
component.title.set(newTitle);
31+
expect(h1.textContent).toContain(oldTitle);
32+
await fixture.whenStable();
33+
expect(h1.textContent).toContain(newTitle);
34+
});
35+
36+
it('should sisplay updated title after detectChanges', () => {
37+
component.title.set('Other');
38+
fixture.detectChanges();
39+
expect(h1.textContent).toContain(component.title());
40+
});
41+
});
Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,26 @@
1-
import { ComponentFixture, TestBed } from '@angular/core/testing';
1+
import { ComponentFixture, TestBed } from "@angular/core/testing";
2+
import { BannerComponent } from "./banner.component";
23

3-
import { BannerComponent } from './banner.component';
4-
import { DebugElement } from '@angular/core';
5-
import { By } from '@angular/platform-browser';
6-
7-
describe('BannerComponent', () => {
4+
describe("BannerComponent", () => {
85
let component: BannerComponent;
96
let fixture: ComponentFixture<BannerComponent>;
7+
let h1: HTMLElement;
108

119
beforeEach(() => {
12-
TestBed.configureTestingModule({
13-
imports: [BannerComponent],
14-
});
15-
10+
TestBed.configureTestingModule({ imports: [BannerComponent] });
1611
fixture = TestBed.createComponent(BannerComponent);
1712
component = fixture.componentInstance;
13+
h1 = fixture.nativeElement.querySelector("h1");
1814
});
1915

20-
it('should create', () => {
21-
expect(component).toBeTruthy();
22-
});
23-
24-
it('should contain "banner works!"', () => {
25-
const bannerElement: HTMLElement = fixture.nativeElement;
26-
expect(bannerElement.textContent).toContain("banner works!");
27-
});
28-
29-
it("should find the <p> with fixture.debugElement.nativeElement)", () => {
30-
const bannerDe: DebugElement = fixture.debugElement;
31-
const bannerEl: HTMLElement = bannerDe.nativeElement;
32-
const p = bannerEl.querySelector("p")!;
33-
expect(p.textContent).toContain("banner works!");
16+
it("should display original title", () => {
17+
fixture.detectChanges();
18+
expect(h1.textContent).toContain(component.title());
3419
});
3520

36-
it("should find the <p> with fixture.debugElement.query(By.css)", () => {
37-
const bannerDe: DebugElement = fixture.debugElement;
38-
const paragraphDe = bannerDe.query(By.css("p"));
39-
const p: HTMLElement = paragraphDe.nativeElement;
40-
expect(p.textContent).toContain("banner works!");
21+
it("should display a different test title", () => {
22+
component.title.set("Other");
23+
fixture.detectChanges();
24+
expect(h1.textContent).toContain("Other");
4125
});
42-
});
26+
});
Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1-
import { Component } from '@angular/core';
1+
import { Component, signal } from "@angular/core";
22

33
@Component({
4-
selector: 'app-banner',
4+
selector: "app-banner",
55
imports: [],
6-
template: `
7-
<p>
8-
banner works!
9-
</p>
6+
template: `<h1>{{ title() }}</h1>`,
7+
styles: `
8+
h1 { color: green; font-size: 350%; }
109
`,
11-
styles: ``
1210
})
1311
export class BannerComponent {
14-
15-
}
12+
title = signal("Test demos");
13+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ComponentFixture, TestBed } from "@angular/core/testing";
2+
3+
import { DisplayComponent } from "./display.component";
4+
5+
describe("DisplayComponent", () => {
6+
let component: DisplayComponent;
7+
let fixture: ComponentFixture<DisplayComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [DisplayComponent],
12+
}).compileComponents();
13+
14+
fixture = TestBed.createComponent(DisplayComponent);
15+
component = fixture.componentInstance;
16+
fixture.autoDetectChanges();
17+
});
18+
19+
it("should display the input entry", async () => {
20+
const input: HTMLInputElement =
21+
fixture.nativeElement.querySelector("input");
22+
const display: HTMLElement = fixture.nativeElement.querySelector("span");
23+
24+
input.value = "Jane";
25+
input.dispatchEvent(new Event("input"));
26+
27+
await fixture.whenStable();
28+
29+
expect(display.textContent).toBe("Jane");
30+
});
31+
});

0 commit comments

Comments
 (0)