<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                本節我們以教師選擇組件為例,展示如何自定義一個`FormControl`。 Angular內置的`FormControl`僅支持綁定到原生的html表單項上,比如`input`、`select`等。對于一些自定義的組件,若想也像`input`一樣使用響應式表單,則需要經過兩步: - 繼續相應的接口,以使得當前組件提供`FormControl`所需的一些方法。 - 將當前組件聲明為響應式表單項,以使響應式表單能夠解析當前組件對應的`selector`。 ## 測試 在寫功能之前先寫測試的模式被稱為`TDD`,全稱為:`Test-Driven Development`,即**測試驅動開發**,網上有很多關于`TDD`的討論,有興趣的同學可以搜索來加深下了解。在此我們嘗試使用`TDD`的模式來開發當前功能。為了規避一些其它的測試代碼帶來的問題,最大限度的減少一些在學習初期不必要的**麻煩**,我們來到教師選擇組件所在文件夾中,新建一個測試文件`klass-select-form-control.component.spec.ts`,并初始化如下: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select-form-control.component.spec.ts import {KlassSelectComponent} from './klass-select.component'; import {TestBed} from '@angular/core/testing'; import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing'; import {TeacherMockApi} from '../../mock-api/teacher.mock.api'; describe('KlassSelectComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [KlassSelectComponent], imports: [ HttpClientModule, FormsModule, ReactiveFormsModule ], providers: [ { provide: HTTP_INTERCEPTORS, multi: true, useClass: MockApiTestingInterceptor.forRoot([ TeacherMockApi ]) } ] }) .compileComponents(); }); fit('響應式表單', () => { }); }); ``` 本次測試的目的在于:當前組件作用子組件使用時,是否支持響應式表單的`FormConrol`。所以在測試過程中,我們需要來搭建當前組件為子組件的測試環境。若要實現該功能,則需要建立一個父組件。而既然是測試,我們在測試文件中來臨時搭建一個父組件好了: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select-form-control.component.spec.ts @@ -1,9 +1,17 @@ import {KlassSelectComponent} from './klass-select.component'; import {TestBed} from '@angular/core/testing'; import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; -import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing'; import {TeacherMockApi} from '../../mock-api/teacher.mock.api'; +import {Component} from '@angular/core'; + +@Component({ + template: '<h1>Test:</h1><app-klass-select [formControl]="teacherIdFormControl"></app-klass-select>' +}) +class TestComponent { + teacherIdFormControl = new FormControl(); +} describe('KlassSelectComponent', () => { beforeEach(async () => { ``` 如上代碼便創建了一個包含有`app-klass-select`組件的父組件`TestComponent`。 在定義該組件時: - 由于該組件并不會做為子組件使用,所以我們并沒有設置其`selector`; - 由于該組件并不需要任何樣式,所以我們并沒有設置其`styleUrls`; - 由于該組件的V層代碼非常的簡單,所以我們移除了`templateUrl`,取而代之的是`template`,并直接在`template`書寫了V層; - 由于當前組件僅在當前文件中使用,所以我們移除了`export`關鍵字,在定義組件時,使用的是`class TestComponent`而非`export class TestComponent`; - 由于當前測試用組件并不需要進行復雜初始化,所以刪除了對`OnInit`接口的實現。 沒錯,這就是一個縮小版本的組件。雖然小,但功能正常。 ### 將組件加入模塊 與其它被測試的模塊相同,要想被動態測試模塊認識,則需要將組件加入到動態測試模塊中: ```typescript describe('KlassSelectComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [KlassSelectComponent], + declarations: [KlassSelectComponent, TestComponent], imports: [ HttpClientModule, FormsModule, ``` ### 創建組件 在單面的章節對動態組件進行分析時,我們已經接觸過了`TestBed`是如何創建動態測試模塊中的某個組件的。上于本次要創建的`TestComponent`在當前單元測試文件中僅用一次,所以將創建該組件的過程直接寫到測試用例的相關方法上: ```typescript fit('響應式表單', () => { // 創建一個組件夾具(容器),這就像我們要測試顯卡是否正常功能時,需要有一臺供顯卡工作的電腦一樣。 // testFixture便是TestComponent這塊顯卡賴以工作的電腦 const fixture = TestBed.createComponent(TestComponent); // 獲取testFixture這臺電腦上的testComponent顯卡 const component = fixture.componentInstance; // 調用detectChanges()渲染V層,開始渲染V層中的子組件。 // 由于當前Test組件未請求后臺,所以省略了getTestScheduler().flush(); // 當然了,寫上也無防 fixture.detectChanges(); ?? // 模擬返回數據后,進行變更檢測重新渲染子組件V層 getTestScheduler().flush(); fixture.detectChanges(); }); ``` 這里的`fixture.detectChanges()`很重要,該方法的作用是渲染`Test`組件的V層,而子組件正是在渲染該V層時被Angular發現的。Angular發現子組件`app-klass-select `后,才開始渲染`KlassSelectComponent`組件,即而發生數據請求。 終止`ng t`后重新啟動一下`ng t`,效果如下: ![image-20210406105011967](https://img.kancloud.cn/6d/62/6d6274f243104d82dbde2fb036081660_2006x212.png) 此時單元測試報了`No value accessor`的異常,這是由于響應式表單在處理`app-klass-select`上的`formControl`時要調用相關的`value accessor`(值處理器)。 ## 實現接口 響應式表單預調用了`value accessor`被規定于`@angular/forms`中的`ControlValueAccessor`接口中,教師選擇組件僅需要實現該接口,便可以借助IDE快速的填充上相關的方法: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts @@ -1,7 +1,7 @@ import {Component, OnInit, EventEmitter, Output, Input} from '@angular/core'; import {Teacher} from '../../entity/teacher'; import {HttpClient} from '@angular/common/http'; -import {FormControl} from '@angular/forms'; +import {ControlValueAccessor, FormControl} from '@angular/forms'; @Component({ @@ -9,7 +9,7 @@ import {FormControl} from '@angular/forms'; templateUrl: './klass-select.component.html', styleUrls: ['./klass-select.component.css'] }) -export class KlassSelectComponent implements OnInit { +export class KlassSelectComponent implements OnInit, ControlValueAccessor { teachers = new Array<Teacher>(); teacherId = new FormControl(); ``` 此時我們把鼠標移至KlassSelectComponent名稱上,按提示進行點擊,便可快速的生成相關方法: ![image-20210406105524867](https://img.kancloud.cn/ba/de/badeff7f9199929f0d1de135ee8507b3_2284x376.png) 生成方法如下: ```typescript @@ -25,6 +24,18 @@ export class KlassSelectComponent implements OnInit, ControlValueAccessor { constructor(private httpClient: HttpClient) { } + writeValue(obj: any): void { + throw new Error('Method not implemented.'); + } + + registerOnChange(fn: any): void { + throw new Error('Method not implemented.'); + } + + registerOnTouched(fn: any): void { + throw new Error('Method not implemented.'); + } + ngOnInit(): void { // 關注teacherId this.teacherId.valueChanges ``` > ? 除上述三個方法外,ControValueAccessor中還存在一個可選的方法 [**setDisabledState**(isDisabled: boolean)?: void](https://angular.cn/api/forms/ControlValueAccessor#setDisabledState)用于設置組件的**disabled**狀態。 在此,我們僅需要`writeValue`及`registerOnChange`方法,兩個方法的作用如下: ```typescript @@ -24,11 +24,22 @@ export class KlassSelectComponent implements OnInit, ControlValueAccessor { constructor(private httpClient: HttpClient) { } - writeValue(obj: any): void { + /** + * 將FormControl中的值通過此方法寫入 + * FormControl的值每變換一次,該方法將被重新執行一次 + * 相當于@Input() set xxx + * @param obj 此類型取決于當前組件的接收類型,比如此時我們接收一個類型為number的teacherId + */ + writeValue(obj: number): void { throw new Error('Method not implemented.'); } - registerOnChange(fn: any): void { + /** + * 組件需要向父組件彈值時,直接調用參數中的fn方法 + * 相當于@Output() + * @param fn 此類型取決于當前組件的彈出值類型,比如我們當前將彈出一個類型為number的teacherId + */ + registerOnChange(fn: (data: number) => void): void { throw new Error('Method not implemented.'); } ``` 成功實現接口,并添加相應的方法后,接下來我們需要通過聲明的方法來使用響應式表單認識當前組件。 ## 聲明 在使用攔截器時我們使用了`provide`以及`useClass`來將` MockApiTestingInterceptor.forRoot()`成功的聲明成了一個HTTP攔截器(HTTP_INTERCEPTORS)。在此聲明組件支持`FormControl`也是同樣的道理: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts @@ -1,12 +1,18 @@ -import {Component, OnInit, EventEmitter, Output, Input} from '@angular/core'; +import {Component, OnInit, EventEmitter, Output, Input, forwardRef} from '@angular/core'; import {Teacher} from '../../entity/teacher'; import {HttpClient} from '@angular/common/http'; -import {ControlValueAccessor, FormControl} from '@angular/forms'; +import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms'; @Component({ selector: 'app-klass-select', templateUrl: './klass-select.component.html', - styleUrls: ['./klass-select.component.css'] + styleUrls: ['./klass-select.component.css'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, multi: true, + useExisting: forwardRef(() => KlassSelectComponent) ?? + } + ] }) ``` 在聲明攔截器時,使用的是`useClass`,注意在這使用`useExisting` ??。 `forwardRef()`是一個方法,該方法中接收了一個回調函數`() => KlassSelectComponent`,該回調函數將`KlassSelectComponent`作為了返回值。該方法的作用是防止在`KlassSelectComponent`引用`KlassSelectComponent`而引發的引用循環(了解即可)。 > 剪頭函數:`() => KlassSelectComponent`等價于:`() => return KlassSelectComponent`。 響應式表單在解析`FormControl`時將調用這個回調方法 : ```typescript providers: [ { provide: NG_VALUE_ACCESSOR, multi: true, - useExisting: forwardRef(() => KlassSelectComponent) + useExisting: forwardRef(() => { + console.log('useExisting->forwardRef中的回調方法被調用一次'); + return KlassSelectComponent; + }) } ] }) ``` ![image-20210406142633858](https://img.kancloud.cn/0f/51/0f51842cf6a7acc576de3c109f5041a9_1104x210.png) 此時響應式表單便認識了當前的子組件為`FormControl`,不再報`No value accessor`異常了。 ![image-20210406144450845](https://img.kancloud.cn/04/1d/041d73bae510dbcc58511397e973b7be_1438x184.png) 該異常是我們使用IDE自動生成`writeValue()`方法時填充的語句: ```typescript writeValue(obj: number): void { throw new Error('Method not implemented.'); ?? } ``` 報此異常說明方法被成功的調用了。 ## 完成功能 參考`@Input()`、`@Output()`書寫功能代碼如下: ```typescript /** * 將FormControl中的值通過此方法寫入 * FormControl的值每變換一次,該方法將被重新執行一次 * 相當于@Input() set xxx * @param obj 此類型取決于當前組件的接收類型,比如此時我們接收一個類型為number的teacherId */ writeValue(obj: number): void { console.log('writeValue is called'); this.teacherId.setValue(obj); } /** * 組件需要向父組件彈值時,直接調用參數中的fn方法 * 相當于@Output() * @param fn 此類型取決于當前組件的彈出值類型,比如我們當前將彈出一個類型為number的teacherId */ registerOnChange(fn: (data: number) => void): void { console.log(`registerOnChange is called`); this.teacherId.valueChanges .subscribe(data => fn(data)); } registerOnTouched(fn: any): void { console.warn('registerOnTouched not implemented'); } ``` 如果我們在特定的方法上加一些輸出,則會更加清晰的明了Angular的執行過程: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select-form-control.component.spec.ts @@ -4,14 +4,18 @@ import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing'; import {TeacherMockApi} from '../../mock-api/teacher.mock.api'; -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {getTestScheduler} from 'jasmine-marbles'; @Component({ template: '<h1>Test:</h1><app-klass-select [formControl]="teacherIdFormControl"></app-klass-select>' }) -class TestComponent { +class TestComponent implements OnInit { teacherIdFormControl = new FormControl(); + + ngOnInit(): void { + console.log('父組件初始化'); + } } ``` 教師選擇組件: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select.component.ts @@ -60,12 +60,16 @@ export class KlassSelectComponent implements OnInit, ControlValueAccessor { } ngOnInit(): void { + console.log('教師選擇組件初始化'); // 關注teacherId this.teacherId.valueChanges .subscribe((data: number) => this.beChange.emit(data)); // 獲取所有教師 this.httpClient.get<Array<Teacher>>('teacher') .subscribe( - teachers => this.teachers = teachers); + teachers => { + this.teachers = teachers; + console.log('教師選擇組件接收到了數據'); + }); } } ``` 單元測試代碼: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select-form-control.component.spec.ts @@ -4,14 +4,18 @@ import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms'; import {MockApiTestingInterceptor} from '@yunzhi/ng-mock-api/testing'; import {TeacherMockApi} from '../../mock-api/teacher.mock.api'; -import {Component} from '@angular/core'; +import {Component, OnInit} from '@angular/core'; import {getTestScheduler} from 'jasmine-marbles'; @Component({ template: '<h1>Test:</h1><app-klass-select [formControl]="teacherIdFormControl"></app-klass-select>' }) -class TestComponent { +class TestComponent implements OnInit { teacherIdFormControl = new FormControl(); + + ngOnInit(): void { + console.log('父組件初始化'); + } } describe('KlassSelectComponent', () => { @@ -38,6 +42,7 @@ describe('KlassSelectComponent', () => { fit('響應式表單', () => { // 創建一個組件夾具(容器),這就像我們要測試顯卡是否正常功能時,需要有一臺供顯卡工 作的電腦一樣。 // testFixture便是TestComponent這塊顯卡賴以工作的電腦 + console.log('開始創建父組件'); const fixture = TestBed.createComponent(TestComponent); // 獲取testFixture這臺電腦上的testComponent顯卡 @@ -45,10 +50,13 @@ describe('KlassSelectComponent', () => { // 調用detectChanges()渲染V層,開始渲染V層中的子組件。 // 由于當前Test組件未請求后臺,所以省略了getTestScheduler().flush(); // 當然了,寫上也無防 + console.log('首次渲染組件'); fixture.detectChanges(); // 模擬返回數據后,進行變更檢測重新渲染子組件V層 + console.log('觸發后臺模擬數據發送'); getTestScheduler().flush(); + console.log('重新渲染組件'); fixture.detectChanges(); }); }); ``` 控制臺如下: ![image-20210406151908337](https://img.kancloud.cn/2f/21/2f210c45e44d52d571a0524e9d2f664f_1106x472.png) ## 測試 最后我們在父組件中完成組件的初始化,并增加一個方法來顯示組件中`FormControl`的值以確認子組件工作正常: ```typescript +++ b/first-app/src/app/clazz/klass-select/klass-select-form-control.component.spec.ts @@ -8,14 +8,18 @@ import {Component, OnInit} from '@angular/core'; import {getTestScheduler} from 'jasmine-marbles'; @Component({ - template: '<h1>Test:</h1><app-klass-select [formControl]="teacherIdFormControl"></app-klass-select>' + template: '<h1 (click)="onTest()">Test:</h1><app-klass-select [formControl]="teacherIdFormControl"></app-klass-select>' }) class TestComponent implements OnInit { - teacherIdFormControl = new FormControl(); + teacherIdFormControl = new FormControl(1); ngOnInit(): void { console.log('父組件初始化'); } + + onTest(): void { + console.log('teacherId值為', this.teacherIdFormControl.value); + } } describe('KlassSelectComponent', () => { ``` 最終效果一,自動選擇教師: ![image-20210406152225884](https://img.kancloud.cn/7d/66/7d669ce5e41c450f20b4fd1671076661_1050x228.png) 最終效果二,選擇其它教師后點擊`Test`成功打印選擇的教師ID: ![image-20210406152346046](https://img.kancloud.cn/cd/f2/cdf26e47b11417f204f7d5f65075348c_542x88.png) ## 本節作業 本節中我們在測試組件中引入了子父組件`app-klass-select`,這使得`useExisting`中`forwardRef`中的回調函數被調用了一次。請嘗試回答以下問題: - 如果父組件未引入子組件`app-klass-select`,`useExisting`中`forwardRef`中的回調函數會被調用嗎? - 如果父組件引入了多次組件`app-klass-select`,`useExisting`中`forwardRef`中的回調函數會被調用幾次? - 驗證自己的猜測。 - | 名稱 | 鏈接 | | ---------------------------------------- | ------------------------------------------------------------ | | ControlValueAccessor | [https://angular.cn/api/forms/ControlValueAccessor](https://angular.cn/api/forms/ControlValueAccessor) | | NG_VALUE_ACCESSOR | [https://angular.cn/api/forms/NG_VALUE_ACCESSOR](https://angular.cn/api/forms/NG_VALUE_ACCESSOR) | | DefaultValueAccessor | [https://angular.cn/api/forms/DefaultValueAccessor](https://angular.cn/api/forms/DefaultValueAccessor) | | 別名提供者:`useExisting` | [https://angular.cn/guide/dependency-injection-in-action#alias-providers-useexisting](https://angular.cn/guide/dependency-injection-in-action#alias-providers-useexisting) | | 使用一個前向引用(*forwardRef*)來打破循環 | [https://angular.cn/guide/dependency-injection-in-action#break-circularities-with-a-forward-class-reference-forwardref](https://angular.cn/guide/dependency-injection-in-action#break-circularities-with-a-forward-class-reference-forwardref) | | 本節源碼 | [https://github.com/mengyunzhi/angular11-guild/archive/step6.4.5.zip](https://github.com/mengyunzhi/angular11-guild/archive/step6.4.5.zip) | ##
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看