การทำงานกับ Angular 4 Forms: Nesting and Input Validation

เผยแพร่แล้ว: 2022-03-11

บนเว็บ องค์ประกอบการป้อนข้อมูลของผู้ใช้แรกสุด ได้แก่ ปุ่ม ช่องทำเครื่องหมาย การป้อนข้อความ และปุ่มตัวเลือก จนถึงทุกวันนี้ องค์ประกอบเหล่านี้ยังคงถูกใช้ในเว็บแอปพลิเคชันสมัยใหม่ แม้ว่ามาตรฐาน HTML จะไปไกลกว่าคำจำกัดความแรกเริ่ม และตอนนี้อนุญาตให้มีการโต้ตอบแฟนซีทุกประเภท

การตรวจสอบอินพุตของผู้ใช้เป็นส่วนสำคัญของเว็บแอปพลิเคชันที่มีประสิทธิภาพ

แบบฟอร์มในแอปพลิเคชันเชิงมุมสามารถรวมสถานะของอินพุตทั้งหมดที่อยู่ภายใต้แบบฟอร์มนั้นและให้สถานะโดยรวม เช่น สถานะการตรวจสอบความถูกต้องของแบบฟอร์มแบบเต็ม วิธีนี้มีประโยชน์มากในการตัดสินใจว่าจะยอมรับหรือปฏิเสธการป้อนข้อมูลของผู้ใช้โดยไม่ต้องตรวจสอบแต่ละอินพุตแยกกัน

การตรวจสอบความถูกต้องของอินพุตฟอร์ม 4 มุม

ในบทความนี้ คุณจะได้เรียนรู้วิธีทำงานกับแบบฟอร์มและดำเนินการตรวจสอบแบบฟอร์มอย่างง่ายดายในแอปพลิเคชัน Angular ของคุณ

ใน Angular 4 มีสองประเภทของแบบฟอร์มที่พร้อมใช้งาน: แบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลตและแบบโต้ตอบ เราจะพูดถึงแบบฟอร์มแต่ละประเภทโดยใช้ตัวอย่างเดียวกันเพื่อดูว่าสามารถนำสิ่งเดียวกันไปใช้ในรูปแบบต่างๆ ได้อย่างไร ต่อมาในบทความ เราจะมาดูวิธีการใหม่ในการตั้งค่าและทำงานกับแบบฟอร์มที่ซ้อนกัน

แบบฟอร์มเชิงมุม 4

ใน Angular 4 สถานะสี่ต่อไปนี้มักใช้ในแบบฟอร์ม:

  • ถูกต้อง – สถานะของความถูกต้องของการควบคุมแบบฟอร์มทั้งหมด เป็นจริง หากการควบคุมทั้งหมดถูกต้อง

  • ไม่ถูกต้อง – ผกผันของ valid ; จริงถ้าการควบคุมบางอย่างไม่ถูกต้อง

  • บริสุทธิ์ – ให้สถานะเกี่ยวกับ "ความสะอาด" ของแบบฟอร์ม จริงถ้าไม่มีการปรับเปลี่ยนการควบคุม

  • สกปรก – ผกผันของเก่า pristine ; จริงหากมีการแก้ไขการควบคุมบางอย่าง

มาดูตัวอย่างพื้นฐานของแบบฟอร์มกัน:

 <form> <div> <label>Name</label> <input type="text" name="name"/> </div> <div> <label>Birth Year</label> <input type="text" name="birthYear"/> </div> <div> <h3>Location</h3> <div> <label>Country</label> <input type="text" name="country"/> </div> <div> <label>City</label> <input type="text" name="city"/> </div> </div> <div> <h3>Phone numbers</h3> <div> <label>Phone number 1</label> <input type="text" name="phoneNumber[1]"/> <button type="button">remove</button> </div> <button type="button">Add phone number</button> </div> <button type="submit">Register</button> <button type="button">Print to console</button> </form>

ข้อกำหนดสำหรับตัวอย่างนี้มีดังต่อไปนี้:

  • ชื่อ - จำเป็นและไม่ซ้ำกันในหมู่ผู้ใช้ที่ลงทะเบียนทั้งหมด

  • birthYear - ควรเป็นตัวเลขที่ถูกต้องและผู้ใช้ต้องมีอย่างน้อย 18 และน้อยกว่า 85 ปี

  • ประเทศ - เป็นข้อบังคับ และเพื่อทำให้เรื่องซับซ้อนขึ้นเล็กน้อย เราต้องการการตรวจสอบว่าหากประเทศเป็นฝรั่งเศส เมืองนั้นจะต้องเป็นปารีส (สมมติว่าบริการของเราให้บริการในปารีสเท่านั้น)

  • หมายเลขโทรศัพท์ – หมายเลขโทรศัพท์แต่ละหมายเลขต้องเป็นไปตามรูปแบบที่กำหนด ต้องมีหมายเลขโทรศัพท์อย่างน้อยหนึ่งหมายเลข และอนุญาตให้ผู้ใช้เพิ่มหมายเลขโทรศัพท์ใหม่หรือลบหมายเลขโทรศัพท์ที่มีอยู่ได้

  • ปุ่ม "ลงทะเบียน" จะเปิดใช้งานก็ต่อเมื่ออินพุตทั้งหมดถูกต้องและเมื่อคลิกแล้วจะเป็นการส่งแบบฟอร์ม

  • “พิมพ์ไปยังคอนโซล” เพียงพิมพ์ค่าของอินพุตทั้งหมดไปยังคอนโซลเมื่อคลิก

เป้าหมายสูงสุดคือการดำเนินการตามข้อกำหนดที่กำหนดไว้อย่างเต็มที่

แบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลต

แบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลตนั้นคล้ายกับแบบฟอร์มใน AngularJS (หรือ Angular 1 ตามที่บางคนอ้างถึง) ดังนั้น คนที่ทำงานกับแบบฟอร์มใน AngularJS จะคุ้นเคยกับวิธีการทำงานกับแบบฟอร์มนี้เป็นอย่างดี

ด้วยการแนะนำโมดูลใน Angular 4 จะมีการบังคับใช้ว่าแบบฟอร์มแต่ละประเภทจะอยู่ในโมดูลที่แยกจากกัน และเราต้องกำหนดอย่างชัดเจนว่าเราจะใช้ประเภทใดโดยการนำเข้าโมดูลที่เหมาะสม โมดูลนั้นสำหรับฟอร์มที่ขับเคลื่อนด้วยเทมเพลตคือ FormsModule ที่ถูกกล่าวว่า คุณสามารถเปิดใช้งานแบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลตได้ดังต่อไปนี้:

 import {FormsModule} from '@angular/forms' import {NgModule} from '@angular/core' import {BrowserModule} from '@angular/platform-browser' import {AppComponent} from 'src/app.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent], bootstrap: [ AppComponent ] }) export class AppModule {}

ดังที่แสดงในข้อมูลโค้ดนี้ ก่อนอื่นเราต้องนำเข้าโมดูลเบราว์เซอร์เนื่องจาก "ให้บริการที่จำเป็นในการเปิดและเรียกใช้แอปเบราว์เซอร์" (จากเอกสาร Angular 4) จากนั้นเรานำเข้า FormsModule ที่จำเป็นเพื่อเปิดใช้งานแบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลต และสุดท้ายคือการประกาศองค์ประกอบรูท AppComponent ซึ่งในขั้นตอนต่อไปเราจะใช้แบบฟอร์ม

โปรดจำไว้ว่าในตัวอย่างนี้และตัวอย่างต่อไปนี้ คุณต้องแน่ใจว่าแอปได้รับการบู๊ตอย่างถูกต้องโดยใช้เมธอด platformBrowserDynamic

 import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {AppModule} from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);

เราสามารถสรุปได้ว่า AppComponent (app.component.ts) ของเรามีลักษณะดังนี้:

 import {Component} from '@angular/core' @Component({ selector: 'my-app', templateUrl: 'src/app.component.tpl.html' }) export class AppComponent { }

ที่เทมเพลตขององค์ประกอบนี้อยู่ใน app.component.tpl.html และเราสามารถคัดลอกเทมเพลตเริ่มต้นไปยังไฟล์นี้ได้

