Angular 4 양식 작업: 중첩 및 입력 유효성 검사
게시 됨: 2022-03-11웹에서 초기 사용자 입력 요소 중 일부는 버튼, 체크박스, 텍스트 입력 및 라디오 버튼이었습니다. HTML 표준이 초기 정의에서 먼 길을 왔고 이제 모든 종류의 멋진 상호 작용을 허용하지만 오늘날까지 이러한 요소는 현대 웹 응용 프로그램에서 여전히 사용됩니다.
사용자 입력의 유효성을 검사하는 것은 강력한 웹 응용 프로그램의 필수적인 부분입니다.
Angular 애플리케이션의 양식은 해당 양식에 있는 모든 입력의 상태를 집계하고 전체 양식의 유효성 검사 상태와 같은 전체 상태를 제공할 수 있습니다. 이는 각 입력을 개별적으로 확인하지 않고 사용자 입력을 수락할지 또는 거부할지 결정하는 데 매우 유용할 수 있습니다.
이 기사에서는 Angular 애플리케이션에서 쉽게 양식을 사용하고 양식 유효성 검사를 수행하는 방법을 배웁니다.
Angular 4에는 템플릿 기반 형식과 반응형 형식의 두 가지 다른 형식을 사용할 수 있습니다. 동일한 예제를 사용하여 각 양식 유형을 살펴보고 동일한 항목을 다른 방식으로 구현하는 방법을 살펴보겠습니다. 나중에 이 기사에서 중첩 양식을 설정하고 사용하는 방법에 대한 새로운 접근 방식을 살펴볼 것입니다.
각도 4 양식
Angular 4에서는 다음 네 가지 상태가 일반적으로 양식에 사용됩니다.
valid – 모든 양식 컨트롤의 유효성 상태, 모든 컨트롤이 유효한 경우 true
유효하지 않음 -
valid
함의 역; 일부 컨트롤이 유효하지 않은 경우 truepristine - 양식의 "깨끗함"에 대한 상태를 제공합니다. 컨트롤이 수정되지 않은 경우 true
더티 -
pristine
반대 ; 일부 컨트롤이 수정된 경우 true
양식의 기본 예를 살펴보겠습니다.
<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는 자동으로 form
HTML 요소를 감지하고 NgForm 구성 요소를 해당 요소에 첨부합니다(NgForm 구성 요소의 selector
에 의해). 우리의 예가 그렇습니다. 이 Angular 4 형식이 선언되었지만 이 시점에서 Angular 4 지원 입력을 알지 못합니다. Angular 4는 각 input
HTML 요소를 가장 가까운 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>
이것은 Component
데코레이터의 exportAs
속성 덕분에 가능합니다.
이 작업이 완료되면 모든 입력 컨트롤의 값에 액세스하고 템플릿을 다음으로 확장할 수 있습니다.
<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 개체에 액세스하려면 어떻게 해야 합니까? 이를 수행하는 두 가지 방법을 살펴보겠습니다.
첫 번째 방법으로, 현재 예제에서 myForm
으로 레이블이 지정된 NgForm은 양식의 onSubmit 이벤트에 대한 핸들러 역할을 할 함수에 대한 인수로 전달할 수 있습니다. 더 나은 통합을 위해 onSubmit 이벤트는 ngSubmit
이라는 Angular 4, NgForm 특정 이벤트로 wrapped
되며 제출 시 일부 작업을 실행하려는 경우 올바른 방법입니다. 이제 예제는 다음과 같습니다.
<form #myForm="ngForm" (ngSubmit)="register(myForm)"> … </form>
AppComponent에서 구현된 해당 메소드 register
가 있어야 합니다. 다음과 같은 것:
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' 메소드를 구현해야 합니다(MUST). 이 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
이 반환되고, 그렇지 않으면 유효성 검사 메시지가 포함된 개체가 반환됩니다. 그리고 마지막이자 가장 중요한 부분은 이 지시문을 Validator로 선언하는 것입니다. 이는 @Directive 데코레이터의 "providers" 매개변수에서 수행됩니다. 이 유효성 검사기는 다중 공급자 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; } } }
새로운 유효성 검사기인 국가-도시 유효성 검사기를 구현했습니다. 이제 validate 메소드가 인수로 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
에 등록되어 있습니다. 비동기 유효성 검사기와 관련된 양식의 유용한 속성 중 하나는 pending
속성입니다. 다음과 같이 사용할 수 있습니다.
<button [disabled]="myForm.pending">Register</button>
비동기 유효성 검사기가 해결될 때까지 버튼을 비활성화합니다.
다음은 완전한 AppComponent, ShowErrors 구성 요소 및 모든 Validator를 포함하는 Plunker입니다.
이 예제를 통해 템플릿 기반 양식으로 작업하는 대부분의 경우를 다뤘습니다. 우리는 템플릿 기반 양식이 AngularJS의 양식과 정말 유사하다는 것을 보여주었습니다(AngularJS 개발자가 마이그레이션하기가 정말 쉬울 것입니다). 이러한 유형의 양식을 사용하면 주로 HTML 템플릿의 조작을 통해 최소한의 프로그래밍으로 Angular 4 양식을 통합하는 것이 매우 쉽습니다.
반응형
반응형 형식은 "모델 기반" 형식으로도 알려져 있지만 저는 "프로그래매틱" 형식이라고 부르는 것을 좋아하며 곧 그 이유를 알게 될 것입니다. 반응형 형식은 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을 가져오지 않았는지 확인하고 가져오지 않았는지 확인하세요. 여기에는 Angular 마법이 없는 몇 가지 양식 컨트롤이 있는 일반 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은 그런 다음 "템플릿으로 전달"되고 해당 컨트롤과 연결됩니다. 그러나 유효성 검사와 동일한 문제가 다시 발생합니다. 이 문제는 다음 섹션에서 해결될 것입니다.
확인
유효성 검사와 관련하여 반응형 양식은 템플릿 기반 양식보다 훨씬 유연합니다. 추가 변경 없이 이전에 구현된 것과 동일한 유효성 검사기를 재사용할 수 있습니다(템플릿 기반의 경우). 따라서 유효성 검사기 지시문을 추가하여 동일한 유효성 검사를 활성화할 수 있습니다.
<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>
이제 ShowErrors 구성 요소에 전달할 NgModel 지시문이 없지만 완전한 FormGroup이 이미 구성되어 있으며 오류 검색을 위해 올바른 AbstractControl을 전달할 수 있습니다.
반응형 형식에 대한 이러한 유형의 유효성 검사가 포함된 전체 작동 Plunker가 있습니다.
하지만 유효성 검사기를 재사용하면 재미가 없겠죠? 양식 그룹을 생성할 때 유효성 검사기를 지정하는 방법을 살펴보겠습니다.
FormGroup, FormControl 및 FormArray의 생성자에 대해 언급한 "3s 규칙" 규칙을 기억하십니까? 예, 생성자가 유효성 검사기 기능을 받을 수 있다고 말했습니다. 따라서 그 접근 방식을 시도해 보겠습니다.
먼저 모든 유효성 검사기의 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.
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의 예는 form-1
을 포함하는 하나의 main
구성 요소를 보여주고 해당 양식 안에는 또 다른 중첩 구성 요소인 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
인젝터는 현재 FormGroup provider
(템플릿 또는 반응형)를 검색하는 데 사용됩니다.
그리고 마지막 인수는 다음과 같습니다.
@Attribute('rootNestableForm') private isRoot
이 양식이 양식 트리에서 분리되었는지 여부를 결정하는 값을 가져옵니다.
다음으로 연기된 작업으로 ngInit
에서(역 데이터 흐름을 기억합니까?) 현재 FormGroup이 해결되고 CHILD_FORMS라는 새 CHILD_FORMS
컨트롤이 이 FormGroup(하위 양식이 등록될 위치)에 등록되고 마지막 작업으로 현재 FormGroup은 상위 중첩 가능 양식의 하위로 등록됩니다.
ngOnDestroy
액션은 폼이 파괴될 때 실행됩니다. 폐기 시 다시 연기된 작업으로 현재 양식이 상위 항목에서 제거됩니다(등록 취소).
중첩 가능한 양식에 대한 지시문은 특정 필요에 따라 추가로 사용자 정의할 수 있습니다. 반응 양식에 대한 지원을 제거하고, 각 자식 양식을 특정 이름(CHILD_FORMS 배열이 아님)으로 등록하는 등의 작업을 수행할 수 있습니다. nestableForm 지시어의 이 구현은 프로젝트의 요구 사항을 충족했으며 여기에서 그대로 제공됩니다. 새 양식을 추가하거나 기존 양식을 동적으로 제거(*ngIf)하고 양식의 상태를 부모에게 전파하는 것과 같은 몇 가지 기본적인 경우를 다룹니다. 이것은 기본적으로 하나의 변경 감지 주기(연기 여부 포함) 내에서 해결할 수 있는 작업으로 요약됩니다.
2회의 변경 감지 라운드가 필요한 일부 입력(예: [required]="someCondition")에 조건부 유효성 검사를 추가하는 것과 같은 고급 시나리오를 원하는 경우 "1회 감지 주기 해결" 규칙 때문에 작동하지 않습니다. Angular 4에 의해 부과됩니다.
어쨌든, 이 지시문을 사용하거나 다른 솔루션을 구현할 계획이라면 변경 감지와 관련하여 언급된 사항에 대해 매우 주의하십시오. 이 시점에서 이것이 Angular 4가 구현되는 방식입니다. 미래에 바뀔 수도 있습니다. 우리는 알 수 없습니다. 이 기사에서 언급한 Angular 4의 현재 설정 및 강제된 제한은 단점이나 장점이 될 수 있습니다. 그것은 여전히 볼 일입니다.
Angular 4로 간편하게 양식 만들기
보시다시피 Angular 팀은 양식과 관련된 많은 기능을 제공하는 데 정말 좋은 일을 해왔습니다. 이 게시물이 Angular 4에서 다양한 유형의 양식 작업에 대한 완전한 가이드 역할을 하고 양식 중첩 및 변경 감지 프로세스와 같은 고급 개념에 대한 통찰력을 제공하기를 바랍니다.
Angular 4 형식(또는 해당 문제에 대한 다른 Angular 4 주제)과 관련된 모든 다른 게시물에도 불구하고 내 생각에 가장 좋은 출발점은 공식 Angular 4 문서입니다. 또한 Angular 사용자는 코드에 멋진 문서가 있습니다. 여러 번, 나는 그들의 소스 코드와 거기에 있는 문서를 보는 것만으로도 해결책을 찾았습니다. 인터넷 검색이나 다른 것은 없었습니다. 지난 섹션에서 논의한 형식의 중첩에 대해 Angular 4를 배우기 시작하는 AngularJS 개발자는 이 게시물을 작성하는 데 영감을 준 어느 시점에서 이 문제를 우연히 발견할 것이라고 믿습니다.
또한 보았듯이 두 가지 유형의 양식이 있으며 함께 사용할 수 없다는 엄격한 규칙은 없습니다. 코드베이스를 깨끗하고 일관성 있게 유지하는 것은 좋지만 때로는 템플릿 기반 양식을 사용하여 더 쉽게 작업을 수행할 수 있으며 때로는 그 반대입니다. 따라서 약간 더 큰 번들 크기가 마음에 들지 않으면 케이스별로 더 적절하다고 생각하는 것을 사용하는 것이 좋습니다. 혼동을 일으킬 수 있으므로 동일한 구성 요소 내에서 혼합하지 마십시오.
이 게시물에 사용된 플런커
템플릿 기반 양식
반응형 양식, 템플릿 유효성 검사기
반응형 양식, 코드 유효성 검사기
반응형 폼, 폼 빌더
동일한 구성 요소 내에 중첩된 템플릿 기반 양식
동일한 구성 요소 내에 중첩된 반응형 양식
구성 요소 트리를 통한 중첩 양식