العمل مع الأشكال الزاويّة 4: التداخل والتحقق من صحة الإدخال

نشرت: 2022-03-11

على الويب ، كانت بعض عناصر إدخال المستخدم الأولى عبارة عن زر ومربع اختيار وإدخال نص وأزرار اختيار. حتى يومنا هذا ، لا تزال هذه العناصر مستخدمة في تطبيقات الويب الحديثة على الرغم من أن معيار HTML قد قطع شوطًا طويلاً منذ تعريفه المبكر ويسمح الآن بجميع أنواع التفاعلات الرائعة.

يعد التحقق من صحة مدخلات المستخدم جزءًا أساسيًا من أي تطبيق ويب قوي.

يمكن للنماذج في تطبيقات Angular تجميع حالة جميع المدخلات الموجودة ضمن هذا النموذج وتوفير حالة عامة مثل حالة التحقق من صحة النموذج الكامل. يمكن أن يكون هذا مفيدًا حقًا لتحديد ما إذا كان سيتم قبول إدخال المستخدم أو رفضه دون التحقق من كل إدخال على حدة.

الزاوي 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 عامًا

  • البلد - إلزامي ، ولجعل الأمور معقدة بعض الشيء ، نحتاج إلى التحقق من صحة أنه إذا كانت الدولة هي فرنسا ، فيجب أن تكون المدينة باريس (دعنا نقول أن خدمتنا تقدم فقط في باريس)

  • phoneNumber - يجب أن يتبع كل رقم هاتف نمطًا محددًا ، ويجب أن يكون هناك رقم هاتف واحد على الأقل ، ويُسمح للمستخدم بإضافة رقم هاتف جديد أو إزالة رقم هاتف موجود.

  • يتم تمكين الزر "تسجيل" فقط إذا كانت جميع الإدخالات صالحة ، وبمجرد النقر فوقه ، يقوم بإرسال النموذج.

  • تقوم "طباعة إلى وحدة التحكم" فقط بطباعة قيمة جميع المدخلات لوحدة التحكم عند النقر فوقها.

الهدف النهائي هو التنفيذ الكامل للمواصفات المحددة.

نماذج يحركها القوالب

تتشابه النماذج التي تعتمد على القوالب إلى حد بعيد مع النماذج الموجودة في 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 .

بمجرد الانتهاء من ذلك ، يمكننا الوصول إلى قيم جميع عناصر التحكم في الإدخال وتوسيع القالب إلى:

 <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); }

يجب أن نخزن معرّفًا فريدًا لكل رقم هاتف جديد مضاف ، وفي *ngFor ، تتبع عناصر التحكم في رقم الهاتف من خلال معرفهم (أعترف أنه ليس لطيفًا جدًا ، ولكن حتى يقوم فريق Angular 4 بتنفيذ هذه الميزة ، أخشى ، هذا أفضل ما يمكننا فعله)

حسنًا ، ماذا لدينا حتى الآن ، لقد أضفنا نموذج Angular 4 المدعوم مع المدخلات ، وأضفنا مجموعة محددة من المدخلات (الموقع وأرقام الهواتف) وكشفنا النموذج داخل القالب. ولكن ماذا لو أردنا الوصول إلى كائن NgForm بطريقة ما في المكون؟ سنلقي نظرة على طريقتين للقيام بذلك.

بالنسبة للطريقة الأولى ، يمكن تمرير NgForm ، المسمى myForm في المثال الحالي ، كوسيطة للدالة التي ستعمل كمعالج لحدث onSubmit للنموذج. من أجل تكامل أفضل ، يتم wrapped حدث onSubmit بحدث 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 ...

إذن ، كيف يمكننا ربط المدقق بمدخل؟ حسنًا ، سهل جدًا ؛ فقط أضف توجيه المدقق إلى عنصر التحكم:

 <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 واجهة يجب على كل مدقق مخصص تنفيذها ، واجهة Validator (يا لها من مفاجأة!). تبدو واجهة Validator بشكل أساسي كما يلي:

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