โปรดสังเกตว่าองค์ประกอบอินพุตแต่ละรายการต้องมีแอตทริบิวต์ name เพื่อระบุอย่างถูกต้องภายในแบบฟอร์ม แม้ว่าจะดูเหมือนแบบฟอร์ม HTML ธรรมดาๆ แต่เราได้กำหนดรูปแบบที่รองรับ Angular 4 แล้ว (บางทีคุณอาจยังไม่เห็น) เมื่อนำเข้า FormsModule Angular 4 จะตรวจหาองค์ประกอบ HTML form โดยอัตโนมัติและแนบองค์ประกอบ NgForm กับองค์ประกอบนั้น (โดย selector ขององค์ประกอบ NgForm) นั่นคือกรณีในตัวอย่างของเรา แม้ว่าจะมีการประกาศฟอร์ม Angular 4 นี้ แต่ ณ จุดนี้ ก็ยังไม่ทราบว่าอินพุตที่รองรับ Angular 4 ใด Angular 4 นั้นไม่รุกรานในการลงทะเบียนองค์ประกอบ HTML input แต่ละรายการกับบรรพบุรุษของ form ที่ใกล้ที่สุด

คีย์ที่อนุญาตให้สังเกตองค์ประกอบอินพุตเป็นองค์ประกอบ Angular 4 และลงทะเบียนกับองค์ประกอบ NgForm คือคำสั่ง NgModel ดังนั้น เราสามารถขยายเทมเพลต app.component.tpl.html ได้ดังนี้:

 <form> .. <input type="text" name="name" ngModel> .. <input type="text" name="birthYear" ngModel > .. <input type="text" name="country" ngModel/> .. <input type="text" name="city" ngModel/> .. <input type="text" name="phoneNumber[1]" ngModel/> </form>

โดยการเพิ่มคำสั่ง NgModel อินพุตทั้งหมดจะถูกลงทะเบียนไปยังคอมโพเนนต์ NgForm ด้วยเหตุนี้ เราได้กำหนดรูปแบบ Angular 4 ที่ทำงานได้อย่างสมบูรณ์และจนถึงขณะนี้ ดีมาก แต่เรายังไม่มีวิธีเข้าถึงองค์ประกอบ NgForm และฟังก์ชันการทำงานที่มีให้ ฟังก์ชันหลักสองอย่างที่นำเสนอโดย NgForm คือ:

  • การดึงค่าของตัวควบคุมอินพุตที่ลงทะเบียนทั้งหมด

  • เรียกสถานะโดยรวมของการควบคุมทั้งหมด

เพื่อเปิดเผย NgForm เราสามารถเพิ่มสิ่งต่อไปนี้ใน <form> องค์ประกอบ:

 <form #myForm="ngForm"> .. </form>

สิ่งนี้เป็นไปได้ด้วยคุณสมบัติ exportAs ของ Component decorator

เมื่อเสร็จแล้ว เราสามารถเข้าถึงค่าของตัวควบคุมอินพุตทั้งหมดและขยายเทมเพลตเป็น:

 <form #myForm="ngForm"> .. <pre>{{myForm.value | json}}</pre> </form>

ด้วย myForm.value เรากำลังเข้าถึงข้อมูล JSON ที่มีค่าของอินพุตที่ลงทะเบียนทั้งหมด และด้วย {{myForm.value | json}} {{myForm.value | json}} เรากำลังพิมพ์ JSON ด้วยค่าต่างๆ

จะเป็นอย่างไรถ้าเราต้องการให้มีกลุ่มย่อยของอินพุตจากบริบทเฉพาะที่รวมอยู่ในคอนเทนเนอร์และแยกอ็อบเจ็กต์ในค่า JSON เช่น ตำแหน่งที่มีประเทศและเมืองหรือหมายเลขโทรศัพท์ อย่าเครียด เพราะรูปแบบที่ขับเคลื่อนด้วยเทมเพลตใน Angular 4 ก็ครอบคลุมเช่นกัน วิธีที่จะบรรลุสิ่งนี้คือการใช้คำสั่ง ngModelGroup

 <form #myForm="ngForm"> .. <div ngModelGroup="location"> .. </div> </div ngModelGroup="phoneNumbers"> .. <div> .. </form>

สิ่งที่เราขาดตอนนี้คือวิธีการเพิ่มหมายเลขโทรศัพท์หลายหมายเลข วิธีที่ดีที่สุดในการทำเช่นนี้คือการใช้อาร์เรย์เพื่อเป็นตัวแทนที่ดีที่สุดของคอนเทนเนอร์ที่ทำซ้ำได้ของหลาย ๆ ออบเจ็กต์ แต่ในขณะที่เขียนบทความนี้ คุณลักษณะดังกล่าวจะไม่ถูกนำมาใช้สำหรับฟอร์มที่ขับเคลื่อนด้วยเทมเพลต ดังนั้น เราต้องใช้วิธีแก้ปัญหาเพื่อให้งานนี้สำเร็จ จำเป็นต้องอัปเดตส่วนหมายเลขโทรศัพท์ดังต่อไปนี้:

 <div ngModelGroup="phoneNumbers"> <h3>Phone numbers</h3> <div *ngFor="let phoneId of phoneNumberIds; let i=index;"> <label>Phone number {{i + 1}}</label> <input type="text" name="phoneNumber[{{phoneId}}]" #phoneNumber="ngModel" ngModel/> <button type="button" (click)="remove(i); myForm.control.markAsTouched()">remove</button> </div> <button type="button" (click)="add(); myForm.control.markAsTouched()">Add phone number</button> </div>

myForm.control.markAsTouched() ใช้เพื่อทำให้แบบฟอร์มถูก touched เพื่อให้เราสามารถแสดงข้อผิดพลาดในขณะนั้น ปุ่มไม่เปิดใช้งานคุณสมบัตินี้เมื่อคลิก เฉพาะอินพุตเท่านั้น เพื่อให้ตัวอย่างต่อไปชัดเจนยิ่งขึ้น ฉันจะไม่เพิ่มบรรทัดนี้บนตัวจัดการการคลิกสำหรับ add() และ remove() แค่จินตนาการว่ามันอยู่ที่นั่น (มีอยู่ใน Plunkers)

เรายังต้องอัปเดต AppComponent เพื่อให้มีรหัสต่อไปนี้:

 private count:number = 1; phoneNumberIds:number[] = [1]; remove(i:number) { this.phoneNumberIds.splice(i, 1); } add() { this.phoneNumberIds.push(++this.count); }

เราต้องเก็บ ID ที่ไม่ซ้ำกันสำหรับหมายเลขโทรศัพท์ใหม่แต่ละหมายเลขที่เพิ่มเข้ามา และใน *ngFor ให้ติดตามการควบคุมหมายเลขโทรศัพท์ด้วย ID ของพวกเขา (ยอมรับว่าไม่ค่อยดีนัก แต่จนกว่าทีม Angular 4 จะใช้ฟีเจอร์นี้ ฉันเกรงว่า , ดีที่สุดที่เราสามารถทำได้)

เอาล่ะ ตอนนี้เรามีอะไรบ้าง เราได้เพิ่มฟอร์มที่รองรับ Angular 4 พร้อมอินพุต เพิ่มการจัดกลุ่มเฉพาะของอินพุต (ตำแหน่งและหมายเลขโทรศัพท์) และเปิดเผยฟอร์มภายในเทมเพลต แต่ถ้าเราต้องการเข้าถึงวัตถุ NgForm ด้วยวิธีการบางอย่างในองค์ประกอบล่ะ เราจะมาดูสองวิธีในการทำเช่นนี้

สำหรับวิธีแรก NgForm ซึ่งมีป้ายกำกับว่า myForm ในตัวอย่างปัจจุบัน สามารถส่งผ่านเป็นอาร์กิวเมนต์ไปยังฟังก์ชันที่จะทำหน้าที่เป็นตัวจัดการสำหรับเหตุการณ์ onSubmit ของฟอร์มได้ เพื่อการบูรณาการที่ดียิ่งขึ้น เหตุการณ์ wrapped ถูกปิดด้วย Angular 4 ซึ่งเป็นเหตุการณ์เฉพาะ NgForm ชื่อ ngSubmit และนี่เป็นวิธีที่ถูกต้องหากเราต้องการดำเนินการบางอย่างในการส่ง ดังนั้น ตัวอย่างตอนนี้จะมีลักษณะดังนี้:

 <form #myForm="ngForm" (ngSubmit)="register(myForm)"> … </form>

เราต้องมีวิธีการที่เกี่ยวข้อง register นำไปใช้ใน AppComponent สิ่งที่ต้องการ:

 register (myForm: NgForm) { console.log('Successful registration'); console.log(myForm); }

ด้วยวิธีนี้ โดยใช้ประโยชน์จากเหตุการณ์ onSubmit เราสามารถเข้าถึงองค์ประกอบ NgForm ได้ก็ต่อเมื่อมีการดำเนินการส่ง

วิธีที่สองคือการใช้คิวรีแบบดูโดยเพิ่ม @ViewChild มัณฑนากรให้กับคุณสมบัติของคอมโพเนนต์

 @ViewChild('myForm') private myForm: NgForm;

