Web Application에 빠질 수 없는 요소가 form입니다. Angular Form요소들을 쉽게 제어할 수 있는 여러 기능들을 제공합니다. 이번에는 Angular가 제공하는 Form 기능의 중 가장 기본적인 내용들을 공유해 보려고 합니다.

이 페이지는 다음의 기능들을 다루고 있습니다.

  • 컴포넌트와 템플릿에 Angular form 만들기.
  • 인풋 값의 읽기와 쓰기를 위한 two-way 바인딩을 만드는 ngModel 사용하기.
  • 상태와 변경을 추적하고, form 컨트롤 유효성 검사하기.
  • 컨트롤 상태를 추적하는 CSS 클래스들을 이용해서 view에 표현하기.
  • 사용자에게 유효성 검사 에러를 보여주고, 사용/비사용 컨트롤 하기.
  • template 참조 변수를 이용하여 HTML 테그 간에 정보 공유하기.

템플릿 기반의 form.

이 페이지에서는 form에 특화된 directives와 명시된 기술을 가지고, Angular template 문법으로 템플릿을 작성하여 form을 만듭니다.

또 다른 방법으로, reactive 접근법(또는 model-driven)을 이용해서 form을 만들 수 있습니다. 하지만, 이 글에서는 template기반의 form에 집중합니다. template-driven 방식은 간단한 form에 대한 검증을 구현하는데 용이합니다.

Angular 템플릿을 사용하면 대부분의 form을 만들 수 있습니다. 로그인, 문의하기, 그외 많은 사업적인 form들을.. Angular를 쓰면 레이아웃을 만들고, 데이터를 바인딩 하고, 특정 유효성 검사를 적용하고, 검사 에러를 보여주고, 활성/비활성을 조절하고, view에 피드백을 보여줄 수 있습니다.

Angular는 반복적인 작업 프로세스를 쉽게 핸들링 할 수 있게 도와줍니다.

여기서 실시간 예제를 보실 수 있으니 함께 테스트하시면 더욱 좋습니다. 실시간 예제를 열어보시면 아래 이미지와 같은 화면을 볼 수 있습니다.

Angular form 만들기

아래는 임의 app/hero-form.component.html 컴포넌트의 템플릿 소스입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="container">
<h1>Hero Form</h1>
<form>
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" required>
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo">
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>
</div>

소스 안에 form 요소가 있고 form 안에는 input, button등의 form을 구성하는 요소들이 있습니다. 별다를게 없다구요? 네 맞습니다. 당연한 이야기 일 수 있지만 angular의 form은 기본 html form으로 부터 출발합니다. 여기에 angular만의 기능들을 하나하나 추가해 봅시다.

Add powers with *ngFor

이번 주제와는 큰 상관없지만, select 요소를 추가 하기 위에 *ngFor 지시자를 추가하겠습니다. button 테그 위에 아래 소스를 추가합니다.

1
2
3
4
5
6
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" id="power" required>
<option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
</select>
</div>

위 소스에서 *ngfor는, component의 powers 배열 길이만큼 순회하면서 option 테그를 생성하고 valuetext에 배열의 각 요소를 바인딩합니다.

two-way 바인딩

위 form에 two-way 바인딩을 만들어 봅시다. two-way 바인딩을 위해서는 ngModel 지시자(directive)를 추가해 줍니다.

1
2
3
<input type="text" class="form-control" id="name" required
[(ngModel)]="model.name" name="name">
TODO: remove this: {{model.name}}

자, 이제 인풋 요소에는 Owen Jeon이라는 이름이 보입니다. 그리고 input 값을 수정할 때마다 TODO: remove this:에 동일한 텍스트가 실시간 반영되어 나타납니다.
Angular에서 데이타 바인딩의 방향은 기본적으로 컴포넌트(클래스) -> 템플릿 입니다. two-way 바인딩을 사용하면 템플릿에서 컴포넌트로도 데이터를 바인딩 할 수 있습니다.
즉 위 예제에서 input 값이 수정되면 이 데이터가 컴포넌트로 바인딩 되고, 이 데이터가 다시 템플릿에 바인딩 되어 TODO: remove this:에 나타나게 되는 것입니다.

two-way 바인딩에 대한 더 자세한 내용은 구글 공식문서를 참고하세요.

<input> 태그에 name 속성을 추가하고 "name"으로 설정한 것은 다른 의미가 있습니다. [(ngModel)]form 태그와 함께 사용하려면 name 속성을 정의해야합니다.
(다시 말해서, form 태그를 쓰지 않는다면 name속성은 없어도 잘 동작 합니다.)

