<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>

                一個完善有效的單選、多選是一項較復雜的工程。在本小節中以實現以下功能為主: * 單選生效 * 全選生效 * 列表中所有的項都被單選選中時,全選自動選中 * 全選選中時,列表中所有的項自動選中 * 全選取消時,列表中所有的項自動取消 * 列表中所有的項未被單選全部選中時,全選自動取消 在初次接觸某項功能時,TDD并不適用。在開發過程中仍需按先實現功能后進行測試的開發步驟進行。 # 單選 使用`<input type="checkbox" />`便能很輕松的生成一個選擇框,該選擇框的狀態有兩個:選中、未選中,對應的值為true、false。實現單選的方案有很多種,本文使用筆者認為實現較為簡單的一種。 在每個需要進行單選的實體上增加一個字段:`isChecked`,類型設置為boolean,默認值設置為false。這樣以來,該字段的值便可以很好的與選擇框的值相對應。比如在本例中: student/index/index.component.html ``` <tr *ngFor="let student of pageStudent.content; index as index"> <td><input type="checkbox" [ngModel]="student.isChecked" ?/></td> <td>{{index + 1}}</td> ``` * ? 將student中的isChecked字段與checkbox相綁定。 但student中我們并未規定isChecked字段,所以此時IDE會出現了一個警告錯誤: ![](https://img.kancloud.cn/c1/d6/c1d6e3a9439b816292917bfc61fe7a39_1069x104.png) 修正該錯誤的方法當然也很簡單:打開Student實體類,添加對應的字段即可。 norm/entity/student.ts ``` export class Student { ... /* 是否選中,輔助實現V層的 選中 功能① */ isChecked = false; ② ``` * ① 注釋內容:它的**作用**是什么 > 它是什么 * ② 使用`=`進行賦值操作 > 直接在前臺實體中增加isChecked字段會有一個弊端:當真實的后臺實體中存在該字段時,會與前臺的字段定義產生沖突。在筆者所在的團隊中,由于規定后臺字段不能以`is`打頭,所以原則上不會出現該字段與后臺實體沖突的問題。在實際的開發中一旦產生沖突,那么可以考慮通過以下方案之一來解決:方案一,對使用選擇框的實體,在傳入V層前進行格式化,比如在格式化過程中再增加了個字段`isCheckedClone`,使用該字段來存儲真實的后臺值;方案二,單元的定義選擇組件,將`isChecked`字段設置為可變更的,比如<yzSelect fieldName='你自己定義的字段' />。 ## 單元測試 寫點功能就測試一下,不會吃虧。找到對應的測試文件,并新建測試用例: student/index/index.component.spec.ts ``` describe('Student -> IndexComponent', () => { ... fit('單選', () => { /* 設置第一個學生的isCheck為true,并重新渲染V層 */ component.pageStudent.content[0].isChecked = true; fixture.detectChanges(); }); }); ``` 使用`ng test`啟動測試并觀察效果: ![](https://img.kancloud.cn/10/19/1019dac36cf076900c144987bead1632_467x131.png) 第一條預期選中,這說明我們上述的思路和代碼的書寫都是正確的。接下來,繼續完善單元測試以達到用代碼來保障代碼功能的目的。 student/index/index.component.spec.ts ``` describe('Student -> IndexComponent', () => { ... fit('單選', () => { const trDebugElement = fixture.debugElement.query(By.css(`table tr:nth-child(2)`)); const checkBoxDebugElement = trDebugElement.query(By.css('input[type=checkBox]')); const checkBoxElement: HTMLInputElement = checkBoxDebugElement.nativeElement; console.log(checkBoxElement); console.log(checkBoxElement.checked); ? expect(checkBoxElement.checked).toBe(false); ? /* 設置第一個學生的isCheck為true,并重新渲染V層 */ component.pageStudent.content[0].isChecked = true; fixture.detectChanges(); /* 斷言checkBox的值為true */ expect(checkBoxElement.checked).toEqual(true); ① }); }); ``` * ? checkBox選中則checked屬性為true,未選中則為false * ? 默認未選中,值為false * ① 選中后checkBox的值為true 但單元測試的結果卻不如人意: ![](https://img.kancloud.cn/78/d7/78d729f0d184e6d5a990840b3a571c1a_479x309.png) 這就有點意思了,通過觀察界面我們可以確認該checkBox就是選中了,這種狀態下checkBox的值應該為true。但最終checkbox的value卻為false。 > 這可能是angular的一個bug。筆者開始嘗試由官方文檔中找到答案,但官方文檔并未對checkbox進行ngModel的使用進行單獨說明。且通過其它的示例代碼,我們確認當前使用ngModel來進行數據綁定是沒有問題的。可能開發ngModel的人并沒有充分的測試此問題,也可能是開發ngModel的人并不推薦我們在checkBox中使用ngModel(但官方文檔卻未同步)。問題產生的原因筆者通過萬能的google也沒有找到答案,但有幸運的找到了另一種寫法。 可能是官方并不推薦我們這么做吧,我們在綁定checkBox時換一種寫法,由ngModel變更為`checked`: student/index/index.component.html ``` <tr *ngFor="let student of pageStudent.content; index as index"> <td><input type="checkbox" [checked]="student.isChecked" /></td> <td>{{index + 1}}</td> ``` 單元測試順利通過。由此我們得出來一個非常重要的結論:**對checkBox進行數據綁定時,應該用checked而非ngModel**。而該checked即為HTMLInputElement的checked屬性,在此相當于直接改變了checkbox的checked屬性。如此我們又得到了以下結論:**當angular缺失某此功能時,可以直接使用`[xxx]`的方法,設置其原生`xxx`屬性** ## 雙向綁定 剛剛測試了C層向V層綁定是成功的,那么當V層中checkBox被用戶點擊后,是否可以自動將值傳給對應的student.isChecked呢? student/index/index.component.spec.ts ``` describe('Student -> IndexComponent', () => { ... fit('單選點擊后綁定到C層', () => { const trDebugElement = fixture.debugElement.query(By.css(`table tr:nth-child(2)`)); const checkBoxDebugElement = trDebugElement.query(By.css('input[type=checkBox]')); const checkBoxElement: HTMLInputElement = checkBoxDebugElement.nativeElement; expect(checkBoxElement.checked).toBe(false); /* 點擊第一個學生對應的checkBox,斷言checkBox的值是true,同時對應學生的相應字段值為true */ checkBoxElement.click(); ? expect(checkBoxElement.checked).toBeTruthy(); ① expect(component.pageStudent.content[0].isChecked).toBeTruthy(); ② }); }); ``` * ? 點擊該checkBox,該方法將自動重新渲染該checkbox,但不會重新渲染整個V層 * ① 斷言checkBox的值為true * ② 斷言對應的字段值為true 單元測試結果: ``` Error: Expected false to be truthy. ``` 同時提示該錯誤發生在`② 斷言對應的字段值為true`,也就是說`[checked]`屬性無法實現雙向數據綁定。的確,在前面我們講過`[xxx]`代表當C層的值單向綁定到V層,而V層的值綁定到C層則需要通過`()`觸發C層的相關方法。checkbox的傳值也是如此,當checkbox被點擊時,會通過`change($event)`向外彈射最新的值。 student/index/index.component.html ``` <tr *ngFor="let student of pageStudent.content; index as index"> <td><input type="checkbox" [checked]="student.isChecked" (change)="onCheckBoxChange($event?, student?)"? /></td> <td>{{index + 1}}</td> ``` * ? change事件綁定到C層的checkBoxChanged方法 * ? $event即為change彈射的值 * ? 對應當前學生 對應C層代碼為: student/index/index.component.ts ``` export class IndexComponent implements OnInit { ... /** * 單選框被用戶點擊時 * @param $event 彈射值 * @param student 當前學生 */ onCheckBoxChange($event, student: Student) { console.log($event); ① } ``` * ① 未熟練使用前在控制臺打印相關的對象,絕對是件事半功倍的操作 觀察控制臺發現,原來我們需要的東西在這: ![](https://img.kancloud.cn/82/5c/825cd5e6f85adf1ce73e809913fdc6af_363x427.png) 于是為了更加清晰的了解自己操作的數據對象,在相應的代碼中補充類型并進行相應的強制轉換: student/index/index.component.ts ``` export class IndexComponent implements OnInit { ... /** * 單選框被用戶點擊時 * @param $event 彈射值 * @param student 當前學生 */ onCheckBoxChange($event: Event①, student: Student) { const checkbox = $event.target as HTMLInputElement②; student.isChecked = checkbox.checked; ③ } ``` * ① 聲明類型為Event * ② 將類型強制轉換為HTMLInputElement * ③ 使用checkbox的值來設置student的isChecked字段 單元測試如期通過。 # 全選 有了單選的經驗,全選初始化便相對簡單了。 student/index/index.component.html ``` <table> <tr> <th>選擇</th> ? <th><input type="checkbox" [checked]="isCheckedAll" (change)="onCheckAllBoxChange($event)" /></th> ? <th>序號</th> ``` 對應增加C層的屬性及方法: student/index/index.component.ts ``` export class IndexComponent implements OnInit { ... /* 是否全部選中 */ isCheckedAll = false; ... /** * 全選框被用戶點擊時觸發 * @param $event checkBox彈射值 */ onCheckAllBoxChange($event: Event) { const checkbox = $event.target as HTMLInputElement; this.isCheckedAll = checkbox.checked; } ``` ## 單元測試 單元測試就是功能點的測試,在此暫不考慮全選與單選的關聯信息,僅就多選進行雙向數據綁定測試。 ### C->V student/index/index.component.spec.ts ``` describe('Student -> IndexComponent', () => { ... fit('多選C->V', () => { /* 獲取到 全選 并斷言其狀態為:未選中 */ const trDebugElement = fixture.debugElement.query(By.css(`table tr:nth-child(1)`)); const checkBoxDebugElement = trDebugElement.query(By.css('input[type=checkBox]')); const checkBoxElement: HTMLInputElement = checkBoxDebugElement.nativeElement; expect(component.isCheckedAll).toBeFalsy(); expect(checkBoxElement.checked).toBe(false); /* 改變C層的值,斷言綁定生效 */ component.isCheckedAll = true; fixture.detectChanges(); expect(component.isCheckedAll).toBeTruthy(); }); }); ``` * ★ 因代碼與前面測試單元時高度重度,不再重復說明。 ### V->C student/index/index.component.spec.ts ``` describe('Student -> IndexComponent', () => { ... fit('多選V->C', () => { /* 獲取到 全選 并斷言其狀態為:未選中 */ const trDebugElement = fixture.debugElement.query(By.css(`table tr:nth-child(1)`)); const checkBoxDebugElement = trDebugElement.query(By.css('input[type=checkBox]')); const checkBoxElement: HTMLInputElement = checkBoxDebugElement.nativeElement; expect(component.isCheckedAll).toBeFalsy(); /* 第一次點擊 false -> true */ checkBoxElement.click(); expect(component.isCheckedAll).toBeTruthy(); /* 再次點擊 true -> false */ checkBoxElement.click(); expect(component.isCheckedAll).toBeFalsy(); }); }); ``` * ★ 因代碼與前面測試單元時高度重度,不再重復說明。 # 單選、多選聯動 既然是聯動,則說明兩者互相影響。單元會影響多選,多選也會影響單選。那么在進行開發時便可以將此功能的粒度進一步縮小為:單元對多選的影響、多選對單元的影響。 ## 多選對單選的影響 當多選選中或是取消選中時,單選應該全部選中或是全部取消選中。也就是說多選的狀態的變更應該對單選產生影響,而單元的狀態又與C層進行綁定,所以此問題便轉換為:多選產生事件時,應該對應student.isChecked字段的值。 student/index/index.component.ts ``` export class IndexComponent implements OnInit { ... onCheckAllBoxChange($event: Event) { const checkbox = $event.target as HTMLInputElement; this.isCheckedAll = checkbox.checked; this.pageStudent.content.forEach((student) => { ① student.isChecked = checkbox.checked; }); } ``` * ① 循環對學生的isChecked字段賦值 ### 單元測試 student/index/index.component.spec.ts ``` describe('Student -> IndexComponent', () => { ... fit('多選V->C', () => { /* 獲取到 全選 并斷言其狀態為:未選中 */ const trDebugElement = fixture.debugElement.query(By.css(`table tr:nth-child(1)`)); const checkBoxDebugElement = trDebugElement.query(By.css('input[type=checkBox]')); const checkBoxElement: HTMLInputElement = checkBoxDebugElement.nativeElement; expect(component.isCheckedAll).toBeFalsy(); /* 第一次點擊 false -> true */ checkBoxElement.click(); expect(component.isCheckedAll).toBeTruthy(); component.pageStudent.content .forEach((student) => { expect(student.isChecked).toBeTruthy(); ① }); /* 再次點擊 true -> false */ checkBoxElement.click(); expect(component.isCheckedAll).toBeFalsy(); component.pageStudent.content .forEach((student) => { expect(student.isChecked).toBeFalsy(); ① }); }); }); ``` * ① 斷言每個學生的選中狀態與全選的相同 ## 單選對多選的影響 用戶點擊某個單選框時,單選對多選的影響有兩種:① 如果當前單元值為false,則應該取消全選,全選值也應該為false ② 如果當前單選值為true,則應該對所有的學生進行遍歷,只要有一個學生的isChecked值為false,則全選值為false。 > 還有很多的算法能夠滿足當前要求。比如建立個選定學生數量計數器,每次單選選中,計數器+1,取消選中-1。然后計算計數器與當前學生的總數值是否相等?相等,則全選選中,不相等則取消選中;或是也可以建立個數組,把選中的學生添加到這個單獨的數組中。 則功能代碼如下: student/index/index.component.ts ``` export class IndexComponent implements OnInit { ... onCheckBoxChange($event: Event, student: Student) { const checkbox = $event.target as HTMLInputElement; student.isChecked = checkbox.checked; if (checkbox.checked) { let checkedAll = true; ① this.pageStudent.content.forEach((value) => { if (!value.isChecked) { checkedAll = false; ② } }); this.isCheckedAll = checkedAll; ③ } else { this.isCheckedAll = false; ④ } } ``` * ① 定義臨時變量 * ② 如果有學生未選中,則設置該臨時變量的值為false。該值可能被多次冗余執行,不過這并不會影響到代碼的執行效果 * ③ 設置全選的值 * ④ 如果為取消選中,則直接設置全選的值為false ### 單元測試 student/index/index.component.spec.ts ``` describe('Student -> IndexComponent', () => { ... fit('點擊單選對多選值的影響', () => { for (let i = 2; i <= 3; i++) { /* 依次點擊2個student的單選 */ const trDebugElement = fixture.debugElement.query(By.css(`table tr:nth-child(${i})`)); const checkBoxDebugElement = trDebugElement.query(By.css('input[type=checkBox]')); const checkBoxElement: HTMLInputElement = checkBoxDebugElement.nativeElement; checkBoxElement.click(); /* 按是否為最后一個學生進行不同的斷言 */ if (i === 3) { expect(component.isCheckedAll).toBeTruthy(); ① checkBoxElement.click(); ② expect(component.isCheckedAll).toBeFalsy(); } else { expect(component.isCheckedAll).toBeFalsy(); ③ } } }); ``` * ① 點擊最后一個學生,則全選選中 * ② 再次點擊(在全選情況下取消一個),全選取消選中 * ③ 點擊非最后的學生,全選不選中 最后進行整個項目的單元測試,以保證當前功能的新增未對歷史功能或單元測試功能造成影響。 # 參考文檔 | 名稱 | 鏈接 | 預計學習時長(分) | | --- | --- | --- | | 源碼地址 | [https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.9](https://github.com/mengyunzhi/spring-boot-and-angular-guild/releases/tag/step4.6.9) | - | | HTMLInputElement | [https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement) | 5 |
                  <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>

                              哎呀哎呀视频在线观看