[ Angular 2: Forms ] 템플릿 기반의 폼

폼은 사소해보인다. Form 만을 위한 모듈이 존재할 필요가 있겠나 싶다. 데이타바인딩과 Input을 사용하면 그만일 것도 같다. 하지만, Form은 그 이상의 기능을 가지고 있다. Data Validation, 향상된 UX, Form 상태 확인 등 차원높은 기능을 구사하기 위해서는 Form과 관련된 Angular 2 모듈이 꼭 필요하다.


HTML vs Angular 2 Forms

HTML 로 간단한 폼을 만들어보자.

<form>
  <input type="text">
  <button type="submit">Submit</button>
</from>

늘 이런 html 코드에 접근하는 것은 컴퓨팅 자원을 낭비하는 일이다. 대신, Angluar 2 는 form 기능을 구현하기 위해 Javascript representation 을 사용한다. 가령 아래와 같은 형식이다.

{
  controls: controlName,
  value: {controlName: ''}
}

Angular 2 는 HTML form 에 대응하는 FormGroup 을 만들고 이 FormGroup을 HTML Form과 동조시킨다. 그런 후, 데이타 유효성 검사, 제출(Submission) 등을 처리한다.


Template-driven vs Data-driven Approach

Angular 2 에서 Form을 구현하는 방식은 크게 두가지로 나뉜다.

Template-driven

  • HTML에서 Form 설정이 이루이지고
  • 이 HTML에 바탕해서 Javascript code의 참조가 이루어진다.
  • ngSubmit() 를 통해 폼 데이타가 전송된다.

Data-driven

  • 클라스 내에서 Typescript 로 Form이 설정된다.
  • 참조하지 않고, Angular 2에서 직접적으로 해당 FormGroup 을 다룬다.
  • ngSubmit() 를 통하지않고 클래스 내에서 폼 데이타가 바로 사용될 수 있다.

예제 프로젝트 생성

$ ng new form-play
$ cd form-play

//template-driven 컴포넌트 생성
$ ng g c template-driven -is

//data-driven 컴포넌트 생성
$ ng g c data-driven -is

src/index.html 을 열고 트위터 부트스트랩 스타일을 적용시킨다.

<!doctype html>
<html>

<head>
  <meta charset="utf-8">
  <title>FormPlay</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- Latest compiled and minified CSS -->
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
    crossorigin="anonymous">
</head>

<body>
  <app-root>Loading...</app-root>
</body>

</html>

src/app/app.component.html 을 열고 TemplateDrivenComponent 가 표시되도록 다음과 같이 수정한다.

<div class="container">
  <div class="row">
    <div class="col-md-6 col-md-offset-3">
      <h1>Forms</h1>
      <app-template-driven></app-template-driven>
      <hr>
    </div>
  </div>
</div>

 


Template-Driven Approach Basic

TemplateDrivenComponent 의 html 파일을 열고 다음과 같이 수정한다.

template-driven.component.html

<h1>Template Driven</h1>
<form>
  <div>
    <div class="form-group">
      <label for="username">Username</label>
      <input type="text"
             class="form-control"
             id="username">
    </div>
    <div class="form-group">
      <label for="email">E-Mail</label>
      <input type="text"
             class="form-control"
             id="email">
    </div>
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password"
           class="form-control"
           id="password">
  </div>
  <div class="radio">
    <label>
    </label>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

위의 <form>…</form> 에서 ‘form’은 Angular 2 Form의 selector 역할을 한다. 다른 말로하면, Angular 2는 이 form tag를 통해 form을 자동으로 인식하게 된다. 하지만, 여러 Input 들 중 어떤 것들이 Angular 2에 의해 처리되어야 할 것인지는 앞으로 지정해주어야한다.

Input field 들이 Angular 2 control 로 인식되기 위해서는 다음과 같이 input tag 안에 ngModel 과 name 속성을 추가해준다. 폼값을 넘겨줄 수 있도록 form tag 는 ngSubmit 이벤트 바인딩 처리해준다. 이때, html form 에서 사용되는 action 속성은 지정할 필요가 없다. 대신, #f=”ngForm” 라는 로컬 레퍼런스를 ngSubmit 이벤트 설정 다음에 붙여준다. f는 폼값을 담고 있다.

template-driven.component.html

<h1>Template Driven</h1>
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
  <div>
    <div class="form-group">
      <label for="username">Username</label>
      <input type="text"
             class="form-control"
             id="username"
             ngModel
             name="username">
    </div>
    <div class="form-group">
      <label for="email">E-Mail</label>
      <input type="text"
             class="form-control"
             id="email"
             ngModel
             name="email">
    </div>
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password"
           class="form-control"
           id="password"
           ngModel
           name="password">
  </div>
  <div class="radio">
    <label>
    </label>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

