|
| 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 | +``` |
0 commit comments