본문 바로가기

프론트엔드/앵귤러

앵귤러 폼 - 리액티브 폼

반응형

앵귤러 리액티브 폼은 옵저버블을 활용하여 폼에 입력되는 값을 스트림의 형태로 관리하며, 새로운 값이 입력될 때마다 이를 감지하고 반영합니다. 템플릿 드리븐 폼과 차이점은 리액티브 폼은 저장된 데이터를 동기식으로 처리하고 원본데이터에 대한 변형이 불가하며 옵저버블의 오퍼레이터를 통해서만 가능합니다.

 

앵귤러 리액티브 폼을 사용하는 방법을 보겠습니다.

목차

사용하기

먼저, 폼을 사용하기 위해서는 앵귤러 폼 라이브러리에서 ReactiveForms 모듈을 가져와야 합니다.

//scr/app/app.module.ts

import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    ReactiveFormsModule // forms module
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

※모듈을 추가를 하지 않으면 아래와 같이 템플릿에서 해당 모듈을 인식할 수 없기 때문에 바인딩이 불가능합니다.

폼을 사용하고자 하는 템플릿에 아래와 같이 폼을 더합니다.

 

기본폼부터 시작해서 하나씩 추가하며 보겠습니다. 먼저, 템플릿 폼의 기능을 하나도 추가하지 않은 폼은 아래와 같습니다.

<!-- form.component.html -->

<form>
  <div>
    <input type="text">
  </div>

  <div>
    <input type="text">
  </div>

  <div>
    <input type="email">
  </div>

  <button>Submit</button>
</form>

리액티프 폼은 폼 그룹과 폼 컨트롤로 나누어지며 각 필드에 입력되는 값을 인식하고 활용하기 위해서 각 필드와 폼컨트롤을 연결합니다.

폼 컨트롤 연결하기

먼저, 클래스 파일에서 폼 컨트롤을 생성합니다.

// format.component.ts

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

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
  title = new FormControl // control
  content = new FormControl // control
  email = new FormControl // control

  constructor() { }

  ngOnInit(): void {
  }
}

생성된 컨트롤을 템플릿의 폼 필드와 연결합니다.

<!-- form.component.html -->

<form>
  <div>
    <input type="text" [formControl]="title">
  </div>

  <div>
    <input type="text" [formControl]="content">
  </div>

  <div>
    <input type="email" [formControl]="email">
  </div>

  <button>Submit</button>
</form>

해당 폼에 서밋 이벤트를 더해 템플릿 폼 객체를 확인해 보겠습니다. 아래와 같이 서밋 이벤트를 연결하고 (리액티브 폼의 경우 자체적으로 폼 서밋 이벤트의 기본 행동을 저지하는 기능이 없기 때문에 매개변수로 이벤트를 추가)

<!-- form.component.html -->

<form (submit)="onSubmit($event)">
  <div>
    <input type="text" [formControl]="title">
  </div>

  <div>
    <input type="text" [formControl]="content">
  </div>

  <div>
    <input type="email" [formControl]="email">
  </div>

  <button>Submit</button>
</form>

해당 템플릿 클래스에 아래와 같이 해당 function을 추가합니다.

// format.component.ts

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

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
  title = new FormControl
  content = new FormControl
  email = new FormControl

  constructor() { }

  ngOnInit(): void {
  }

  onSubmit(e:Event) {
    e.preventDefault()
    console.log('title: ', this.title)
    console.log('content: ', this.content)
    console.log('email: ', this.email)
  }
}

이후 템플릿에서 제출을 클릭하면 아래와 같이 제출된 폼을 확인 가능합니다.

폼 그룹사용하기

폼 그룹은 폼컨트롤을 그룹으로 묶어 하나의 폼으로 관리하게 해 줍니다.

// format.component.ts

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
  form = new FormGroup({
    title : new FormControl,
    content : new FormControl,
    email : new FormControl
  })

  constructor() { }

  ngOnInit(): void {
  }

  onSubmit(e:Event) {
    e.preventDefault()  
    console.log(this.form)  
  }
}

다음으로, 템플릿에서 폼을 폼그룹과 연결하고 지정된 폼컨트롤은 'formControlName' 속성을 통해 지정합니다.

<!-- form.component.html -->

<form (submit)="onSubmit($event)" [formGroup]="form">
  <div>
    <input type="text" formControlName="title">
  </div>  

  <div>
    <input type="text" formControlName="content">
  </div>

  <div>
    <input type="email" formControlName="email">
  </div>

  <button>Submit</button>
</form>

폼을 작성하고 제출하면 아래처럼 하나의 폼으로 들어오는 것을 확인가능합니다.

제약조건 설정하기

템플릿 드리븐 폼이랑 달리 제약조건은 클래스에서 'Validators'모듈을 사용하여 추가합니다. 가장 기본적인 'required' 제약조건을 추가해 보겠습니다.

 

폼 컨트롤 첫 번째 매개변수는 초기값이고 그다음에 객체의 형태로 제약조건을 추가합니다. 

// format.component.ts

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
  form = new FormGroup({
    title : new FormControl('', {validators:[Validators.required]}),
    content : new FormControl,
    email : new FormControl
  })

  constructor() { }

  ngOnInit(): void {
  }

  onSubmit(e:Event) {
    e.preventDefault()  
    console.log(this.form)      
  }
}

필수 제약조건을 추가 후 공백으로 제출하게 되면 아래와 같이 'status'가 'INVALID'라고 뜹니다.