다음은 TemplateDrivenComponent 의 ts 파일에서 onSubmit() 메서드를 정의해준다. 폼 값이 넘어가는지 확인 하기위해 간단히 form값은 콘솔창에 띄워본다.

template-driven.component.ts

import { Component, OnInit } from '@angular/core';
import { NgForm } from "@angular/forms";

@Component({
  selector: 'app-template-driven',
  templateUrl: './template-driven.component.html',
  styles: []
})
export class TemplateDrivenComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

  onSubmit(form:NgForm) {
    console.log(form);
  }

}

브라우저 창에서 실행해보자.

$ ng serve

브라우저 창에서 f12를 눌러 개발자창을 띄우고, 폼에 임의의 값을 입력하고 Submit 버튼을 누르면 다음과 같은 내용이 표시된다. controls 안에 email, password,username 이 등록되어있는 것을 확인할 수 있다.

여기에 표시된 다양한 Property 값은 데이타 유효성을 검사하고 사용자 인터액션을 감지하는 등 다양한 목적으로 사용될 수 있다. 하나하나 뜯어보고 익혀놓자.


Form Validation

위에서 만들었던 form-group 들 중 email control에 관한 것이다.

<div class="form-group">
  <label for="email">E-Mail</label>
  <input type="text"
         class="form-control"
         id="email"
         ngModel
         name="email"
         required             
         pattern="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?">
</div>

required 를 추가 하면 필수항목이 되고, pattern 에 regex email validation 패턴을 대입해주면 email 데이타 유효성 검사가 가능해진다.

다음 이미지에서 노란색 마커로 표시된 email input field에 대한 class 에 주목하자. 우리가 설정한 class는 form-control 뿐이다. 나머지 ng-dirty ng-touched ng-valid 는 Angular 2 가 자동으로 추가한 것이다.

아래와 같이 유효하지 않은 email 이 입력되면 ng-valid class 대신  ng-invalid 가 추가 된다. 이것을 이용하면, 보다 효율적인 UX를 구현할 수 있다.

TemplateDrivenComponent 에서 .ng-invalid css 클라스에 대한 스타일을 지정해주자.

template-driven.component.ts

import { Component, OnInit } from '@angular/core';
import { NgForm } from "@angular/forms";

@Component({
  selector: 'app-template-driven',
  templateUrl: './template-driven.component.html',
  styles: [`
    input.ng-invalid {
      border:1px solid red;
    }
  `]
})
export class TemplateDrivenComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

  onSubmit(form:NgForm) {
    console.log(form);
  }

}

유효하지 않은 항목에 대해 붉은 테두리가 표시되는 것을 확인할 수 있다.


Default Value (기본 값)

각 필드에 들어갈 기본값을 설정하려면 어떻게 해야할까? 간단히 정리하면, ngModel 을 통한 Two way binding을 활용하면 된다. input 태그 안에 ngModel은 현재 Data Binding 이 적용되어 있지 않다. 브라켓 [ ] 과 괄호 ( )로 ngModel을 감싸주고 변수를 할당해주면 된다.

template-driven.component.html

<h1>Template Driven</h1>
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
  <div>
    <div class="form-group">
      <label for="username">Username</label>
      <input type="text"
             class="form-control"
             id="username"
             [(ngModel)]="user.username"
             name="username"
             required>
    </div>
    <div class="form-group">
      <label for="email">E-Mail</label>
      <input type="text"
             class="form-control"
             id="email"
             [(ngModel)]="user.email"
             name="email"
             required             
             pattern="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?">
    </div>
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password"
           class="form-control"
           id="password"
           [(ngModel)]="user.password"
           name="password">
  </div>
  <div class="radio">
    <label>
    </label>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

template-driven.component.ts

import { Component, OnInit } from '@angular/core';
import { NgForm } from "@angular/forms";

@Component({
  selector: 'app-template-driven',
  templateUrl: './template-driven.component.html',
  styles: [`
    input.ng-invalid {
      border:1px solid red;
    }
  `]
})
export class TemplateDrivenComponent implements OnInit {
  user = {
    username:'Jane',
    email:'jane@example.com',
    password:'123456'
  }
  constructor() { }

  ngOnInit() {
  }

  onSubmit(form:NgForm) {
    console.log(this.user);
  }

}

위 ts  파일에서 user object를 설정해주고 기본값을 넣어주었다.

다음과 같이 기본값이 표시되고 Submit을 누르면 콘솔창에 form object 대신에 user object 가 나타난다.

데이타 그룹핑 : ngModelGroup

