2. 반응형 폼
반응형 폼은 화면에 있는 입력 필드의 값이 변경될 때마다 원하는 로직이 실행되게 하는 모델-드리븐 방식의 폼이다. 이 문서에서는 폼 컨틀롤을 만드는 방법부터 시작해서 폼 컨트롤의 값을 갱신하는 방법, 폼 컨트롤 여러 개를 그룹으로 묶어서 한 번에 처리하는 방법, 폼 컨트롤의 유효성을 검사하는 방법, 동적으로 폼 컨트롤을 추가하고 제거하는 방법에 대해 안내한다.
Note
- 이 문서에서 다루는 예제는 Reactive Forms live-example / 다운로드 링크에서 직접 실행하거나 다운받아 확인할 수 있다.
1. 사전지식
반응형 폼에 대해 알아보기 전에 이런 내용을 미리 알아두는 것이 좋다:
- TypeScript 프로그래밍
- Angular 개요 문서에서 설명하는 Angular 앱 설계 개념
- 폼 소개 문서에서 설명하는 폼 구성 개념
2. 반응형 폼 개요
반응형 폼이란 원하는 시점에 명시적으로 폼에 접근해서 상태를 참조하는 방식을 이야기한다. 반응형 폼에서 폼 상태가 변경되면 새로운 상태를 반환하기 때문에 전체 폼 모델 중 어느 부분이 변경되었는지 추적할 수 있다. 반응형 폼은 옵저버블 스트림을 기반으로 동작하기 때문에, 폼에 입력되는 값도 스트림 형태로 전달된다.
반응형 폼을 사용하면 개발자가 의도한 대로만 데이터가 변경되며, 현재 상태를 쉽게 예측할 수 있기 때문에 테스트하기도 편하다. 데이터가 변경되는 것을 감지하는 쪽에서 값을 받아 반응하는 것도 쉽다.
반응형 폼은 여러 가지 면에서 템플릿 기반 폼과 다르다. 반응형 폼은 데이터 모델에 동기 방식으로 접근할 수 있기 때문에 동작을 예측하기 쉬우며, 옵저버블 연산자를 활용해서 조작할 수 있고, 옵저버블 스트림을 추적하는 방식으로 변화를 감지할 수도 있다.
반면에 템플릿 기반 폼은 템플릿 안에서만 동작하며 템플릿 안에 있는 디렉티브를 기반으로 동작하기 때문에 동작을 폼 모델에 직접 접근할 수 없으며 비동기 방식으로만 변화를 감지할 수 있다.
3. 기본 폼 컨트롤 추가하기
폼 컨트롤은 3단계를 거쳐 정의한다.
1] 앱에 반응형 폼 모듈을 로드한다. 이 모듈은 반응형 폼에서 사용하는 디렉티브를 제공하는 모듈이다.
2] 컴포넌트에 FormControl
인스턴스를 정의한다.
3] 템플릿에 FormControl
을 등록한다.
그러면 컴포넌트 템플릿에서 폼이 동작하는 것을 확인할 수 있다.
아래 예제는 폼 컨트롤 하나를 정의하는 예제 코드이다. 이 예제 코드에서 사용자가 입력 필드에 이름을 입력하면 폼이 이벤트를 감지해서 현재 값을 화면에 표시한다.
반응형 폼 모듈 로드하기
반응형 폼 컨트롤을 사용하려면 @angular/forms
패키지가 제공하는 ReactiveFormsModule
을 로드해서 NgModule
의 imports
배열에 추가하면 된다.
import { ReactiveFormsModule } from "@angular/forms";
@NgModule({
imports: [
// 다른 모듈들...
ReactiveFormsModule,
],
})
export class AppModule {}
FormControl
생성하기
Angular CLI 명령 ng generate
를 실행하면 프로젝트에 컴포넌트를 추가할 수 있다.
그리고 폼 컨트롤을 하나 추가하려면 이 컴포넌트 파일에 FormControl
클래스를 로드하고 클래스 프로퍼티로 FormControl
을 선언하면 된다.
import { Component } from "@angular/core";
import { FormControl } from "@angular/forms";
@Component({
selector: "app-name-editor",
templateUrl: "./name-editor.component.html",
styleUrls: ["./name-editor.component.css"],
})
export class NameEditorComponent {
name = new FormControl("");
}
그러면 FormControl
의 생성자가 실행되면서 인자로 전달한 빈 문자열이 초기값으로 설정된다. 이렇게 클래스 프로퍼티에 폼 컨트롤을 선언하면 폼에서 입력되는 이벤트나 값, 유효성 겁사 결과를 컴포넌트 코드에서 간단하게 확인할 수 있다.
템플릿에 폼 컨트롤 연결하기
컴포넌트 클래스에 폼 컨트롤을 추가하고 나면, 컴포넌트 템플릿에서도 폼 컨트롤을 연결해야 이 폼 컨트롤을 사용할 수 있다. 그래서 FormControlDirective
를 의미하는 formControl
을 입력 필드에 지정하면 템플릿과 컴포넌트의 폼 컨트롤을 연결할 수 있다. 이 디렉티브는 ReactiveFormsModule
에서 제공하는 디렉티브이다.
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name" />
Note
ReactiveFormsModule
에서 제공하는 클래스나 디렉티브에 대해 좀 더 알아보려면 아래 반응형 폼 API 섹션을 참고한다.
이렇게 템플릿 바인딩 문법을 사용하면 템플릿에 있는 name
입력 필드가 폼 컨트롤과 연결된다. 이제 폼 컨트롤과 DOM 엘리먼트는 서로 연동되는데, 화면에서 값이 변경되면 모델값도 변경되고, 모델의 값이 변경되면 화면의 값도 변경된다.
컴포넌트 표시하기
이제 NameEditorComponent
를 템플릿에 추가하면 다음과 같은 입력 필드가 화면에 표시된다.
1) 폼 컨트롤 값 표시하기
폼 컨트롤의 값을 표시하려면 다음과 같은 방법을 사용할 수 있다:
- 폼 값이 변경된 것을 알려주는
valueChanges
옵저버블을AsyncPipe
파이프와 함께 사용할 수 있다. 이 옵저버블은 컴포넌트 클래스에서subscribe()
메서드로 구독해도 된다. - 폼 컨트롤의 현재 값을 표현하는
value
프로퍼티를 사용해도 된다.
다음 예제는 폼 컨트롤의 갑을 문자열 삽입으로 템플릿에 표시하는 예제 코드이다.
그리고 이 값은 폼 컨트롤 엘리먼트의 값이 변경될 때마다 자동으로 갱신된다.
반응형 폼에서는 각 폼 컨트롤의 프로퍼티나 메서드를 사용해서 폼 컨트롤이 제공하는 정보에 접근할 수 있다. 프로퍼티와 메서드는 AbstractControl
클래스에 정의되어 있으며, 폼 컨트롤의 값을 확인하는 것외에도 폼 컨트롤의 상태를 확인하거나 유효성을 검사하는 용도로도 사용할 수 있다.
2) 폼 컨트롤 값 변경하기
반응형 폼에서 제공하는 메서드를 사용하면 사용자의 동작 없이도 폼 컨트롤의 값을 컴포넌트 코드에서 변경할 수 있다. 이 중 setValue()
메서드를 사용하면 폼 컨트롤의 값을 변경할 수 있으며, 이 때 폼 컨트롤의 구조에 맞지 않는 데이터가 전달되는 유효성 검사도 수행한다. 그래서 백엔드 API나 서비스에서 데이터를 가져온 후에 setValue()
메서드를 사용하면 현재 폼 컨트롤의 값 전체를 한 번에 변경할 수 있다.
다음 예제 코드는 setValue()
메서드를 사용해서 폼 컨트롤의 값을 Nancy로 변경하는 예제 코드이다.
이 메서드를 실행할 수 있도록 템플릿을 수정한다. 다음과 같이 구현한 후에 Update Name 버튼을 클릭하면, 폼 컨트롤 엘리먼트에 입력된 값에 관계없이 지정된 값으로 변경된다.
<button (click)="updateName()">Update Name</button>
그리고 이 때 폼 컨트롤의 값은 폼 모델과 연결되어 있기 때문에, 버튼을 눌러서 폼 컨트롤의 값이 변경되면 컴포넌트 클래스에 있는 폼 모델 값도 함께 변경된다.
Note
- 이 예제에서는 폼 컨트롤 하나를 대상으로
setValue()
메서드를 사용했지만, 이 메서드의 인자로 전달하는 타입에 따라 폼 그룹이나 폼 배열에도 사용할 수 있다.
4. 연관된 폼 컨트롤 묶기
폼은 보통 몇 개 폼이 연관되는 방식으로 구성된다. 반응형 폼을 사용하면 두 가지 방법으로 폼 컨트롤을 묶어서 관리할 수 있다.
- 관련된 폼 컨트롤을 그룹으로 정의할 수 있다. 이번 섹션에서는 폼 그룹에 대해 이야기 해보자. 폼이 복잡하다면 중첩된 폼 그룹을 구성할 수도 있다.
- 폼이 동적으로 변경된다면 폼 배열을 정의할 수 있다. 그리고 이 경우에도 폼이 복잡하다면 폼 배열을 중첩할 수도 있다.
입력 필드 하나를 의미하는 폼 컨트롤 여러 개는 함께 묶어서 폼 그룹으로 처리할 수 있다. 실제로 폼을 구성할 때도 개별 폼 컨트롤을 처리하지 않고 폼 그룹으로 한 번에 처리하는 것이 효율적이며, 폼 그룹에 포함된 폼 컨트롤은 이름으로 구분한다. 폼 컨트롤 여러 개를 폼 그룹으로 한 번에 처리하는 방법을 알아보자.
다음 명령을 실행해서 ProfileEditor
컴포넌트를 생성하고 이 컴포넌트에 @angular/forms
패키지에서 제공하는 FormGroup
클래스와 FormControl
클래스를 로드한다.
import { FormGroup, FormControl } from "@angular/forms";
폼 그룹은 다음과 같은 단계로 추가한다.
1] FormGroup
인스턴스를 추가한다.
2] FormGroup
모델과 화면을 연결한다.
3] 폼 데이터를 저장한다.
FormGroup
인스턴스 생성하기
컴포넌트 클래스에 profileForm
프로퍼티를 선언하고 이 프로퍼티에 폼 그룹 인스턴스를 생성해서 할당한다. 폼 그룹을 생성하려면 폼 컨트롤과 키를 매칭시켜서 생성자에 인자로 전달하면 된다.
그러면 이 폼 안에 firstName
인스턴스와 lastName
인스턴스가 선언된다.
import { Component } from "@angular/core";
import { FormGroup, FormControl } from "@angular/forms";
@Component({
selector: "app-profile-editor",
templateUrl: "./profile-editor.component.html",
styleUrls: ["./profile-editor.component.css"],
})
export class ProfileEditorComponent {
profileForm = new FormGroup({
firstName: new FormControl(""),
lastName: new FormControl(""),
});
}
이제 폼 컨트롤을 그룹으로 묶었다. 폼 컨트롤을 그룹으로 묶으면 폼 컨트롤의 값을 번거롭게 하나씩 참조할 필요 없이 폼 그룹에서 제공하는 모델을 사용할 수 있다. 폼 그룹 인스턴스도 폼 컨트롤 인스턴스와 비슷하게 value
나 untouched
프로퍼티, setValue()
메서드를 제공한다.
FormGroup
모델과 화면 연결하기
폼 그룹도 각각의 폼 컨트롤의 상태와 변화를 추적한다. 그래서 폼 컨트롤 중 하나의 상태나 값이 변경되면 이 폼 컨트롤을 감싸고 있는 부모 폼 컨트롤도 새로운 상태로 변경되고 값도 변경된다. 이 폼 그룹이 관리하는 모델은 각각의 폼 컨트롤로 구성된다. 그래서 이 모델을 템플릿에 연결하면 폼 컨트롤을 처리했던 것과 비슷하게 폼 그룹을 처리할 수 있다.
<form [formGroup]="profileForm">
<label for="first-name">First Name: </label>
<input id="first-name" type="text" formControlName="firstName" />
<label for="last-name">Last Name: </label>
<input id="last-name" type="text" formControlName="lastName" />
</form>
폼 그룹은 폼 컨트롤의 묶음을 표현하며, 이 예제 코드에서는 FormGroup
타입의 profileForm
이 form
엘리먼트와 바인딩되어 폼 모델과 입력 필드를 연결한다. 이 코드에 사용된 formControlName
은 FormControlName
디렉티브의 셀렉터이며, 개별 입력 필드를 FormGroup
안에 있는 폼 컨트롤과 연결하는 동작을 수행한다. 그러면 각각의 폼 컨트롤에 연결된 엘리먼트는 해당 폼 컨트롤이 관리하며, 폼 그룹 인스턴스를 통해 전체 모델에 접근할 수도 있다.
데이터 저장하기
이 예제에서는 ProfileEditor
컴포넌트가 사용자의 입력을 받기만 하지만, 실제 애플리케이션이라면 폼에 입력된 값을 컴포넌트 외부로 전달해서 어떤 동작을 수행해야 할 것이다. FormGroup
디렉티브는 form
엘리먼트에서 발생하는 submit
이벤트를 감지하며, submit
이벤트가 발생했을 때 폼에 바인딩된 콜백 함수를 실행하기 위해 ngSubmit
이벤트를 새로 발생시킨다.
다음과 같이 form
태그에 ngSubmit
이벤트를 바인딩해서 onSubmit()
콜백 메서드가 실행되게 작성해 보자.
<form [formGroup]="profileForm" (ngSubmit)="onSubmit()"></form>
그러면 ProfileEditor
컴포넌트에 정의된 onSubmit()
메서드가 profileForm
의 현재 값을 읽어서 특정 동작을 실행하게 할 수 있다. 이 때 컴포넌트와 폼의 캡슐화를 보장하기 위해 EventEmitter
를 사용해서 폼 값을 컴포넌트 외부로 전달하는 것이 좋다. 다음 예제는 이렇게 가져온 폼 값을 console.warn
함수로 브라우저 콘솔에 출력하는 예제이다.
onSubmit() {
// TODO: EventEmitter로 폼 내용 보내기
console.warn(this.profileForm.value);
}
submit
이벤트는 form
태그에서 발생하는 네이티브 DOM 이벤트이다. 이 이벤트는 submit
타입의 버튼을 클릭했을 때 발생하며, 사용자가 폼 내용을 입력하고 엔터 키를 눌렀을 때도 발생한다.
이 이벤트를 발생시키기 위해 폼 아래에 button
엘리먼트를 다음과 같이 추가한다.
<p>Complete the form to enable button.</p>
<button type="submit" [disabled]="!profileForm.valid">Submit</button>
Note
- 예제로 구현한 버튼은
profileForm
의 유효성 검사 결과가 유효하지 않을 때disabled
어트리뷰트를 지정하도록 바인딩되어 있다. - 아직 유효성 검사 로직은 아무것도 적용하지 않았기 때문에 버튼은 항상 활성화되어 있을 것이다.
컴포넌트 표시하기
ProfileEditor
컴포넌트를 화면에 표시하기 위해 AppComponent
템플릿에 다음과 같이 컴포넌트를 추가한다.
그러면 ProfileEditor
컴포넌트가 화면에 표시되며, 이 컴포넌트에 정의된 폼 그룹 안에 있는 firstName
과 lastName
폼 컨트롤도 화면에 함께 표시된다.
1) 중첩 폼 그룹(Neting form groups) 만들기
폼 그룹은 폼 그룹 안에 있는 개별 폼 컨트롤 인스턴스에 직접 접근할 수 있으며 다른 폼 그룹도 자식 폼 컨트롤로 가질 수 있다. 그래서 폼 그룹을 논리적으로 구성하면 폼 전체를 한 번에 관리하기도 편하다.
복잡한 폼을 구성할 때는 관련된 영역끼리 작게 묶어서 관리하는 것이 편하다. 결국 관련된 영역을 작게 묶는 것이 폼 그룹의 역할이다.
복잡한 폼을 구성할 때는 이렇게 구현해 보자.
1] 중첩된 그룹을 정의한다.
2] 템플릿에서도 중첩된 그룹을 묶어서 관리한다.
데이터들 중에는 같은 그룹으로 묶는 것이 자연스러운 경우가 있다. 사용자의 이름과 주소가 그런 경우에 해당된다.
중첩 그룹 만들기
profileForm
폼 안에 중첩된 폼을 만들기 위해 address
그룹을 이렇게 구성해 보자.
import { Component } from "@angular/core";
import { FormGroup, FormControl } from "@angular/forms";
@Component({
selector: "app-profile-editor",
templateUrl: "./profile-editor.component.html",
styleUrls: ["./profile-editor.component.css"],
})
export class ProfileEditorComponent {
profileForm = new FormGroup({
firstName: new FormControl(""),
lastName: new FormControl(""),
address: new FormGroup({
street: new FormControl(""),
city: new FormControl(""),
state: new FormControl(""),
zip: new FormControl(""),
}),
});
}
이 예제에서 firstName
과 lastName
은 폼 컨트롤이지만 address
는 street
와 city
, state
, zip
으로 이루어진 폼 그룹이다. address
의 FormGroup
은 그 자체로도 폼 그룹이지만 부모 폼 그룹인 profileForm
의 자식 폼 그룹이며, 기존에 사용하던 폼 상태와 값이 변경되는 상황도 이전과 마찬가지로 참조할 수 있다. 자식 객체에서 발생하는 상태/값 변화는 부모 폼 그룹으로 전파되기 때문에, 전체 폼 그룹을 참조해도 개별 값이 변경되는 것을 감지할 수 있다.
템플릿에서 중첩 폼 묶기
컴포넌트 클래스의 모델을 수정하고 나면, 템플릿에도 이 내용과 관련된 폼 그룹 인스턴스와 입력 엘리먼트를 수정해야 한다.
ProfileEditor
템플릿에 다음과 같이 address
폼 그룹을 추가한다.
<div formGroupName="address">
<h2>Address</h2>
<label for="street">Street: </label>
<input id="street" type="text" formControlName="street" />
<label for="city">City: </label>
<input id="city" type="text" formControlName="city" />
<label for="state">State: </label>
<input id="state" type="text" formControlName="state" />
<label for="zip">Zip Code: </label>
<input id="zip" type="text" formControlName="zip" />
</div>
그러면 ProfileEditor
폼에서 address
부분이 다시 한 번 그룹으로 묶여 다음과 같이 화면에 표시된다.
Note
- 폼 그룹의 내용을 컴포넌트 템플릿에 표시하려면
value
프로퍼티에JsonPipe
를 사용하면 된다.
2) 데이터 모델 일부만 갱신하기
폼 그룹을 처리하면서 매번 전체 폼 값을 한 번에 갱신하는 것은 번거롭기 때문에, 일부 폼 컨트롤의 값만 변경하는 것이 더 편한 경우도 있다. 이번에는 폼 모델의 일부 값만 변경하는 방법을 알아보자.
모델 값은 두 가지 방법으로 변경할 수 있다.
- 폼 컨트롤에
setValue()
메서드를 사용할 수 있다.setValue()
메서드는 폼 그룹의 구조와 정확하게 일치하는 객체를 받았을 때만 폼 그룹 전체값을 한 번에 갱신한다. - 폼 모델의 특정 부분만 변경하려면
patchValue()
메서드를 사용할 수 있다.
setValue()
메서드를 사용하면 인자의 형태가 복잡한 폼 구조에 맞을 때만 값을 변경할 수 있다. 반면에 patchValue()
메서드는 폼 그룹의 구조와 다른 인자가 전달되어도 에러를 발생하지 않는다.
아래 예제는 ProfileEditorComponent
에 선언된 updateProfile
메서드를 사용해서 사용자의 이름과 주소를 변경하는 예제 코드이다.
updateProfile() {
this.profileForm.patchValue({
firstName: 'Nancy',
address: {
street: '123 Drew Street'
}
});
}
그리고 이 메서드를 실행하는 버튼을 템플릿에 다음과 같이 추가한다.
<button (click)="updateProfile()">Update Profile</button>
이제 사용자가 이 버튼을 클릭하면 profileForm
모델의 firstName
값과 street
값이 새로운 값으로 변경된다. 이 때 폼 그룹의 구조에 맞추기 위해 street
필드의 값은 address
객체 안에 지정했다. patchValue()
메서드는 전달된 인자 중에서 폼 모델의 구조에 맞는 값만 변경한다.
5. FormBuilder
로 폼 컨트롤 생성하기
폼 컨트롤을 매번 직접 생성해야 하는데, 이런 폼이 여러 개라면 귀찮은 반복작업이 될 것이다. 이 때 FormBuilder
서비스를 사용하면 좀 더 편하게 폼을 구성할 수 있다.
FormBuilder
서비스는 이렇게 사용한다.
1] FormBuilder
클래스를 로드한다.
2] FormBuilder
서비스를 의존성으로 주입한다.
3] FormBuilder
서비스로 폼을 구성한다.
아래 예제를 보면서 ProfileEditor
컴포넌트에서 폼을 정의하는 부분을 폼 빌더 서비스를 사용하는 방식으로 리팩토링하려면 어떻게 해야 하는지 확인해 보자.
FormBuilder
클래스 로드하기
먼저 @angular/forms
패키지에서 FormBuilder
클래스를 로드한다.
import { FormBuilder } from "@angular/forms";
FormBuilder
서비스를 의존성으로 주입하기
폼 빌더는 ReactiveFormsModule
에서 제공하며, 의존성으로 주입할 수 있도록 서비스로 정의되어 있다. 다음과 같이 컴포넌트 생성자에 FormBuilder
서비스를 주입한다.
폼 컨트롤 생성하기
FormBuilder
서비스는 control()
, group()
, array()
메서드를 제공하는데, 이 메서드는 각각 폼 컨트롤, 폼 그룹, 폼 배열 인스턴스를 생성해서 반환하는 팩토리 메서드이다.
group()
메서드를 사용해서 profileForm
컨트롤을 만들어 보자.
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
@Component({
selector: "app-profile-editor",
templateUrl: "./profile-editor.component.html",
styleUrls: ["./profile-editor.component.css"],
})
export class ProfileEditorComponent {
profileForm = this.fb.group({
firstName: [""],
lastName: [""],
address: this.fb.group({
street: [""],
city: [""],
state: [""],
zip: [""],
}),
});
constructor(private fb: FormBuilder) {}
}
위 코드에서 group()
메서드에 전달하는 객체의 구조는 폼 모델의 구조와 같다. 그리고 프로퍼티 값으로는 배열을 전달하는데, 폼 컨트롤의 초기값은 배열의 첫 번째 항목으로 전달한다.
Note
- 폼 컨트롤의 초기값만 지정한다면 배열을 사용하지 않아도 된다.
- 하지만 폼 컨트롤에 유효성 검사기를 지정하려면 프로퍼티 값에 배열을 사용해야 한다.
- 이 때 동기 유효성 검사기는 두 번째 인자로, 비동기 유효성 검사기는 세 번째 인자로 지정한다.
폼 컨트롤을 직접 생성하는 방식과 폼 빌더를 사용하는 방식이 어떻게 다른지 비교해 보자.
6. 폼 유효성 검사하기
폼 유효성 검사(Form validation)는 사용자가 필수 항목을 모두 입력했는지, 입력한 내용이 올바른지 검증하는 것이다. 이 섹션에서는 폼 컨트롤에 유효성 검사기를 하나 붙여서 폼 상태를 화면에 표시해 보자.
폼 유효성 검사기는 다음과 같은 과정으로 적용한다.
1] 폼 컴포넌트에 유효성 검사 함수를 로드한다.
2] 원하는 폼 필드에 유효성 검사기를 적용한다.
3] 폼 유효성 검사 결과에 따라 적절한 처리 로직을 추가한다.
가장 일반적인 유효성 검사는 필드를 필수 항목으로 만드는 것이다. 아래 예제를 보면서 firstName
폼 컨트롤에 필수항목 유효성 검사기를 추가하고 유효성 검사 결과를 화면에 표시하는 방법에 대해 알아보자.
유효성 검사 함수 로드하기
반응형 폼 모듈은 다양한 유효성 검사 함수도 함께 제공한다. 이 함수들은 폼 컨트롤 인스턴스를 인자로 받으며, 유효성 검사에 실패한 경우에 에러 객체를 반환하고 유효성 검사를 통과하면 null
을 반환한다.
먼저, @angular/forms
패키지에서 Validators
클래스를 로드한다.
import { Validators } from "@angular/forms";
필수 항목 지정하기
ProfileEditor
컴포넌트에서 firstName
폼 컨트롤을 생성하던 배열의 두 번째 항목으로 Validators.required
를 추가한다.
profileForm = this.fb.group({
firstName: ["", Validators.required],
lastName: [""],
address: this.fb.group({
street: [""],
city: [""],
state: [""],
zip: [""],
}),
});
폼 상태 표시하기
폼 컨트롤을 필수항목으로 지정하면 이 폼 컨트롤의 초기 상태는 유효하지 않은 것이 된다. 폼 컨트롤의 상태는 부모 폼 그룹 엘리먼트로 전파되며, 결과적으로 폼 그룹 전체가 유효성 검사를 통과하지 못한 것으로 처리된다. 그러면 폼 그룹 인스턴스의 status
프로퍼티를 참조해서 이 폼의 상태를 확인할 수 있다.
profileForm
의 상태를 표시하려면 다음과 같이 템플릿을 작성한다.
<p>Form Status: {{ profileForm.status }}</p>
firstName
폼 컨트롤이 필수항목으로 지정되었지만 유효성 검사를 통과하지 못했기 때문에 profileForm
전체가 유효하지 않은 것으로 판단되고, Submit 버튼도 비활성화된다. firstName
입력 필드에 데이터를 입력하면 폼 전체의 유효성 검사가 통과되면서, Submit 버튼도 활성화될 것이다.
7. 동적 폼 구성하기
폼 컨트롤에 이름이 없고 폼 컨트롤 개수가 변한다면 FormGroup
대신 FormArray
를 사용하는 방법도 고려해볼만 하다. 폼 그룹 인스턴스와 마찬가지로 폼 배열도 폼 컨트롤은 동적으로 추가하거나 제거할 수 있으며, 자식 폼 컨트롤을 모아 값이나 유효성 검사 결과를 한 번에 참조할 수도 있다. 폼 배열은 이름을 지정하는 방식으로 정의하지 않는데, 자식 폼 컨트롤의 개수가 몇 개인지 몰라도 된다는 점에서 이 방식이 유리하다.
동적 폼은 이런 순서로 구현한다.
1] FormArray
클래스를 로드한다.
2] FormArray
컨트롤을 정의한다.
3] 게터 메서드를 사용해서 FormArray
컨트롤에 접근한다.
4] 템플릿에 폼 배열을 연결한다.
폼 배열을 어떻게 활용할 수 있는지 예제를 보면서 확인해 보자.
FormArray
클래스 로드하기
먼저 @angular/forms
패키지에서 FormArray
클래스를 로드한다. 이것은 컴포넌트 클래스에서 타입 참조를 위해 로드한 것이며, FormBuilder
는 이 과정이 없어도 내부적으로 FormArray
타입을 지원한다.
import { FormArray } from "@angular/forms";
FormArray
정의하기
폼 배열은 배열로 초기화하며, 이 때 배열의 길이는 어떠한 것이라도 가능하다. profileForm
에 aliases
프로퍼티를 추가하고, 이 프로퍼티를 폼 배열로 정의해 보자.
FormBuilder
를 사용한다면 array()
메서드로 폼 배열을 정의할 수 있으며, FormBuilder.control()
메서드를 사용해서 기본 폼 컨트롤을 생성한다.
profileForm = this.fb.group({
firstName: ["", Validators.required],
lastName: [""],
address: this.fb.group({
street: [""],
city: [""],
state: [""],
zip: [""],
}),
aliases: this.fb.array([this.fb.control("")]),
});
그러면 aliases
폼 컨트롤은 FormGroup
안에 폼 배열로 선언되며, 이후에 동적으로 추가되기 전까지는 폼 컨트롤 1개로 구성된다.
FormArray
에 접근하기
폼 배열을 사용한다면 폼 배열 안의 각 인스턴스를 profileForm.get()
메서드로 참조하는 것보다 게터(getter) 함수를 사용하는 것이 편하다. 게터 함수를 사용하면 폼 배열 안에 있는 폼 컨트롤의 개수에 관계없이 간단하게 폼 컨트롤의 값을 참조할 수 있고, 반복문을 작성할 때도 편하다.
게터 함수는 다음과 같이 정의한다.
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
Note
- 폼 컨트롤을 참조하기 위해
get()
메서드를 사용하면AbstractControl
타입으로 폼 컨트롤을 받는다. - 공통 메서드를 사용한다면 이대로 활용해도 되지만,
FormArray
에 해당되는 메서드를 사용하려면 타입 캐스팅해야 한다.
이번에는 FormArray
에 동적으로 폼 컨트롤을 추가하는 함수를 정의해 보자. 폼 배열에 새로운 폼 컨트롤을 추가할 때는 FormArray.push()
메서드를 사용한다.
addAlias() {
this.aliases.push(this.fb.control(''));
}
이제 이렇게 만든 폼 배열을 템플릿에 표시해 보자.
템플릿에 폼 배열 표시하기
사용자가 폼 모델에 aliases
값을 입력하려면 이 폼 컨트롤을 템플릿에 추가해야 한다. 이전 예제에서 FormGroupNameDirective
를 formGroupName
어트리뷰트로 바인딩했던 것과 비슷하게, FormArrayNameDirective
가 제공하는 formArrayName
어트리뷰트를 사용해서 FormArray
를 템플릿에 바인딩하면 된다.
formGroupName
<div>
엘리먼트 뒤에 다음과 같은 템플릿을 추가한다.
<div formArrayName="aliases">
<h2>Aliases</h2>
<button (click)="addAlias()" type="button">+ Add another alias</button>
<div *ngFor="let alias of aliases.controls; let i=index">
<!-- alias 폼 배열이 반복되는 부분 -->
<label for="alias-{{ i }}">Alias:</label>
<input id="alias-{{ i }}" type="text" [formControlName]="i" />
</div>
</div>
*ngFor
디렉티브는 폼 배열 인스턴스의 각 폼 컨트롤 인스턴스를 순회한다. 폼 배열 안에 있는 항목은 이름이 없기 때문에 인덱스를 변수 i
에 활용해서 formControlName
으로 바인딩했다.
이제는 폼 배열의 개수가 변하더라도 각각의 폼 컨트롤은 인덱스로 관리된다. 이 폼 배열이나 전체 폼 그룹의 값, 상태를 확인할 때도 같은 인덱스를 활용할 수 있다.
별칭 추가하기
이 폼의 Alias
필드는 기본적으로 폼 컨트롤이 하나 존재한다. 그리고 Add Alias 버튼을 누르면 폼 컨트롤이 추가되며, 이렇게 추가된 폼 컨트롤의 유효성 상태도 템플릿 아래에 추가했던 Form Value
부분을 통해 확인할 수 있다.
Note
aliases
필드는 폼 컨트롤 단위로 추가할 수도 있지만 폼 그룹 단위로 추가할 수도 있다.- 폼 컨트롤을 정의하고 활용하는 방법은 같다.
8. 반응형 폼 API
아래 표는 반응형 폼을 구현할 때 자주 사용하는 클래스와 서비스 목록을 나열한 것이다.
클래스 | 설명 |
---|---|
AbstractControl |
폼 컨트롤을 표현하는 FormControl , FormGroup , FormArray 의 추상 클래스이다. 폼 컨트롤의 공통 기능과 프로퍼티를 정의한다. |
FormControl |
개별 폼 컨트롤의 값과 유효성 검사 상태를 관리하는 클래스이다. 이 클래스는 HTML 문서의 <input> 이나 <select> 엘리먼트와 연동된다. |
FormGroup |
연관된 AbstractControl 인스턴스를 그룹으로 관리할 때 사용하는 클래스이다. 이 그룹은 자식 폼 컨트롤을 프로퍼티로 관리하며, 그룹에 접근해도 자식 폼 컨트롤의 값이나 유효성 검사 상태도 확인할 수 있다. 컴포넌트의 최상위 폼도 FormGroup 이다. |
FormArray |
AbastractControl 을 배열 형태로 관리할 때 사용하는 클래스이다. |
FormBuilder |
폼 컨트롤 인스턴스를 간편하게 만들 때 사용하는 서비스이다. |
디렉티브 | 설명 |
---|---|
FormControlDirective |
개별 FormControl 인스턴스와 폼 컨트롤 엘리먼트를 연결한다. |
FormControlName |
이름을 기준으로 FormGroup 안에 있는 FormControl 을 연결한다. |
FormGroupDirective |
FormGroup 인스턴스를 DOM 엘리먼트와 연결한다. |
FormGroupName |
중첩된 FormGroup 인스턴스를 DOM 엘리먼트와 연결한다. |
FormArrayName |
FormArray 인스턴스를 DOM 엘리먼트와 연결한다. |