[Angular] ControlValueAccessor

문지은·2024년 6월 2일
0

Angular

목록 보기
4/4
post-thumbnail

ControlValueAccessor

  • Angular 폼 API의 핵심 인터페이스 중 하나로, 커스텀 폼 컨트롤을 만들 때 사용
  • 이 인터페이스를 구현하면 Angular의 NgModel, FormControl, FormGroup과 같은 폼 API와 커스텀 폼 컨트롤을 통합할 수 있다.

ControlValueAccessor는 다음과 같은 메서드들을 포함한다.

  1. writeValue(obj: any): void
    1. Angular 폼 모델의 값을 컴포넌트에 전달할 때 호출
    2. 폼 컨트롤의 값을 설정하는 데 사용
  2. registerOnChange(fn: any): void
    1. 폼 컨트롤의 값이 변경될 때 호출되는 콜백 함수를 등록
    2. 폼 컨트롤의 값이 바뀔 때 Angular에 이를 알리기 위해 사용
  3. registerOnTouched(fn: any): void
    1. 폼 컨트롤이 터치되었을 때 호출되는 콜백 함수를 등록
    2. 폼 컨트롤이 사용자의 상호작용으로 터치되었음을 Angular에 알리기 위해 사용
  4. setDisabledState?(isDisabled: boolean): void
    1. 폼 컨트롤의 활성화/비활성화 상태를 설정
    2. 선택적이며, 컨트롤이 비활성화된 상태에서도 UI를 업데이트하는 데 사용

예를 들어, 커스텀 입력 컴포넌트를 만들고 이를 Angular 폼과 함께 사용하고자 할 때 ControlValueAccessor를 구현할 수 있다.

Boilerplate

  • ControlValueAccessor interface 확장
import { ControlValueAccessor } from '@angular/forms';

export class InputTextComponent implements ControlValueAccessor {}
  • 컴포넌트 Provider에 아래 코드 삽입
@Component({
  ...
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputTextComponent),
      multi: true
    }
  ]
})
  • 아래와 같은 유틸 함수를 만들면 편리하다.
import {forwardRef} from '@angular/core';
import { NG_VALIDATORS } from '@angular/forms';

export const provideValueAccessor = (component: any) => {
  return {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => component),
    multi: true,
  };
};
@Component({
  providers: [provideValueAccessor(InputTextComponent)],
})
  • ControlValueAccessor 메서드 정의
export class InputTextComponent implements ControlValueAccessor {
	value = signal<string | null>('');
	
	// 외부에서 무언가 변경되면 이 컴포넌트에서 무엇을 해야 할지 정의
	writeValue(value: string | null): void {
    this.value.set(value);
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  onChange = (value: any) => {};
  onTouched = (value: any) => {};
  setDisabledState?(isDisabled: boolean): void {}
}

Custom Input Component 구현하기

방법 1. - RxJS를 사용한 프로퍼티 구독

  • toObservable을 사용하여 value 신호(signal)를 옵저버블로 변환한 후, constructor에서 옵저버블을 구독하여 값의 변화를 처리한다.
import {
  Component,
  forwardRef,
  signal,
  viewChild,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged } from 'rxjs';
import { toObservable } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-input-text',
  standalone: true,
  imports: [],
  templateUrl: './input-text.component.html',
  styleUrl: './input-text.component.scss',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputTextComponent),
      multi: true,
    },
  ],
})
export class InputTextComponent implements ControlValueAccessor {
  
  value = signal<string | null>('');

  constructor() {
    toObservable(this.value)
      .pipe(distinctUntilChanged())
      .subscribe((value) => this.onChange(value));
  }

  writeValue(value: string | null): void {
    this.value.set(value);
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  onChange = (value: any) => {};
  onTouched = (value: any) => {};
  setDisabledState?(isDisabled: boolean): void {}
}
<input
  [ngModel]="value()"
  (ngModelChange)="onChange($event)"
/>

방법 2 - AfterViewInit에서 DOM 이벤트 관찰

  • AfterViewInit 라이프사이클 훅에서 fromEvent를 사용하여 input DOM 이벤트를 직접 관찰한다.
  • viewChild를 사용하여 DOM 요소에 접근.
  • 템플릿의 DOM 요소에 직접적으로 접근하여 이벤트를 처리할 수 있다.
import {
  AfterViewInit,
  Component,
  ElementRef,
  forwardRef,
  signal,
  viewChild,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { distinctUntilChanged, fromEvent } from 'rxjs';

@Component({
  selector: 'app-input-text',
  standalone: true,
  imports: [],
  templateUrl: './input-text.component.html',
  styleUrl: './input-text.component.scss',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => InputTextComponent),
      multi: true,
    },
  ],
})
export class InputTextComponent implements ControlValueAccessor, AfterViewInit {
  inputRef = viewChild.required<ElementRef<HTMLInputElement>>('inputRef');
  value = signal<string | null>('');

  ngAfterViewInit(): void {
    fromEvent(this.inputRef().nativeElement, 'input')
      .pipe(distinctUntilChanged())
      .subscribe((event: any) => {
        this.value.set(event.target.value);
        this.onChange(event.target.value);
      });
  }

  writeValue(value: string | null): void {
    this.value.set(value);
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  onChange = (value: any) => {};
  onTouched = (value: any) => {};
  setDisabledState?(isDisabled: boolean): void {}
}
<input #inputRef [ngModel]="value()" (blur)="onChange(value())" />

[추가] (blur) 이벤트 바인딩

  • (blur)는 Angular의 이벤트 바인딩 문법으로, blur 이벤트가 발생할 때 지정된 메서드를 호출
  • blur 이벤트는 사용자가 입력 필드를 클릭한 후 다른 요소를 클릭하여 포커스를 잃을 때 발생
  • (blur)="onChange(value())"input 요소에 추가하면, 입력 필드에서 포커스가 벗어났을 때 onChange 메서드가 호출된다. 이때 value()를 인수로 전달하여 현재 입력 값을 전달하게 된다.

Custom Input 사용하기

  • 이제 만든 input 컴포넌트를 다음과 같이 사용할 수 있다.
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  form = new FormGroup({
    customInput: new FormControl('')
  });
}
<form [formGroup]="form">
  <app-input-text formControlName="customInput"></app-input-text>
</form>

References

Understanding angular ControlValueAccessor with an example
www.tsmean.com

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

0개의 댓글