ด้วยวิธีการนี้ เราจึงได้รับอนุญาตให้เข้าถึงแบบฟอร์มได้ไม่ว่าเหตุการณ์ onSubmit จะถูกไล่ออกหรือไม่ก็ตาม

ยอดเยี่ยม! ตอนนี้ เรามีฟอร์ม Angular 4 ที่ทำงานได้อย่างสมบูรณ์พร้อมการเข้าถึงแบบฟอร์มในคอมโพเนนต์ แต่คุณสังเกตเห็นบางสิ่งที่ขาดหายไปหรือไม่? จะเกิดอะไรขึ้นถ้าผู้ใช้ป้อนบางอย่างเช่น "นี่ไม่ใช่ปี" ในการป้อนข้อมูล "ปี" ใช่ เข้าใจแล้ว เราขาดการตรวจสอบอินพุต และเราจะกล่าวถึงในส่วนต่อไปนี้

การตรวจสอบความถูกต้อง

การตรวจสอบความถูกต้องมีความสำคัญมากสำหรับแต่ละแอปพลิเคชัน เราต้องการตรวจสอบข้อมูลของผู้ใช้เสมอ (เราไม่สามารถเชื่อถือผู้ใช้ได้) เพื่อป้องกันการส่ง/บันทึกข้อมูลที่ไม่ถูกต้อง และเราต้องแสดงข้อความที่มีความหมายเกี่ยวกับข้อผิดพลาดเพื่อแนะนำให้ผู้ใช้ป้อนข้อมูลที่ถูกต้อง

สำหรับกฎการตรวจสอบบางอย่างที่จะบังคับใช้กับอินพุตบางรายการ ตัวตรวจสอบความถูกต้องจะต้องเชื่อมโยงกับอินพุตนั้น Angular 4 มีชุดเครื่องมือตรวจสอบทั่วไปอยู่แล้ว เช่น: required , maxLength , minLength

ดังนั้น เราจะเชื่อมโยงเครื่องมือตรวจสอบกับข้อมูลเข้าได้อย่างไร ค่อนข้างง่าย เพียงเพิ่มคำสั่ง validator ให้กับตัวควบคุม:

 <input name="name" ngModel required/>

ตัวอย่างนี้ทำให้การป้อนข้อมูล "ชื่อ" บังคับ มาเพิ่มการตรวจสอบให้กับอินพุตทั้งหมดในตัวอย่างของเรา

 <form #myForm="ngForm" (ngSubmit)="actionOnSubmit(myForm)" novalidate> <p>Is "myForm" valid? {{myForm.valid}}</p> .. <input type="text" name="name" ngModel required/> .. <input type="text" name="birthYear" ngModel required pattern="\\d{4,4}"/> .. <div ngModelGroup="location"> .. <input type="text" name="country" ngModel required/> .. <input type="text" name="city" ngModel/> </div> <div ngModelGroup="phoneNumbers"> .. <input type="text" name="phoneNumber[{{phoneId}}]" ngModel required/> .. </div> .. </form>

หมายเหตุ: novalidate ใช้เพื่อปิดใช้งานการตรวจสอบแบบฟอร์มดั้งเดิมของเบราว์เซอร์

เราได้กำหนด "ชื่อ" ที่จำเป็น ฟิลด์ "ปี" เป็นสิ่งจำเป็น และต้องประกอบด้วยตัวเลขเท่านั้น จำเป็นต้องป้อนข้อมูลประเทศ และหมายเลขโทรศัพท์ด้วย นอกจากนี้เรายังพิมพ์สถานะความถูกต้องของแบบฟอร์มด้วย {{myForm.valid}}

การปรับปรุงตัวอย่างนี้คือการแสดงสิ่งผิดปกติกับข้อมูลที่ผู้ใช้ป้อน (ไม่ใช่แค่แสดงสถานะโดยรวม) ก่อนที่เราจะเพิ่มการตรวจสอบเพิ่มเติมต่อไป ฉันต้องการใช้องค์ประกอบตัวช่วยที่จะช่วยให้เราสามารถพิมพ์ข้อผิดพลาดทั้งหมดสำหรับการควบคุมที่มีให้

 // show-errors.component.ts import { Component, Input } from '@angular/core'; import { AbstractControlDirective, AbstractControl } from '@angular/forms'; @Component({ selector: 'show-errors', template: ` <ul *ngIf="shouldShowErrors()"> <li *ngFor="let error of listOfErrors()">{{error}}</li> </ul> `, }) export class ShowErrorsComponent { private static readonly errorMessages = { 'required': () => 'This field is required', 'minlength': (params) => 'The min number of characters is ' + params.requiredLength, 'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength, 'pattern': (params) => 'The required pattern is: ' + params.requiredPattern, 'years': (params) => params.message, 'countryCity': (params) => params.message, 'uniqueName': (params) => params.message, 'telephoneNumbers': (params) => params.message, 'telephoneNumber': (params) => params.message }; @Input() private control: AbstractControlDirective | AbstractControl; shouldShowErrors(): boolean { return this.control && this.control.errors && (this.control.dirty || this.control.touched); } listOfErrors(): string[] { return Object.keys(this.control.errors) .map(field => this.getMessage(field, this.control.errors[field])); } private getMessage(type: string, params: any) { return ShowErrorsComponent.errorMessages[type](params); } }

รายการที่มีข้อผิดพลาดจะแสดงเฉพาะเมื่อมีข้อผิดพลาดที่มีอยู่และการป้อนข้อมูลถูกสัมผัสหรือสกปรก

ข้อความสำหรับข้อผิดพลาดแต่ละรายการจะค้นหาในแผนที่ของข้อความแสดงข้อผิดพลาดที่กำหนดไว้ errorMessages (ฉันได้เพิ่มข้อความทั้งหมดไว้ข้างหน้าแล้ว)

ส่วนประกอบนี้สามารถใช้ได้ดังนี้:

 <div> <label>Birth Year</label> <input type="text" name="birthYear" #birthYear="ngModel" ngModel required pattern="\\d{4,4}"/> <show-errors [control]="birthYear"></show-errors> </div>

เราจำเป็นต้องเปิดเผย NgModel สำหรับแต่ละอินพุตและส่งต่อไปยังส่วนประกอบที่แสดงข้อผิดพลาดทั้งหมด คุณสามารถสังเกตได้ว่าในตัวอย่างนี้ เราได้ใช้รูปแบบเพื่อตรวจสอบว่าข้อมูลเป็นตัวเลขหรือไม่ เกิดอะไรขึ้นถ้าผู้ใช้ป้อน “0000”? นี่จะเป็นอินพุตที่ไม่ถูกต้อง นอกจากนี้เรายังไม่มีตัวตรวจสอบความถูกต้องสำหรับชื่อที่ไม่ซ้ำ ข้อ จำกัด แปลก ๆ ของประเทศ (หากประเทศ = 'ฝรั่งเศส' เมืองจะต้องเป็น 'ปารีส') รูปแบบสำหรับหมายเลขโทรศัพท์ที่ถูกต้องและการตรวจสอบที่มีหมายเลขโทรศัพท์อย่างน้อยหนึ่งหมายเลข มีอยู่ นี่เป็นเวลาที่เหมาะสมในการดูตัวตรวจสอบความถูกต้องแบบกำหนดเอง

Angular 4 นำเสนออินเทอร์เฟซที่แต่ละเครื่องมือตรวจสอบความถูกต้องที่กำหนดเองต้องติดตั้ง ส่วนต่อประสานเครื่องมือตรวจสอบ (ที่น่าแปลกใจ!) อินเทอร์เฟซของเครื่องมือตรวจสอบโดยทั่วไปมีลักษณะดังนี้:

 export interface Validator { validate(c: AbstractControl): ValidationErrors | null; registerOnValidatorChange?(fn: () => void): void; }

โดยที่การดำเนินการที่เป็นรูปธรรมแต่ละครั้งจะต้องใช้วิธี 'ตรวจสอบ' วิธีการ validate นี้น่าสนใจมากกับสิ่งที่สามารถรับได้เป็นอินพุต และสิ่งที่ควรส่งคืนเป็นเอาต์พุต อินพุตคือ AbstractControl ซึ่งหมายความว่าอาร์กิวเมนต์สามารถเป็นประเภทใดก็ได้ที่ขยาย AbstractControl (FormGroup, FormControl และ FormArray) ผลลัพธ์ของวิธีการ validate ควรเป็น null หรือ undefined (ไม่มีผลลัพธ์) หากการป้อนข้อมูลของผู้ใช้ถูกต้อง หรือส่งคืนอ็อบเจ็กต์ ValidationErrors หากการป้อนข้อมูลของผู้ใช้ไม่ถูกต้อง ด้วยความรู้นี้ ตอนนี้เราจะใช้ตัวตรวจสอบวันเกิดที่กำหนด birthYear

 import { Directive } from '@angular/core'; import { NG_VALIDATORS, FormControl, Validator, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[birthYear]', providers: [{provide: NG_VALIDATORS, useExisting: BirthYearValidatorDirective, multi: true}] }) export class BirthYearValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const numValue = Number(c.value); const currentYear = new Date().getFullYear(); const minYear = currentYear - 85; const maxYear = currentYear - 18; const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear; const message = { 'years': { 'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear } }; return isValid ? null : message; } }