이러한 기능을 활용하여 다양한 구현이 가능한데요 그럼 템플릿 폼을 활용하여 제약조건과 에러 메시지를 포함한 간단한 폼을 만들어 보겠습니다.  

 

클래스 파일에서 아래와 같이 제약조건을 설정합니다.

// format.component.ts

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
  form = new FormGroup({
    title : new FormControl('', {validators:[Validators.required]}),
    content : new FormControl('', {validators:[Validators.minLength(10)]}),
    email : new FormControl('', {validators:[
    Validators.pattern('^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$')
    ]})
  })

  constructor() { }

  ngOnInit(): void {
  }

  onSubmit(e:Event) {
    e.preventDefault()  
    console.log(this.form)   
  }
}

해당 컨트롤이 제약조건에 어긋날 경우 에러메시지를 표시하기 위해서는 ng-if를 통해 에러메시지를 표시할 요소를 추가합니다.

<!-- form.component.html -->

<form (submit)="onSubmit($event)" [formGroup]="form">
  <div>
    <input type="text" formControlName="title">
    <p *ngIf="form.touched && form.get('title')?.invalid">Title Required</p>
  </div>  

  <div>
    <input type="text" formControlName="content">
    <p *ngIf="form.touched && form.get('content')?.invalid">Minimum 10 characters</p>
  </div>

  <div>
    <input type="email" formControlName="email">
    <p *ngIf="form.touched && form.get('email')?.invalid">Wrong Format</p>
  </div>

  <button>Submit</button>
</form>

'touched'라는 조건을 추가하였는데 이는 에러의 여부만 조건으로 사용하면 최초 필수항목에 값이 없으므로 에러값이 참으로 반환되어 사용자가 입력을 시도하기 전에 메시지가 표시되어 사용자가 불편함을 느낄 수 있기 때문입니다.

 

설정이 완료되면 아래와 같이 조건에 어긋날 때 에러 메시지가 표시됩니다.

 

제출된 폼의 상태에 따라 처리하는 코드는 아래와 같습니다.

// format.component.ts

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
  form = new FormGroup({
    title : new FormControl('', {validators:[Validators.required]}),
    content : new FormControl('', {validators:[Validators.minLength(10)]}),
    email : new FormControl('', {validators:[Validators.pattern('^[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*$')]})
  })

  constructor() { }

  ngOnInit(): void {
  }

  onSubmit(e:Event) {
    e.preventDefault()  
    if(this.form.valid) {
      console.log(this.form)        
    }    
    this.form.reset() // clears the form
  }
}

폼 빌더

폼빌더는 리액티브 폼을 조금 더 쉽게 생성하게 해 주는 서비스로 여느 서비스처럼 컨스트럭터에 추가하여 사용합니다.

// format.component.ts

import { Component, OnInit } from '@angular/core';
import {
  FormBuilder,
  FormControl,
  FormGroup,
  Validators,
} from '@angular/forms';

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css'],
})
export class FormComponent implements OnInit {  
  formBuilder = this.fb.group({
    title: ['', Validators.required],
    content: ['', Validators.minLength(10)],
    email: [
      '',
      Validators.pattern(
        '^[a-zA-Z0-9]+(?:.[a-zA-Z0-9]+)*@[a-zA-Z0-9]+(?:.[a-zA-Z0-9]+)*$'
      ),
    ],
  });
  constructor(private fb: FormBuilder) {}

  ngOnInit(): void {}

  onSubmit(e: Event) {
    e.preventDefault();
    if (this.form.valid) {
      console.log(this.form);
    }
    this.form.reset();
  }
}

기타 기능들

Two-way binding

템플릿과 클래스에서 데이터의 상태를 공유하기 때문에 폼 컨트롤의 'value' 속성을 통해 템플릿에서 해당 값의 표시가 가능합니다.

<!-- form.component.html -->

<form (submit)="onSubmit($event)">
  <div>
    <input type="text" [formControl]="title">
  </div>

  <p>{{title.value}}</p>

  <div>
    <input type="text" [formControl]="content">
  </div>

  <div>
    <input type="email" [formControl]="email">
  </div>

  <button>Submit</button>
</form>

클래스에서 값 변경하기

아래 함수를 사용하여 클래스 파일에서 폼컨트롤의 값을 변경하는 것도 가능한데요.

.setValue()
.patchValue()

클래스에서 제출되는 폼을 처리하는 함수 하단에 값을 변경하는 코드를 추가하면

// format.component.ts

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

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit {
  title = new FormControl
  content = new FormControl
  email = new FormControl

  constructor() { }

  ngOnInit(): void {
  }

  onSubmit(e:Event) {
    e.preventDefault()
    console.log('title: ', this.title)
    console.log('content: ', this.content)
    console.log('email: ', this.email)
    this.title.setValue('override') // 값 설정
  }
}

아래처럼 클래스에서 지정된 값이 저장되는 것을 확인가능합니다.

이상으로 앵귤러 폼 - Template-driven form에 대해서 알아보았습니다.


소스코드

https://github.com/jin-co/web-mobile/tree/master/Angular/cheat-sheet/reactive-forms

 

GitHub - jin-co/web-mobile

Contribute to jin-co/web-mobile development by creating an account on GitHub.

github.com

참고

Angular - Reactive forms

 

Angular

 

angular.io

 

728x90
반응형