폼 안에 있는 input 필드들을 필요에 따라 그룹 지어 데이타를 출력해보자. 우리 예에서 userData 라는 그룹이름 아래 username과 email을 묷어보자.

template-driven.component.html

<h1>Template Driven</h1>
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
  <div ngModelGroup="userData">
    <div class="form-group">
      <label for="username">Username</label>
      <input type="text"
             class="form-control"
             id="username"
             [(ngModel)]="user.username"
             name="username"
             required>
    </div>
    <div class="form-group">
      <label for="email">E-Mail</label>
      <input type="text"
             class="form-control"
             id="email"
             [(ngModel)]="user.email"
             name="email"
             required             
             pattern="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?">
    </div>
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password"
           class="form-control"
           id="password"
           [(ngModel)]="user.password"
           name="password">
  </div>
  <div class="radio">
    <label>
    </label>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

template-driven.component.ts

import { Component, OnInit } from '@angular/core';
import { NgForm } from "@angular/forms";

@Component({
  selector: 'app-template-driven',
  templateUrl: './template-driven.component.html',
  styles: [`
    input.ng-invalid {
      border:1px solid red;
    }
  `]
})
export class TemplateDrivenComponent implements OnInit {
  user = {
    username:'Jane',
    email:'jane@example.com',
    password:'123456'
  }
  constructor() { }

  ngOnInit() {
  }

  onSubmit(form:NgForm) {
    console.log(form.value);
  }

}


Radio Button

template-driven.component.html

<h1>Template Driven</h1>
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
  <div ngModelGroup="userData">
    <div class="form-group">
      <label for="username">Username</label>
      <input type="text"
             class="form-control"
             id="username"
             [(ngModel)]="user.username"
             name="username"
             required>
    </div>
    <div class="form-group">
      <label for="email">E-Mail</label>
      <input type="text"
             class="form-control"
             id="email"
             [(ngModel)]="user.email"
             name="email"
             required             
             pattern="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?">
    </div>
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password"
           class="form-control"
           id="password"
           [(ngModel)]="user.password"
           name="password">
  </div>
  <div class="radio" class="radio" *ngFor="let g of genders">
    <label>
      <input type="radio"
            name="gender"
            [(ngModel)]="user.gender"
            [value]="g"> {{g}}
    </label>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

template-driven.component.ts

import { Component, OnInit } from '@angular/core';
import { NgForm } from "@angular/forms";

@Component({
  selector: 'app-template-driven',
  templateUrl: './template-driven.component.html',
  styles: [`
    input.ng-invalid {
      border:1px solid red;
    }
  `]
})
export class TemplateDrivenComponent implements OnInit {
  user = {
    username:'Jane',
    email:'jane@example.com',
    password:'123456',
    gender:'male'
  }
  genders=[
    'male',
    'female'
  ]
  constructor() { }

  ngOnInit() {
  }

  onSubmit(form:NgForm) {
    console.log(form.value);
  }

}


Final Touch

마지막으로, input field 아래에 오류 메시지가 나타나고, 유효하지 않은 폼인 경우 Submit 버튼이 작동하지 않도록 하는 기능을 구현해보자.

22번째 줄 input tag 에 #email=”ngModel” 을 추가함으로써 Angular 2가 이 필드를 관리할 수 있게 해준다. form tag 에 #f=”ngForm”을 추가한 것과 같은 원리다. 23번째 줄 email.valid 는 22번째 줄의 email을 참조한 것이다. 44번째줄의 f.valid는 2번째 줄의 f를 참조한 것이다.

template-driven.component.html

<h1>Template Driven</h1>
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
  <div ngModelGroup="userData">
    <div class="form-group">
      <label for="username">Username</label>
      <input type="text"
             class="form-control"
             id="username"
             [(ngModel)]="user.username"
             name="username"
             required>
    </div>
    <div class="form-group">
      <label for="email">E-Mail</label>
      <input type="text"
             class="form-control"
             id="email"
             [(ngModel)]="user.email"
             name="email"
             required             
             pattern="[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
             #email="ngModel">
      <div *ngIf="!email.valid">
        Invalid Email
      </div>
    </div>
  </div>
  <div class="form-group">
    <label for="password">Password</label>
    <input type="password"
           class="form-control"
           id="password"
           [(ngModel)]="user.password"
           name="password">
  </div>
  <div class="radio" class="radio" *ngFor="let g of genders">
    <label>
      <input type="radio"
            name="gender"
            [(ngModel)]="user.gender"
            [value]="g"> {{g}}
    </label>
  </div>
  <button type="submit" class="btn btn-primary" [disabled]="!f.valid">Submit</button>
</form>

 

 

 

“[ Angular 2: Forms ] 템플릿 기반의 폼”에 대한 2개의 댓글

댓글 남기기