มีบางสิ่งที่จะอธิบายที่นี่ อันดับแรก คุณอาจสังเกตเห็นว่าเราได้ติดตั้งอินเทอร์เฟซตัวตรวจสอบความถูกต้อง วิธี validate จะตรวจสอบว่าผู้ใช้มีอายุระหว่าง 18 ถึง 85 ปีภายในปีเกิดที่ป้อนหรือไม่ หากอินพุตถูกต้อง null จะถูกส่งกลับ มิฉะนั้น อ็อบเจ็กต์ที่มีข้อความตรวจสอบความถูกต้องจะถูกส่งกลับ และส่วนสุดท้ายและสำคัญที่สุดคือการประกาศคำสั่งนี้ในฐานะผู้ตรวจสอบความถูกต้อง ทำได้ในพารามิเตอร์ "ผู้ให้บริการ" ของมัณฑนากร @Directive เครื่องมือตรวจสอบนี้ถูกจัดให้เป็นค่าเดียวของผู้ให้บริการหลายราย NG_VALIDATORS นอกจากนี้ อย่าลืมประกาศคำสั่งนี้ใน NgModule และตอนนี้เราสามารถใช้ตัวตรวจสอบความถูกต้องได้ดังต่อไปนี้:

 <input type="text" name="birthYear" #year="ngModel" ngModel required birthYear/>

ใช่ง่ายๆอย่างนั้น!

สำหรับหมายเลขโทรศัพท์ เราสามารถตรวจสอบรูปแบบของหมายเลขโทรศัพท์ได้ดังนี้:

 import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[telephoneNumber]', providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumberFormatValidatorDirective, multi: true}] }) export class TelephoneNumberFormatValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value); const message = { 'telephoneNumber': { 'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)' } }; return isValidPhoneNumber ? null : message; } }

การตรวจสอบความถูกต้องทั้งสองมาถึงแล้ว สำหรับประเทศและหมายเลขโทรศัพท์ สังเกตเห็นบางสิ่งที่เหมือนกันสำหรับทั้งคู่? ทั้งสองต้องมีการควบคุมมากกว่าหนึ่งตัวเพื่อทำการตรวจสอบความถูกต้อง คุณจำอินเทอร์เฟซของ Validator ได้หรือไม่และเราพูดอะไรเกี่ยวกับมัน? อาร์กิวเมนต์ของวิธีการ validate คือ AbstractControl ซึ่งสามารถเป็นอินพุตของผู้ใช้หรือแบบฟอร์มเองได้ สิ่งนี้สร้างโอกาสในการนำเครื่องมือตรวจสอบไปใช้ซึ่งใช้การควบคุมหลายรายการเพื่อกำหนดสถานะการตรวจสอบที่เป็นรูปธรรม

 import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[countryCity]', providers: [{provide: NG_VALIDATORS, useExisting: CountryCityValidatorDirective, multi: true}] }) export class CountryCityValidatorDirective implements Validator { validate(form: FormGroup): ValidationErrors { const countryControl = form.get('location.country'); const cityControl = form.get('location.city'); if (countryControl != null && cityControl != null) { const country = countryControl.value; const city = cityControl.value; let error = null; if (country === 'France' && city !== 'Paris') { error = 'If the country is France, the city must be Paris'; } const message = { 'countryCity': { 'message': error } }; return error ? message : null; } } }

เราได้ใช้ตัวตรวจสอบความถูกต้องใหม่ ซึ่งเป็นตัวตรวจสอบเมืองและประเทศ คุณสามารถสังเกตได้ว่าขณะนี้เป็นอาร์กิวเมนต์ วิธีการตรวจสอบได้รับ FormGroup และจาก FormGroup นั้น เราสามารถดึงข้อมูลอินพุตที่จำเป็นสำหรับการตรวจสอบความถูกต้อง ส่วนที่เหลือคล้ายกับตัวตรวจสอบอินพุตเดียวมาก

เครื่องมือตรวจสอบหมายเลขโทรศัพท์จะมีลักษณะดังนี้:

 import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors, FormControl } from '@angular/forms'; @Directive({ selector: '[telephoneNumbers]', providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumbersValidatorDirective, multi: true}] }) export class TelephoneNumbersValidatorDirective implements Validator { validate(form: FormGroup): ValidationErrors { const message = { 'telephoneNumbers': { 'message': 'At least one telephone number must be entered' } }; const phoneNumbers = <FormGroup> form.get('phoneNumbers'); const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0; return hasPhoneNumbers ? null : message; } }

เราสามารถใช้ได้ดังนี้:

 <form #myForm="ngForm" countryCity telephoneNumbers> .. </form>

เช่นเดียวกับตัวตรวจสอบอินพุตใช่ไหม ตอนนี้เพิ่งสมัครเข้าฟอร์ม

คุณจำองค์ประกอบ ShowErrors ได้หรือไม่? เราปรับใช้มันเพื่อทำงานกับ AbstractControlDirective ซึ่งหมายความว่าเราสามารถนำกลับมาใช้ใหม่เพื่อแสดงข้อผิดพลาดทั้งหมดที่เกี่ยวข้องโดยตรงกับแบบฟอร์มนี้เช่นกัน โปรดทราบว่า ณ จุดนี้ กฎการตรวจสอบที่เกี่ยวข้องโดยตรงกับแบบฟอร์มเท่านั้นคือ Country-city และ Telephone numbers (เครื่องมือตรวจสอบอื่น ๆ จะเชื่อมโยงกับตัวควบคุมแบบฟอร์มเฉพาะ) หากต้องการพิมพ์ข้อผิดพลาดของแบบฟอร์มทั้งหมด ให้ทำดังต่อไปนี้:

 <form #myForm="ngForm" countryCity telephoneNumbers > <show-errors [control]="myForm"></show-errors> .. </form>