내부적으로 Angular는 FormControl 인스턴스를 만들고 <form> 태그에 Angular가 첨부한 NgForm 지시자(directive)를 등록합니다. 각 FormControlname 속성에 지정한 이름으로 등록됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
{{diagnostic}}
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" id="name" required
[(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" class="form-control" id="alterEgo"
[(ngModel)]="model.alterEgo" name="alterEgo">
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" id="power" required
[(ngModel)]="model.power" name="power">
<option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
</select>
</div>
...
  • input 태그에는, label 태그의 for 속성이 input과 매칭시키는 데 사용하는 id 속성이 있습니다.
  • input 태그에는 angular의 form 이 가진 컨트롤을 form에 등록하는 데 필요한 name 속성이 있습니다.

위쪽 영역(diagnostic)에 model의 값들이 반영되어 나타납니다.

Track control state and validity with ngModel

form에 ngModel를 사용하면 2 way 바인딩 이외에도, 사용자가 컨트롤 영역을 손댔는지(touch), 값이 변경되었는지, 값이 유효성 검사를 통과 했는지 등을 확인 할 수 있습니다.
이는 css class 이름으로 나타나게 됩니다.

상태 true일때 class false일때 class
컨트롤 영역에 손을 댔음. ng-touched ng-untouched
값 변경됨. ng-dirty ng-pristine
유효성 검사 통과. ng-valid ng-invalid

template의 input에 #spy를 추가합니다. 그러면 spy 변수에 input엘리먼트 객체가 담기게되어, 해당 엘리먼트 프로퍼티에 접근 할 수 있습니다. className프로퍼티를 사용해서 CSS class에 접근해 봅시다.

1
2
3
4
5
<input type="text" class="form-control" id="name"
required
[(ngModel)]="model.name" name="name"
#spy>
<br>TODO: remove this: {{spy.className}}


위 그림을 보면 input을 조작함에 따라 CSS class가 어떻게 바뀌는지 알 수 있습니다.

유효성 검증 메시지 보여주기

form의 기능을 향상시키기 위해서, 에러 메세지를 보여주려고 합니다. 에러 메세지는 사용자가 입력에 반응해, 본인이 잘못 입력하는 것을 바로바로 알려 줄 수 있습니다.

이 효과를 만들기 위해, <input>테그의 기능을 확장하겠습니다.

  • ngModel을 참조하는 변수를 만듭니다. #name="ngModel"
  • input의 유효성 검사가 통과하지 못했을때 보여 줄 <div>를 만듭니다.
1
2
3
4
5
6
7
<label for="name">Name</label>
<input type="text" class="form-control" id="name" required
[(ngModel)]="model.name" name="name"
#name="ngModel">
<div [hidden]="name.valid || name.pristine" class="alert alert-danger">
Name is required
</div>

template 내에서 input box를 제어하려면 template 참조 변수가 필요합니다. 여기서 name이라는 변수를 선언하고 “ngModel”값을 부여했습니다.

왜 “ngModel”일까요? directive의 exportAs속성은 Angular에게 참조 변수를 directive에 연결하는 방법을 알려줍니다. ngModel directive의 exportAs 속성이 “ngModel”이기 때문에 namengModel로 설정합니다.

위 로직대로 구현하면, input의 input 값이 수정되지 않은 상태거나, 유효성 검사를 통과했다면 error영역을 감춥니다.

Submit the form with ngSubmit

사용자는 form의 인풋 요소들을 입력하고 나면 제출을 할 것입니다. 하단에 있는 제출 버튼은 그 자체로는 특별한 로직이 보이지 않습니다만, type이 submit으로 설정되어 있어서, form의 제출을 트리거 합니다(type="submit").

1
<form (ngSubmit)="onSubmit(ngForm)" #heroForm="ngForm">

form 엘리먼트에 몇가지를 추가했습니다. template 참조 변수인 #heroForm을 정의하고 값을 “ngForm”으로 초기화했습니다. 변수 heroForm은 이제 양식 전체를 관리하는 NgForm directive에 대한 참조입니다.
onSubmit함수를 class에서 받아 form의 값들을 사용할 수 있습니다. 또한 valueChanges프로퍼티를 이용하면 class에서 form 요소들의 값이 변할때마다 값을 observable로 던저줍니다.

이제 우리는 form의 요소들이 모두 validation을 통과했다면 submit버튼을 보여주고, 아니면 가리고 싶습니다.

1
<button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button>

이제 사용자가 Name을 지우면 submit버튼은 사라집니다.

지금까지 template-driven 기반의 form과 관련된 기능들을 살펴보았습니다. 이후에는 model-driven 기반의 form에 대해서 살펴보겠습니다. model-driven은 검증 로직을 class 내에서 구현하여 view와 js코드간의 구분을 보다 명확히 하고, validation기능을 확장 할 수 있다는 장점이 있습니다.