هناك بعض الأشياء لشرحها هنا. قد تلاحظ أولاً أننا قمنا بتنفيذ واجهة Validator. validate طريقة التحقق مما إذا كان عمر المستخدم بين 18 و 85 عامًا بحلول سنة الميلاد التي تم إدخالها. إذا كان الإدخال صالحًا ، فسيتم إرجاع قيمة null ، وإلا فسيتم إرجاع كائن يحتوي على رسالة التحقق من الصحة. والجزء الأخير والأكثر أهمية هو إعلان هذا التوجيه كمدقق. يتم ذلك في معلمة "Provider" لمصمم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); }); } }

نحن ننتظر ثانية واحدة ثم نعيد النتيجة. على غرار أدوات التحقق من المزامنة ، إذا تم حل الوعد بقيمة null ، فهذا يعني أن التحقق قد تم ؛ إذا تم الوفاء بالوعد بأي شيء آخر ، فإن التحقق من الصحة قد فشل. لاحظ أيضًا أن هذا المدقق مسجل الآن لموفر متعدد آخر ، NG_ASYNC_VALIDATORS . إحدى الخصائص المفيدة للنماذج المتعلقة بأدوات التحقق غير المتزامنة هي الخاصية pending . يمكن استخدامه على النحو التالي:

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

سيتم تعطيل الزر حتى يتم حل أدوات التحقق غير المتزامن.

إليك 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 والقالب كما في القسم السابق.

في هذه المرحلة ، إذا لم يتم استيراد FormsModule (ويرجى التأكد من أنها ليست كذلك) ، فلدينا فقط عنصر نموذج HTML عادي مع اثنين من عناصر التحكم في النموذج ، ولا يوجد سحر Angular هنا.

لقد وصلنا إلى النقطة التي ستلاحظ فيها سبب رغبتي في تسمية هذا النهج "بالبرنامج". لتمكين نماذج 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('')); }

الآن ، لدينا الشكل التفاعلي للزاوية 4 الذي يعمل بشكل كامل. لاحظ الاختلاف عن النماذج المستندة إلى القوالب حيث تم "إنشاء FormGroup في القالب" (عن طريق مسح بنية القالب) وتمريره إلى المكون ، وفي النماذج التفاعلية يكون العكس بالعكس ، يتم إنشاء FormGroup الكاملة في المكون ، ثم "تم تمريره إلى النموذج" وربطه بعناصر التحكم المقابلة. ولكن ، مرة أخرى لدينا نفس المشكلة المتعلقة بالتحقق ، وهي مشكلة سيتم حلها في القسم التالي.

تصديق

عندما يتعلق الأمر بالتحقق من الصحة ، تكون النماذج التفاعلية أكثر مرونة من النماذج التي تعتمد على القوالب. مع عدم وجود تغييرات إضافية ، يمكننا إعادة استخدام نفس أدوات التحقق التي تم تنفيذها مسبقًا (للقوالب المدفوعة). لذلك ، بإضافة توجيهات المدقق ، يمكننا تنشيط نفس التحقق:

 <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 ، لعدم تطابق نفسه ، و @ اختياري لأنه قد لا يجد أحد الوالدين ، في حالة النموذج الجذر. الآن لدينا إشارة إلى نموذج التعشيش الأصل.

الحجة الثانية هي:

 private injector: Injector

يتم استخدام الحاقن لاسترداد provider FormGroup الحالي (قالب أو تفاعلي).

والحجة الأخيرة هي:

 @Attribute('rootNestableForm') private isRoot

للحصول على القيمة التي تحدد ما إذا كان هذا النموذج معزولًا عن شجرة النماذج.

بعد ذلك ، في ngInit كإجراء مؤجل (تذكر تدفق البيانات العكسي؟) ، تم حل FormGroup الحالية ، تم تسجيل عنصر تحكم FormArray جديد يسمى CHILD_FORMS في FormGroup هذه (حيث سيتم تسجيل النماذج الفرعية) وكإجراء أخير ، تم تسجيل FormGroup الحالي كطفل للنموذج المتداخل الأصل.

يتم تنفيذ الإجراء ngOnDestroy عند إتلاف النموذج. عند التدمير ، مرة أخرى كإجراء مؤجل ، تتم إزالة النموذج الحالي من الأصل (إلغاء التسجيل).