สิ่งสุดท้ายที่เหลืออยู่คือการตรวจสอบความถูกต้องของชื่อที่ไม่ซ้ำ สิ่งนี้แตกต่างออกไปเล็กน้อย ในการตรวจสอบว่าชื่อไม่ซ้ำกันหรือไม่ ส่วนใหญ่จำเป็นต้องมีการเรียกไปยังส่วนหลังเพื่อตรวจสอบชื่อที่มีอยู่ทั้งหมด สิ่งนี้จัดประเภทเป็นการดำเนินการแบบอะซิงโครนัส เพื่อจุดประสงค์นี้ เราสามารถใช้เทคนิคก่อนหน้านี้ซ้ำสำหรับตัวตรวจสอบความถูกต้องที่กำหนดเอง เพียงแค่ทำให้การ validate ส่งคืนวัตถุที่จะได้รับการแก้ไขในอนาคต (สัญญาหรือสิ่งที่สังเกตได้) ในกรณีของเรา เราจะใช้สัญญา:

 import { Directive } from '@angular/core'; import { NG_ASYNC_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[uniqueName]', providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: UniqueNameValidatorDirective, multi: true}] }) export class UniqueNameValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const message = { 'uniqueName': { 'message': 'The name is not unique' } }; return new Promise(resolve => { setTimeout(() => { resolve(c.value === 'Existing' ? message : null); }, 1000); }); } }

เรากำลังรอ 1 วินาทีแล้วส่งคืนผลลัพธ์ คล้ายกับตัวตรวจสอบการซิงค์ หากสัญญาได้รับการแก้ไขด้วย null แสดงว่าผ่านการตรวจสอบแล้ว หากคำมั่นสัญญาได้รับการแก้ไขด้วยสิ่งอื่นใด การตรวจสอบความถูกต้องก็ล้มเหลว นอกจากนี้ โปรดสังเกตว่าตอนนี้ตัวตรวจสอบความถูกต้องนี้ได้ลงทะเบียนกับผู้ให้บริการหลายรายแล้ว NG_ASYNC_VALIDATORS คุณสมบัติที่มีประโยชน์อย่างหนึ่งของแบบฟอร์มเกี่ยวกับตัวตรวจสอบความถูกต้องของ async คือคุณสมบัติที่ pending การ สามารถใช้ได้ดังนี้:

 <button [disabled]="myForm.pending">Register</button>

มันจะปิดการใช้งานปุ่มจนกว่าตัวตรวจสอบ async จะได้รับการแก้ไข

นี่คือ Plunker ที่มี AppComponent ที่สมบูรณ์ องค์ประกอบ ShowErrors และเครื่องมือตรวจสอบทั้งหมด

ด้วยตัวอย่างเหล่านี้ เราได้ครอบคลุมกรณีส่วนใหญ่สำหรับการทำงานกับฟอร์มที่เป็นเทมเพลต เราได้แสดงให้เห็นแล้วว่าฟอร์มที่ขับเคลื่อนด้วยเทมเพลตนั้นคล้ายกับฟอร์มใน AngularJS จริงๆ (มันจะง่ายมากสำหรับนักพัฒนา AngularJS ในการโยกย้าย) ด้วยแบบฟอร์มประเภทนี้ มันค่อนข้างง่ายที่จะรวมฟอร์ม Angular 4 เข้ากับการเขียนโปรแกรมขั้นต่ำ โดยส่วนใหญ่มีการปรับเปลี่ยนในเทมเพลต HTML

แบบฟอร์มปฏิกิริยา

รูปแบบปฏิกิริยาเรียกอีกอย่างว่าแบบฟอร์ม "ที่ขับเคลื่อนด้วยแบบจำลอง" แต่ฉันชอบเรียกมันว่าแบบฟอร์ม "แบบเป็นโปรแกรม" และในไม่ช้า คุณจะเห็นว่าทำไม รูปแบบปฏิกิริยาเป็นแนวทางใหม่ในการรองรับรูปแบบ Angular 4 จึงไม่เหมือนกับรูปแบบที่ขับเคลื่อนด้วยเทมเพลต นักพัฒนา AngularJS จะไม่คุ้นเคยกับประเภทนี้

เราสามารถเริ่มต้นได้แล้ว จำได้ไหมว่าแบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลตมีโมดูลพิเศษอย่างไร แบบฟอร์มปฏิกิริยายังมีโมดูลของตัวเองที่เรียกว่า ReactiveFormsModule และต้องนำเข้าเพื่อเปิดใช้งานแบบฟอร์มประเภทนี้

 import {ReactiveFormsModule} from '@angular/forms' import {NgModule} from '@angular/core' import {BrowserModule} from '@angular/platform-browser' import {AppComponent} from 'src/app.component'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule ], declarations: [ AppComponent], bootstrap: [ AppComponent ] }) export class AppModule {}

อย่าลืมบูตแอปพลิเคชันด้วย

เราสามารถเริ่มต้นด้วย AppComponent และ template เดียวกันกับในส่วนก่อนหน้า

ณ จุดนี้ หาก FormsModule ไม่ถูกนำเข้า (และโปรดตรวจสอบให้แน่ใจว่าไม่ได้นำเข้า) เรามีเพียงองค์ประกอบฟอร์ม HTML ปกติที่มีตัวควบคุมรูปแบบสองสามตัว ไม่มีเวทย์มนตร์เชิงมุมที่นี่

เรามาถึงจุดที่คุณจะสังเกตเห็นว่าทำไมฉันถึงชอบเรียกแนวทางนี้ว่า "แบบเป็นโปรแกรม" เพื่อเปิดใช้งานรูปแบบ Angular 4 เราต้องประกาศวัตถุ FormGroup ด้วยตนเองและเติมด้วยตัวควบคุมดังนี้:

 import { FormGroup, FormControl, FormArray, NgForm } from '@angular/forms'; import { Component, OnInit } from '@angular/core'; @Component({ selector: 'my-app', templateUrl: 'src/app.component.html' }) export class AppComponent implements OnInit { private myForm: FormGroup; constructor() { } ngOnInit() { this.myForm = new FormGroup({ 'name': new FormControl(), 'birthYear': new FormControl(), 'location': new FormGroup({ 'country': new FormControl(), 'city': new FormControl() }), 'phoneNumbers': new FormArray([new FormControl('')]) }); } printMyForm() { console.log(this.myForm); } register(myForm: NgForm) { console.log('Registration successful.'); console.log(myForm.value); } }

วิธีการ printForm และการ register จะเหมือนกันจากตัวอย่างก่อนหน้านี้ และจะนำไปใช้ในขั้นตอนต่อไป ประเภทคีย์ที่ใช้ในที่นี้คือ FormGroup, FormControl และ FormArray ทั้งสามประเภทนี้เป็นสิ่งที่เราต้องการเพื่อสร้าง FormGroup ที่ถูกต้อง FormGroup เป็นเรื่องง่าย มันเป็นภาชนะควบคุมที่เรียบง่าย FormControl นั้นง่ายเช่นกัน มันคือการควบคุมใดๆ (เช่น อินพุต) และสุดท้าย FormArray คือชิ้นส่วนของปริศนาที่เราขาดหายไปในแนวทางที่ขับเคลื่อนด้วยเทมเพลต FormArray ช่วยให้สามารถรักษากลุ่มของการควบคุมโดยไม่ต้องระบุคีย์ที่เป็นรูปธรรมสำหรับการควบคุมแต่ละรายการ โดยทั่วไปจะเป็นอาร์เรย์ของการควบคุม (ดูเหมือนจะเป็นสิ่งที่สมบูรณ์แบบสำหรับหมายเลขโทรศัพท์ใช่ไหม)

เมื่อสร้างหนึ่งในสามประเภทนี้ จำกฎของ 3 นี้ คอนสตรัคเตอร์สำหรับแต่ละประเภทได้รับอาร์กิวเมนต์สามตัว ได้แก่ value ตรวจสอบความถูกต้องหรือรายการตัวตรวจสอบความถูกต้อง ตัวตรวจสอบความถูกต้องแบบอะซิงโครนัสหรือรายการตัวตรวจสอบแบบอะซิงโครนัสซึ่งกำหนดไว้ในโค้ด:

 constructor(value: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]);

สำหรับ FormGroup value คืออ็อบเจ็กต์ที่แต่ละคีย์แสดงถึงชื่อของคอนโทรล และค่าคือคอนโทรลเอง

สำหรับ FormArray value จะเป็นอาร์เรย์ของตัวควบคุม

สำหรับ FormControl value จะเป็นค่าเริ่มต้นหรือสถานะเริ่มต้น (วัตถุที่มี value และคุณสมบัติที่ disabled ) ของตัวควบคุม

เราได้สร้างวัตถุ FormGroup แล้ว แต่แม่แบบยังไม่รับรู้ถึงวัตถุนี้ การเชื่อมโยงระหว่าง FormGroup ในส่วนประกอบและแม่แบบนั้นเสร็จสิ้นด้วยสี่คำสั่ง: formGroup , formControlName , formGroupName และ formArrayName ใช้ดังนี้:

 <form [formGroup]="myForm" (ngSubmit)="register(myForm)"> <div> <label>Name</label> <input type="text" name="name" formControlName="name"> </div> <div> <label>Birth Year</label> <input type="text" name="birthYear" formControlName="birthYear"> </div> <div formGroupName="location"> <h3>Location</h3> <div> <label>Country</label> <input type="text" name="country" formControlName="country"> </div> <div> <label>City</label> <input type="text" name="city" formControlName="city"> </div> </div> <div formArrayName="phoneNumbers"> <h3>Phone numbers</h3> <div *ngFor="let phoneNumberControl of myForm.controls.phoneNumbers.controls; let i=index;"> <label>Phone number {{i + 1}}</label> <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i"> <button type="button" (click)="remove(i)">remove</button> </div> <button type="button" (click)="add()">Add phone number</button> </div> <pre>{{myForm.value | json}}</pre> <button type="submit">Register</button> <button type="button" (click)="printMyForm()">Print to console</button> </form>

ตอนนี้เรามี FormArray แล้ว คุณจะเห็นว่าเราสามารถใช้โครงสร้างนั้นเพื่อแสดงหมายเลขโทรศัพท์ทั้งหมดได้

และตอนนี้เพื่อเพิ่มการสนับสนุนสำหรับการเพิ่มและลบหมายเลขโทรศัพท์ (ในองค์ประกอบ):

 remove(i: number) { (<FormArray>this.myForm.get('phoneNumbers')).removeAt(i); } add() { (<FormArray>this.myForm.get('phoneNumbers')).push(new FormControl('')); }