يمكن تخصيص التوجيه الخاص بالنماذج المتداخلة بشكل أكبر لحاجة معينة - ربما إزالة دعم النماذج التفاعلية ، وتسجيل كل نموذج فرعي تحت اسم معين (ليس في مصفوفة CHILD_FORMS) ، وما إلى ذلك. هذا التنفيذ لتوجيه nestableForm يفي بمتطلبات المشروع ويتم تقديمه هنا على هذا النحو. يغطي بعض الحالات الأساسية مثل إضافة نموذج جديد أو إزالة نموذج موجود ديناميكيًا (* ngIf) ونشر حالة النموذج إلى الأصل. يتلخص هذا بشكل أساسي في العمليات التي يمكن حلها خلال دورة واحدة للكشف عن التغيير (بالتأجيل أو عدم التأجيل).

إذا كنت تريد سيناريو أكثر تقدمًا مثل إضافة تحقق شرطي إلى بعض المدخلات (على سبيل المثال [مطلوب] = "بعض الظروف") التي تتطلب جولتي اكتشاف تغيير ، فلن تعمل بسبب قاعدة "حل دورة الاكتشاف الواحدة" التي يفرضها Angular 4.

على أي حال ، إذا كنت تخطط لاستخدام هذا التوجيه ، أو تنفيذ بعض الحلول الأخرى ، فكن حريصًا جدًا فيما يتعلق بالأشياء التي تم ذكرها فيما يتعلق باكتشاف التغيير. في هذه المرحلة ، هذه هي الطريقة التي يتم بها تنفيذ Angular 4. قد يتغير في المستقبل - لا يمكننا أن نعرف. قد يكون الإعداد الحالي والقيود المفروضة في Angular 4 التي تم ذكرها في هذه المقالة عيبًا أو فائدة. يبقى أن نرى.

أشكال سهلة مع الزاوي 4

كما ترى ، قام فريق Angular بعمل جيد حقًا في توفير العديد من الوظائف المتعلقة بالنماذج. آمل أن يكون هذا المنشور بمثابة دليل كامل للعمل مع الأنواع المختلفة من النماذج في Angular 4 ، كما يعطي نظرة ثاقبة على بعض المفاهيم الأكثر تقدمًا مثل تداخل النماذج وعملية اكتشاف التغيير.

على الرغم من جميع المنشورات المختلفة المتعلقة بأشكال Angular 4 (أو أي موضوع Angular 4 آخر لهذه المسألة) ، في رأيي ، فإن أفضل نقطة بداية هي توثيق Angular 4 الرسمي. أيضًا ، لدى الرجال Angular وثائق جيدة في التعليمات البرمجية الخاصة بهم. في كثير من الأحيان ، وجدت حلاً فقط من خلال النظر في التعليمات البرمجية المصدر والوثائق هناك ، لا يوجد Googling أو أي شيء آخر. حول تداخل النماذج ، الذي تمت مناقشته في القسم الأخير ، أعتقد أن أي مطور AngularJS يبدأ في تعلم Angular 4 سوف يعثر على هذه المشكلة في مرحلة ما ، والتي كانت نوعًا من إلهامي لكتابة هذا المنشور.

كما رأينا أيضًا ، هناك نوعان من النماذج ، ولا توجد قاعدة صارمة تمنع استخدامهما معًا. من الجيد الاحتفاظ بقاعدة الشفرة نظيفة ومتسقة ، ولكن في بعض الأحيان ، يمكن القيام بشيء ما بسهولة أكبر باستخدام النماذج التي تعتمد على القوالب ، وفي بعض الأحيان يكون العكس. لذلك ، إذا كنت لا تمانع في أحجام الحزم الأكبر قليلاً ، أقترح استخدام كل ما تعتبره أكثر ملاءمة لكل حالة على حدة. فقط لا تخلطهم في نفس المكون لأنه من المحتمل أن يؤدي إلى بعض الالتباس.

الغطاس المستخدمة في هذا المنشور

  • نماذج يحركها القوالب

  • النماذج التفاعلية ، قوالب التحقق

  • النماذج التفاعلية ، مدققات التعليمات البرمجية

  • النماذج التفاعلية ، منشئ النموذج

  • النماذج المستندة إلى القوالب المتداخلة في نفس المكون

  • النماذج التفاعلية ، متداخلة داخل نفس المكون

  • الأشكال المتداخلة من خلال شجرة المكونات

ذات صلة: التحقق من صحة نموذج Node.js الذكية