ตอนนี้ เรามีรูปแบบปฏิกิริยา Angular 4 ที่ทำงานได้อย่างสมบูรณ์ สังเกตความแตกต่างจากฟอร์มที่ขับเคลื่อนด้วยเทมเพลตซึ่ง FormGroup ถูก "สร้างในเทมเพลต" (โดยการสแกนโครงสร้างเทมเพลต) และส่งผ่านไปยังส่วนประกอบ ในรูปแบบปฏิกิริยาจะกลับกัน FormGroup ที่สมบูรณ์จะถูกสร้างขึ้นใน คอมโพเนนต์ จากนั้น "ส่งผ่านไปยังเทมเพลต" และเชื่อมโยงกับตัวควบคุมที่เกี่ยวข้อง แต่เรามีปัญหาเดียวกันกับการตรวจสอบความถูกต้องอีกครั้ง ซึ่งเป็นปัญหาที่จะได้รับการแก้ไขในส่วนถัดไป

การตรวจสอบความถูกต้อง

เมื่อพูดถึงการตรวจสอบความถูกต้อง แบบฟอร์มปฏิกิริยาจะมีความยืดหยุ่นมากกว่าแบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลต หากไม่มีการเปลี่ยนแปลงเพิ่มเติม เราสามารถใช้ตัวตรวจสอบความถูกต้องเดิมซ้ำกับที่เคยใช้งานก่อนหน้านี้ได้ (สำหรับเทมเพลตที่ขับเคลื่อนด้วย) ดังนั้น โดยการเพิ่มคำสั่ง validator เราจึงสามารถเปิดใช้งานการตรวจสอบเดียวกันได้:

 <form [formGroup]="myForm" (ngSubmit)="register(myForm)" countryCity telephoneNumbers novalidate> <input type="text" name="name" formControlName="name" required uniqueName> <show-errors [control]="myForm.controls.name"></show-errors> .. <input type="text" name="birthYear" formControlName="birthYear" required birthYear> <show-errors [control]="myForm.controls.birthYear"></show-errors> .. <div formGroupName="location"> .. <input type="text" name="country" formControlName="country" required> <show-errors [control]="myForm.controls.location.controls.country"></show-errors> .. <input type="text" name="city" formControlName="city"> .. </div> <div formArrayName="phoneNumbers"> <h3>Phone numbers</h3> .. <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i" required telephoneNumber> <show-errors [control]="phoneNumberControl"></show-errors> .. </div> .. </form>

โปรดทราบว่าตอนนี้เราไม่มีคำสั่ง NgModel เพื่อส่งผ่านไปยังองค์ประกอบ ShowErrors แต่ FormGroup ที่สมบูรณ์ได้ถูกสร้างขึ้นแล้ว และเราสามารถส่ง AbstractControl ที่ถูกต้องเพื่อเรียกข้อผิดพลาดได้

นี่คือ Plunker ที่ใช้งานได้เต็มรูปแบบพร้อมการตรวจสอบความถูกต้องสำหรับฟอร์มปฏิกิริยา

แต่มันจะไม่สนุกถ้าเราใช้เครื่องตรวจสอบความถูกต้องซ้ำ ๆ ใช่ไหม? เราจะมาดูวิธีการระบุเครื่องมือตรวจสอบเมื่อสร้างกลุ่มแบบฟอร์ม

จำกฎ "กฎ 3s" ที่เราพูดถึงเกี่ยวกับตัวสร้างสำหรับ FormGroup, FormControl และ FormArray ได้หรือไม่ ใช่ เราบอกว่าตัวสร้างสามารถรับฟังก์ชันตัวตรวจสอบความถูกต้องได้ ลองใช้วิธีนั้นดู

ขั้นแรก เราต้องแยกฟังก์ชัน validate ของเครื่องมือตรวจสอบทั้งหมดออกเป็นคลาสโดยเปิดเผยเป็นวิธีการแบบคงที่:

 import { FormArray, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; export class CustomValidators { static birthYear(c: FormControl): ValidationErrors { const numValue = Number(c.value); const currentYear = new Date().getFullYear(); const minYear = currentYear - 85; const maxYear = currentYear - 18; const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear; const message = { 'years': { 'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear } }; return isValid ? null : message; } static countryCity(form: FormGroup): ValidationErrors { const countryControl = form.get('location.country'); const cityControl = form.get('location.city'); if (countryControl != null && cityControl != null) { const country = countryControl.value; const city = cityControl.value; let error = null; if (country === 'France' && city !== 'Paris') { error = 'If the country is France, the city must be Paris'; } const message = { 'countryCity': { 'message': error } }; return error ? message : null; } } static uniqueName(c: FormControl): Promise<ValidationErrors> { const message = { 'uniqueName': { 'message': 'The name is not unique' } }; return new Promise(resolve => { setTimeout(() => { resolve(c.value === 'Existing' ? message : null); }, 1000); }); } static telephoneNumber(c: FormControl): ValidationErrors { const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value); const message = { 'telephoneNumber': { 'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)' } }; return isValidPhoneNumber ? null : message; } static telephoneNumbers(form: FormGroup): ValidationErrors { const message = { 'telephoneNumbers': { 'message': 'At least one telephone number must be entered' } }; const phoneNumbers = <FormArray>form.get('phoneNumbers'); const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0; return hasPhoneNumbers ? null : message; } }

Now we can change the creation of 'myForm' to:

 this.myForm = new FormGroup({ 'name': new FormControl('', Validators.required, CustomValidators.uniqueName), 'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]), 'location': new FormGroup({ 'country': new FormControl('', Validators.required), 'city': new FormControl() }), 'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()]) }, Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers]) );

ดู? The rule of “3s,” when defining a FormControl, multiple validators can be declared in an array, and if we want to add multiple validators to a FormGroup they must be “merged” using Validators.compose (also Validators.composeAsync is available). And, that's it, validation should be working completely. There's a Plunker for this example as well.

This goes out to everybody that hates the “new” word. For working with the reactive forms, there's a shortcut provided—a builder, to be more precise. The FormBuilder allows creating the complete FormGroup by using the “builder pattern.” And that can be done by changing the FormGroup construction like this:

 constructor(private fb: FormBuilder) { } ngOnInit() { this.myForm = this.fb.group({ 'name': ['', Validators.required, CustomValidators.uniqueName], 'birthYear': ['', [Validators.required, CustomValidators.birthYear]], 'location': this.fb.group({ 'country': ['', Validators.required], 'city': '' }), 'phoneNumbers': this.fb.array([this.buildPhoneNumberComponent()]) }, { validator: Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers]) } ); }

Not a very big improvement from the instantiation with “new,” but there it is. And, don't worry, there's a Plunker for this also.

In this second section, we had a look at reactive forms in Angular 4. As you may notice, it is a completely new approach towards adding support for forms. Even though it seems verbose, this approach gives the developer total control over the underlying structure that enables forms in Angular 4. Also, since the reactive forms are created manually in the component, they are exposed and provide an easy way to be tested and controlled, while this was not the case with the template-driven forms.

Nesting Forms

Nesting forms is in some cases useful and a required feature, mainly when the state (eg, validity) of a sub-group of controls needs to determined. Think about a tree of components; we might be interested in the validity of a certain component in the middle of that hierarchy. That would be really hard to achieve if we had a single form at the root component. But, oh boy, it is a sensitive manner on a couple of levels. First, nesting real HTML forms, according to the HTML specification, is not allowed. We might try to nest <form> elements. In some browsers it might actually work, but we cannot be sure that it will work on all browsers, since it is not in the HTML spec. In AngularJS, the way to work around this limitation was to use the ngForm directive, which offered the AngularJS form functionalities (just grouping of the controls, not all form capabilities like posting to the server) but could be placed on any element. Also, in AngularJS, nesting of forms (when I say forms, I mean NgForm) was available out of the box. Just by declaring a tree of couple of elements with the ngForm directive, the state of each form was propagated upwards to the root element.

In the next section, we will have a look at a couple options on how to nest forms. I like to point out that we can differentiate two types of nesting: within the same component and across different components.

Nesting within the Same Component

If you take a look at the example that we implemented with the template-driven and the reactive approach, you will notice that we have two inner containers of controls, the “location” and the “phone numbers.” To create that container, to store the values in a separate property object, we used the NgModelGroup, FormGroupName, and the FormArrayName directives. If you have a good look at the definition of each directive, you may notice that each one of them extends the ControlContainer class (directly or indirectly). Well, what do you know, it turns out this is enough to provide the functionality that we require, wrapping up the state of all inner controls and propagating that state to the parent.

For the template-driven form, we need to do the following changes:

 <form #myForm="ngForm" (ngSubmit)="register(myForm)" novalidate> .. <div ngModelGroup="location" #location="ngModelGroup" countryCity> .. <show-errors [control]="location"></show-errors> </div> <div ngModelGroup="phoneNumbers" #phoneNumbers="ngModelGroup" telephoneNumbers> .. <show-errors [control]="phoneNumbers"></show-errors> </div> </form>

We added the ShowErrors component to each group, to show the errors directly associated with that group only. Since we moved the countryCity and telephoneNumbers validators to a different level, we also need to update them appropriately:

 // country-city-validator.directive.ts let countryControl = form.get('country'); let cityControl = form.get('city');

And telephone-numbers-validator.directive.ts to:

 let phoneNumbers = form.controls; let hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers).length > 0;

You can try the full example with template-driven forms in this Plunker.

And for the reactive forms, we will need some similar changes:

 <form [formGroup]="myForm" (ngSubmit)="register(myForm)" novalidate> .. <div formGroupName="location"> .. <show-errors [control]="myForm.controls.location"></show-errors> </div> <div formArrayName="phoneNumbers"> .. <show-errors [control]="myForm.controls.phoneNumbers"></show-errors> </div> .. </form>

The same changes from country-city-validator.directive.ts and telephone-numbers-validator.directive.ts are required for the countryCity and telephoneNumbers validators in CustomValidators to properly locate the controls.

And lastly, we need to modify the construction of the FormGroup to:

 this.myForm = new FormGroup({ 'name': new FormControl('', Validators.required, CustomValidators.uniqueName), 'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]), 'location': new FormGroup({ 'country': new FormControl('', Validators.required), 'city': new FormControl() }, CustomValidators.countryCity), 'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()], CustomValidators.telephoneNumbers) });

And there you have it—we've improved the validation for the reactive forms as well and as expected, the Plunker for this example.

Nesting across Different Components

It may come as a shock to all AngularJS developers, but in Angular 4, nesting of forms across different component doesn't work out of the box. I'm going to be straight honest with you; my opinion is that nesting is not supported for a reason (probably not because the Angular 4 team just forgot about it). Angular4's main enforced principle is a one-way data flow, top to bottom through the tree of components. The whole framework was designed like that, where the vital operation, the change detection, is executed in the same manner, top to bottom. If we follow this principle completely, we should have no issues, and all changes should be resolved within one full detection cycle. That's the idea, at least. In order to check that one-way data flow is implemented correctly, the nice guys in the Angular 4 team implemented a feature that after each change detection cycle, while in development mode, an additional round of change detection is triggered to check that no binding was changed as a result of reverse data propagation. What this means, let's think about a tree of components (C1, C2, C3, C4) as in Fig. 1, the change detection starts at the C1 component, continues at the C2 component and ends in the C3 component.

A tree of nested components holding a form.

If we have some method in C3 with a side effect that changes some binding in C1, that means that we are pushing data upwards, but the change detection for C1 already passed. When working in dev mode, the second round kicks in and notices a change in C1 that came as a result of a method execution in some child component. Then you are in trouble and you'll probably see the “Expression has changed after it was checked” exception. You could just turn off the development mode and there will be no exception, but the problem will not be solved; plus, how would you sleep at night, just sweeping all your problems under the rug like that?

เมื่อคุณรู้แล้ว ให้คิดว่าเรากำลังทำอะไรอยู่หากเรารวมสถานะของแบบฟอร์ม ถูกต้อง ข้อมูลถูกผลักขึ้นไปบนแผนผังองค์ประกอบ แม้ว่าเมื่อทำงานกับแบบฟอร์มระดับเดียว การผสานรวมของตัวควบคุมแบบฟอร์ม ( ngModel ) และตัวแบบฟอร์มเองก็ไม่ค่อยดีนัก ซึ่งจะทริกเกอร์รอบการตรวจจับการเปลี่ยนแปลงเพิ่มเติมเมื่อลงทะเบียนหรืออัปเดตค่าของตัวควบคุม (ทำได้โดยใช้สัญญาที่แก้ไขแล้ว แต่เก็บเป็นความลับ) เหตุใดจึงต้องมีรอบเพิ่มเติม ด้วยเหตุผลเดียวกัน ข้อมูลจึงไหลขึ้นด้านบน จากตัวควบคุมไปยังแบบฟอร์ม แต่บางครั้ง การซ้อนฟอร์มในองค์ประกอบหลายๆ ส่วนก็เป็นคุณลักษณะที่จำเป็น และเราจำเป็นต้องคิดหาวิธีแก้ไขเพื่อรองรับข้อกำหนดนี้

จากสิ่งที่เรารู้จนถึงตอนนี้ แนวคิดแรกที่เข้ามาในหัวคือการใช้รูปแบบปฏิกิริยา สร้างแผนผังแบบเต็มในองค์ประกอบรากบางส่วน แล้วส่งแบบฟอร์มย่อยไปยังองค์ประกอบย่อยเป็นอินพุต วิธีนี้ทำให้คุณเชื่อมโยงพาเรนต์กับองค์ประกอบย่อยอย่างแน่นหนา และทำให้ตรรกะทางธุรกิจขององค์ประกอบรูทยุ่งเหยิงด้วยการจัดการกับการสร้างฟอร์มย่อยทั้งหมด มาเถอะ เราเป็นมืออาชีพ ฉันแน่ใจว่าเราสามารถหาวิธีสร้างองค์ประกอบที่แยกจากกันโดยสิ้นเชิงด้วยแบบฟอร์ม และจัดเตรียมวิธีที่แบบฟอร์มเพื่อเผยแพร่สถานะไปยังใครก็ตามที่เป็นผู้ปกครอง

ทั้งหมดที่กล่าวมานี้เป็นคำสั่งที่อนุญาตให้ซ้อนรูปแบบ Angular 4 (ดำเนินการเพราะจำเป็นสำหรับโครงการ):

 import { OnInit, OnDestroy, Directive, SkipSelf, Optional, Attribute, Injector, Input } from '@angular/core'; import { NgForm, FormArray, FormGroup, AbstractControl } from '@angular/forms'; const resolvedPromise = Promise.resolve(null); @Directive({ selector: '[nestableForm]' }) export class NestableFormDirective implements OnInit, OnDestroy { private static readonly FORM_ARRAY_NAME = 'CHILD_FORMS'; private currentForm: FormGroup; @Input() private formGroup: FormGroup; constructor(@SkipSelf() @Optional() private parentForm: NestableFormDirective, private injector: Injector, @Attribute('rootNestableForm') private isRoot) { } ngOnInit() { if (!this.currentForm) { // NOTE: at this point both NgForm and ReactiveFrom should be available this.executePostponed(() => this.resolveAndRegister()); } } ngOnDestroy() { this.executePostponed(() => this.parentForm.removeControl(this.currentForm)); } public registerNestedForm(control: AbstractControl): void { // NOTE: prevent circular reference (adding to itself) if (control === this.currentForm) { throw new Error('Trying to add itself! Nestable form can be added only on parent "NgForm" or "FormGroup".'); } (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)).push(control); } public removeControl(control: AbstractControl): void { const array = (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)); const idx = array.controls.indexOf(control); array.removeAt(idx); } private resolveAndRegister(): void { this.currentForm = this.resolveCurrentForm(); this.currentForm.addControl(NestableFormDirective.FORM_ARRAY_NAME, new FormArray([])); this.registerToParent(); } private resolveCurrentForm(): FormGroup { // NOTE: template-driven or model-driven => determined by the formGroup input return this.formGroup ? this.formGroup : this.injector.get(NgForm).control; } private registerToParent(): void { if (this.parentForm != null && !this.isRoot) { this.parentForm.registerNestedForm(this.currentForm); } } private executePostponed(callback: () => void): void { resolvedPromise.then(() => callback()); } }

ตัวอย่างใน GIF ต่อไปนี้แสดงองค์ประกอบ main หนึ่งองค์ประกอบที่มี form-1 และภายในแบบฟอร์มนั้นมีองค์ประกอบที่ซ้อนกันอยู่ component-2 component-2 ประกอบด้วย form-2 ซึ่งซ้อน form-2.1 , form-2.2 และส่วนประกอบ ( component-3 ) ที่มีแผนผังของรูปแบบปฏิกิริยาอยู่ในนั้นและส่วนประกอบ ( component-4 ) ที่มีรูปแบบที่ แยกออกจากรูปแบบอื่นทั้งหมด ฉันรู้ดีว่าค่อนข้างยุ่ง แต่ฉันต้องการสร้างสถานการณ์ที่ค่อนข้างซับซ้อนเพื่อแสดงการทำงานของคำสั่งนี้

กรณีที่ซับซ้อนของการตรวจสอบรูปแบบเชิงมุมที่มีหลายองค์ประกอบ

ตัวอย่างถูกนำมาใช้ใน Plunker นี้

คุณสมบัติที่มีให้คือ:

  • เปิดใช้งานการซ้อนโดยการเพิ่มคำสั่ง nestableForm ให้กับองค์ประกอบ: form, ngForm, [ngForm], [formGroup]

  • ทำงานร่วมกับรูปแบบที่ขับเคลื่อนด้วยเทมเพลตและแบบตอบสนอง

  • เปิดใช้งานการสร้างแผนผังของรูปแบบที่ครอบคลุมหลายองค์ประกอบ

  • แยกแผนผังย่อยของฟอร์มด้วย rootNestableForm=”true” (จะไม่ลงทะเบียนกับพาเรนต์ nestableForm)

คำสั่งนี้อนุญาตให้ฟอร์มในองค์ประกอบลูกลงทะเบียนกับ nestableForm พาเรนต์แรก โดยไม่คำนึงว่าฟอร์มหลักจะถูกประกาศในองค์ประกอบเดียวกันหรือไม่ เราจะเข้าสู่รายละเอียดของการดำเนินการ

ก่อนอื่น มาดูคอนสตรัคเตอร์กันก่อน อาร์กิวเมนต์แรกคือ:

 @SkipSelf() @Optional() private parentForm: NestableFormDirective

สิ่งนี้จะค้นหาพาเรนต์ NestableFormDirective แรก @SkipSelf เพื่อไม่ให้จับคู่ตัวเอง และ @Optional เนื่องจากอาจไม่พบพาเรนต์ ในกรณีของฟอร์มรูท ตอนนี้เรามีการอ้างอิงถึงรูปแบบที่ซ้อนกันของพาเรนต์แล้ว

อาร์กิวเมนต์ที่สองคือ:

 private injector: Injector

หัวฉีดใช้เพื่อดึง provider FormGroup ปัจจุบัน (เทมเพลตหรือปฏิกิริยา)

และอาร์กิวเมนต์สุดท้ายคือ:

 @Attribute('rootNestableForm') private isRoot

เพื่อรับค่าที่กำหนดว่าฟอร์มนี้แยกจากแผนผังของฟอร์มหรือไม่

ถัดไป ใน ngInit เป็นการดำเนินการที่เลื่อนออกไป (จำกระแสข้อมูลย้อนกลับได้หรือไม่) FormGroup ปัจจุบันได้รับการแก้ไข ตัวควบคุม FormArray ใหม่ที่ชื่อ CHILD_FORMS ได้รับการลงทะเบียนกับ FormGroup นี้ (ซึ่งแบบฟอร์มย่อยจะถูกลงทะเบียน) และเป็นการกระทำสุดท้าย FormGroup ปัจจุบันลงทะเบียนเป็นเด็กในแบบฟอร์มที่ซ้อนกันของพาเรนต์

การดำเนินการ ngOnDestroy จะดำเนินการเมื่อแบบฟอร์มถูกทำลาย เมื่อทำลาย อีกครั้งในฐานะการดำเนินการที่เลื่อนออกไป แบบฟอร์มปัจจุบันจะถูกลบออกจากผู้ปกครอง (ยกเลิกการลงทะเบียน)

คำสั่งสำหรับแบบฟอร์มที่ซ้อนกันได้สามารถปรับแต่งเพิ่มเติมสำหรับความต้องการเฉพาะ—อาจลบการสนับสนุนสำหรับแบบฟอร์มปฏิกิริยา ลงทะเบียนแต่ละแบบฟอร์มย่อยภายใต้ชื่อเฉพาะ (ไม่ใช่ในอาร์เรย์ CHILD_FORMS) เป็นต้น การนำคำสั่ง nestableForm ไปใช้งานนี้เป็นไปตามข้อกำหนดของโครงการและได้นำเสนอไว้ ณ ที่นี้ ครอบคลุมกรณีพื้นฐานบางอย่าง เช่น การเพิ่มแบบฟอร์มใหม่หรือการลบแบบฟอร์มที่มีอยู่แบบไดนามิก (*ngIf) และการเผยแพร่สถานะของแบบฟอร์มไปยังพาเรนต์ โดยพื้นฐานแล้วจะลดเหลือการดำเนินการที่สามารถแก้ไขได้ภายในรอบการตรวจจับการเปลี่ยนแปลงหนึ่งรอบ (มีการเลื่อนหรือไม่)

หากคุณต้องการสถานการณ์ขั้นสูงเพิ่มเติม เช่น เพิ่มการตรวจสอบตามเงื่อนไขให้กับอินพุตบางส่วน (เช่น [required]=”someCondition”) ที่อาจต้องมีการตรวจสอบการเปลี่ยนแปลง 2 รอบ จะไม่ทำงานเนื่องจากกฎ กำหนดโดย Angular 4

อย่างไรก็ตาม หากคุณวางแผนที่จะใช้คำสั่งนี้ หรือใช้โซลูชันอื่น ให้ระมัดระวังอย่างมากเกี่ยวกับสิ่งที่กล่าวถึงที่เกี่ยวข้องกับการตรวจจับการเปลี่ยนแปลง ณ จุดนี้ นี่คือวิธีการใช้งาน Angular 4 มันอาจจะเปลี่ยนไปในอนาคต—เราไม่รู้หรอก การตั้งค่าปัจจุบันและการจำกัดการบังคับใช้ใน Angular 4 ที่กล่าวถึงในบทความนี้อาจเป็นข้อเสียหรือข้อดี มันยังคงที่จะเห็น

แบบฟอร์มทำได้ง่ายด้วย Angular 4

อย่างที่คุณเห็น ทีม Angular ทำได้ดีมากในการจัดหาฟังก์ชันต่างๆ ที่เกี่ยวข้องกับแบบฟอร์ม ฉันหวังว่าโพสต์นี้จะใช้เป็นแนวทางที่สมบูรณ์ในการทำงานกับแบบฟอร์มประเภทต่างๆ ใน ​​Angular 4 และยังให้ข้อมูลเชิงลึกเกี่ยวกับแนวคิดขั้นสูงบางอย่าง เช่น การซ้อนแบบฟอร์มและกระบวนการตรวจจับการเปลี่ยนแปลง

แม้จะมีโพสต์ที่แตกต่างกันทั้งหมดที่เกี่ยวข้องกับรูปแบบ Angular 4 (หรือหัวข้ออื่น ๆ ของ Angular 4 สำหรับเรื่องนั้น) ในความคิดของฉันจุดเริ่มต้นที่ดีที่สุดคือเอกสารอย่างเป็นทางการของ Angular 4 นอกจากนี้ พวก Angular มีเอกสารที่ดีในโค้ดของพวกเขา หลายครั้งที่ฉันพบวิธีแก้ปัญหาเพียงแค่ดูซอร์สโค้ดและเอกสารประกอบที่นั่น ไม่มี Googling หรืออะไรเลย เกี่ยวกับการซ้อนแบบฟอร์มที่กล่าวถึงในส่วนสุดท้าย ฉันเชื่อว่านักพัฒนา AngularJS ที่เริ่มเรียนรู้ Angular 4 จะพบปัญหานี้ในบางจุด ซึ่งเป็นแรงบันดาลใจในการเขียนโพสต์นี้

ดังที่เราได้เห็นแล้ว แบบฟอร์มมีสองประเภท และไม่มีกฎเกณฑ์ที่เข้มงวดที่คุณไม่สามารถใช้ร่วมกันได้ การรักษา codebase ให้สะอาดและสม่ำเสมอนั้นเป็นเรื่องดี แต่บางครั้ง บางสิ่งสามารถทำได้ง่ายขึ้นด้วยแบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลต และบางครั้งก็ตรงกันข้าม ดังนั้น หากคุณไม่สนใจขนาดบันเดิลที่ใหญ่กว่าเล็กน้อย ฉันขอแนะนำให้ใช้สิ่งที่คุณพิจารณาว่าเหมาะสมกว่าเป็นกรณีไป อย่าผสมมันไว้ในองค์ประกอบเดียวกันเพราะอาจทำให้สับสนได้

Plunkers ที่ใช้ในโพสต์นี้

  • แบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลต

  • แบบฟอร์มปฏิกิริยา ตัวตรวจสอบเทมเพลต

  • แบบฟอร์มปฏิกิริยา ตัวตรวจสอบรหัส

  • รูปแบบปฏิกิริยา ตัวสร้างแบบฟอร์ม

  • แบบฟอร์มที่ขับเคลื่อนด้วยเทมเพลต ซ้อนอยู่ภายในส่วนประกอบเดียวกัน

  • รูปแบบปฏิกิริยา ซ้อนกันภายในองค์ประกอบเดียวกัน

  • รูปแบบที่ซ้อนกันผ่านต้นไม้ของส่วนประกอบ

ที่เกี่ยวข้อง: การ ตรวจสอบแบบฟอร์ม Smart Node.js