Angular에서 재사용 가능한 다중 검사 필드 만들기

표지 사진은 Alev Takil 에서 Unsplash
일반적으로 사용자는 여러 가지 사용 가능한 양식에서 여러 가지 옵션을 선택할 수 있습니다.

Angular에서 이 문제를 처리하는 가장 일반적인 방법은 한 그룹<input type="checkbox">과 한 그룹FormArray을 사용하는 것이다.그러나 응용 프로그램의 몇 가지 형식이 같은 기능을 필요로 할 때, 우리는 논리와 표기를 포함한 대량의 코드를 반복하기 시작할 가능성이 높다.
본 논문에서는 다음과 같은 특성을 가진 구성 요소를 구축하여 이 문제를 해결하고자 합니다.
  • 다중 검사: 여러 옵션을 동시에 선택할 수 있음
  • 재사용 가능: 옵션은 서로 다른 시각화로 구현할 수 있으며 논리를 다시 작성할 필요가 없음
  • 사용자 정의 필드: 각도 모양에 직접 사용되는 사용자 정의 모양 필드
  • 완료되면 이 구성 요소를 사용하여 다음 비헤이비어의 양식을 구성할 수 있습니다.

    카탈로그

  • 설계
  • 1단계: SimpleCheckOption 구성 요소 지원
  • 단계 2: 모든 유형의 옵션 구성 요소 지원
  • 단계 3: 각도 형태와의 통합
  • 마지막 단어
  • 데모
  • 향상
  • 코드 라이브러리 링크
  • 설계


    우리의 구성 요소는 두 개의 요소로 구성된다.
  • 필드 구성 요소로 선택한 옵션을 추적하고 각형과의 통합을 제공합니다.
  • 옵션 구성 요소는 단일 검사 옵션을 표시하고 가시화를 제공합니다.이 생각은 우리가 몇 가지 이런 것을 가지고 있다는 것이다.
  • 1단계: SimpleCheckOption 구성 요소 지원


    우선, 우리는 simple-check-option 을 통해 multi-check-field 만 지원하지만, 이 필드는 모든 옵션 구성 요소와 함께 사용하기를 희망한다는 것을 기억하십시오.
    즉, 다음과 같이 컨텐츠 투영을 사용하여 multi-check-field 에 필요한 옵션을 제공합니다.
    <multi-check-field>
        <simple-check-option *ngFor="let option of options" [value]="option"
          [label]="option.label">
        </single-check-option>
    </multi-check-field>
    
    multi-check-field 닫힌 태그 내에서 컨텐트 투영 옵션을 전달하는 방법에 유의하십시오.
    이제 simple-check-option 의 실현을 살펴보겠습니다.
    @Component({
      selector: 'simple-check-option',
      template: `
        <label>
          <input type="checkbox" [formControl]="control">
          {{ label }}
        </label>
      `
    })
    export class SimpleCheckOptionComponent {
    
      @Input() value: any;
      @Input() label: string;
    
      public control = new FormControl(false);
    
      get valueChanges$(): Observable<boolean> {
        return this.control.valueChanges;
      }
    
    }
    
    이 구성 요소는 라벨이 있는 표준<input type="checkbox">이 있습니다.또한 확인란 값을 조작하기 위해 FormControl 를 설명했고, 외부에서 유형 보안이 있는 구성 요소와 상호작용할 수 있도록 valueChanges$ 접근기를 제공했다.multi-check-field 구성 요소는 ContentChildren 데코더를 사용하여 투영 옵션을 조회합니다.
    @Component({
      selector: 'multi-check-field',
      template: `<ng-content></ng-content>`
    })
    export class MultiCheckFieldComponent implements AfterContentInit {
    
      @ContentChildren(SimpleCheckOptionComponent)
      options!: QueryList<SimpleCheckOptionComponent>;
    
      ngAfterContentInit(): void {
        // Content query ready
      }
    
    }
    
    주의해야 할 것은 내용 조회는 우선 AfterContentInit 생명주기에 사용할 준비가 되어 있지만 이전은 아니다.또한 구성 요소 템플릿의 <ng-content> 태그를 사용하여 제공된 내용 (옵션) 을 표시하는 방법을 참조하십시오.
    이제 선택한 옵션을 추적하는 방법을 살펴보겠습니다.
    private subscriptions = new Subscription();
    private selectedValues: any[] = [];
    
    ngAfterContentInit(): void {
      this.options.forEach(option => {
        this.subscriptions.add(
          option.valueChanges$.subscribe(
            (optionChecked) => {
              if (optionChecked) {
                this.add(option.value);
              } else {
                this.remove(option.value);
              }
            }
          )
        );
      });
    }
    
    private add(value: any): void {
      this.selectedValues.push(value);
    }
    
    private remove(value: any): void {
      const idx = this.selectedValues.findIndex(v => v === value);
      if (idx >= 0) {
        this.selectedValues.splice(idx, 1);
      }
    }
    
    옵션을 선택/선택 취소할 때 옵션의 valueChanges$ 방문자 구독 이벤트를 사용합니다.optionChecked 부울 값에 따라 이 옵션을 selectedValues 그룹에서 계속 추가하거나 삭제합니다.

    이때 우리의 multi-check-fieldsimple-check-option 는 완전히 통합되었다.그러나 Angular 컨텐트 투영을 사용하여 검사 옵션으로 모든 구성 요소를 지원해야 합니다.어떻게 하는지 봅시다.

    2단계: 모든 유형의 옵션 구성 요소 지원

    simple-check-option와 매우 다르지만 같은 기능을 가진 새로운 옵션 구성 요소를 만듭니다.우리는 그것을 user-check-option 라고 명명했다. 그것은 대표할 것이다.응, 한 사용자😅.

    구성 요소 논리는 기본적으로 simple-check-option 와 같지만 템플릿에 큰 차이가 있습니다.
    @Component({
      selector: 'user-check-option',
      template: `
        <label>
          <input type="checkbox" [formControl]="control">
          <div class="card">
            <div class="avatar">
              <img src="assets/images/{{ value.avatar }}">
              <div class="span"></div>
            </div>
            <h1>{{ value.name }}</h1>
            <h2>{{ value.location }}</h2>
          </div>
        </label>
      `
    })
    export class UserCheckOptionComponent {
    
      @Input() value: any;
    
      public control = new FormControl(false);
    
      get valueChanges$(): Observable<boolean> {
        return this.control.valueChanges;
      }
    
    }
    
    필드 구성 요소를 통해 새로운 user-check-option 쿼리를 지원하기 위해서, 우리는 더 이상 ContentChildren 쿼리를 수정해야 합니다.이것은 우리의 현재 조회입니다.
    @ContentChildren(SimpleCheckOptionComponent)
    options!: QueryList<SimpleCheckOptionComponent>;
    
    불행하게도 우리는 두 가지 다른 구성 요소에 대해 SimpleCheckOption 를 사용할 수 없지만, Angular's Dependency Injection (DI) 의 기능을 사용하여 이런 상황을 극복할 수 있다.

    의존 주입 구조👨‍🚒 👩‍🚒 🚒


    이 문제의 가능한 해결 방안은 alias providers 를 사용하여 우리의 옵션 구성 요소가 사용할 수 있도록 공공 DI token 을 만드는 것이다.
    abstract class MultiCheckOption { }                        // (1)
    
    @Component({
      selector: 'simple-check-option',
      providers: [
        {                                                      // (2)
          provide: MultiCheckOption,
          useExisting: SimpleCheckOptionComponent,
        }
      ]
    })
    export class SimpleCheckOptionComponent { ... }
    
    @Component({
      selector: 'user-check-option',
      providers: [
        {                                                      // (3)
          provide: MultiCheckOption,
          useExisting: UserCheckOptionComponent
        }
      ]
    })
    export class UserCheckOptionComponent { ... }
    
  • 우선 옵션 구성 요소가 DI 영패로 사용되는 ContentChildren 클래스를 만듭니다.
  • 우리는 providers 메타데이터 키를 사용하여 MultiCheckOption 구성 요소 수준에서 분유기를 설정합니다.이 설정에서 Angular의 DI가 구성 요소의 주입기에 요청SimpleCheckOptionComponent의 실례를 요청하면 구성 요소 자체의 기존 실례를 전달합니다.
  • 우리도 MultiCheckOption 이렇게 한다.
  • UserCheckOptionComponent 현재 검색어는 다음과 같이 다시 쓸 수 있습니다.
    @ContentChildren(MultiCheckOption)
    options!: QueryList<MultiCheckOption>;
    
    하지만 우리는 아직 끝나지 않았어...이 때, 옵션 구성 요소의 구성원과 방법에 접근할 수 없습니다. 왜냐하면 ContentChildren 클래스가 비어 있기 때문입니다.우리는 클래스 자체를 사용하여 옵션에서 흔히 볼 수 있는 내용을 저장하고 필요한 내용을 공개함으로써 이 문제를 해결할 수 있다.그 후에 우리는 ES6 클래스 계승을 이용하여 MultiCheckOption 에서 option 구성 요소를 확장했다.
    export abstract class MultiCheckOption {
      abstract value: any;
      public control = new FormControl(false);
      get valueChanges$(): Observable<boolean> {
        return this.control.valueChanges;
      }
    }
    
    @Component(...)
    export class SimpleCheckOptionComponent extends MultiCheckOption {
      @Input() value: any;
      @Input() label: string;
    }
    
    @Component(...)
    export class UserCheckOptionComponent extends MultiCheckOption {
      @Input() value: any;
    }
    
    그렇듯이, MultiCheckOption 논리를 실현하는 모든 구성 요소를 지원합니다.

    3단계: 각도 형태와의 통합


    이 단계에서 multi-check-field 각도와 결합하여 사용할 수 있습니다
    <multi-check-field formControlName="subjects">
        ...
    </multi-check-field>
    
    하지만 다음과 같은 오류가 발생합니다.

    No value accessor for form control with name: 'subjects'


    원인은 MultiCheckOption 원생 형식 원소(예를 들어 multi-check-fieldAngularFormsModule만 어떻게 처리하는지 알기 때문이다.우리의 맞춤형<input>이 각도 형식을 처리할 수 있도록 프레임워크와 어떻게 통신하는지 알려야 한다.(Angular의 사용자 정의 폼 필드를 처음 들어보셨다면 post 를 확인해 보시기 바랍니다.

    1.select 공급자


    우리는 먼저 전 세계multi-check-field 공급업체에 구성 요소를 등록합니다.
    import { Component, forwardRef } from '@angular/core';
    import { NG_VALUE_ACCESSOR } from '@angular/forms';
    
    @Component({
      selector: 'multi-check-field',
      providers: [
        {
          provide: NG_VALUE_ACCESSOR,
          useExisting: forwardRef(() => MultiCheckFieldComponent),
          multi: true
        }
      ]
    })
    export class MultiCheckFieldComponent { ... }
    

    이.NG\u VALUE\u 액세스기 인터페이스


    그 밖에 우리는 NG_VALUE_ACCESSOR 인터페이스를 실현해야 한다. 이 인터페이스는 보기 (우리의 구성 요소) 와 모델 (표 컨트롤) 의 동기화를 유지하기 위해 다음과 같은 방법 집합을 정의했다.
    writeValue(obj: any): void;
    registerOnChange(fn: any): void;
    registerOnTouched(fn: any): void;
    setDisabledState?(isDisabled: boolean): void;
    

    writeValue(객체: 임의)


    이 함수는 모델에서 보기까지의 필드 값을 설정하기 위해 프레임에서 실행됩니다.예를 들어, 다음 작업을 수행할 때
    multiCheckControl = new FormControl(TEST_INITIAL_VALUE);
    multiCheckControl.setValue(TEST_VALUE);
    multiCheckControl.patchValue(TEST_VALUE);
    
    우리의 예에서, ControlValueAccesor 매개 변수는 선택한 옵션 값을 포함하는 그룹이어야 한다.가독성을 높이기 위해서 우리는 그것을 ControlValueAccesor 라고 명명하는 것이 가장 좋다.
    writeValue(values: any[]): void {
        this.selectedValues = [];
        values = values || [];
        values.forEach(selectedValue => {
          const selectedOption = this.options.find(v => v.value === selectedValue);
          selectedOption.control.setValue(true);
        });
    }
    
    obj 수조의 모든 항목이 상응하는 values 에 비추고 검사 값이 보기에 반영된다. (우리의 예시에서 이것은 다른 컨트롤러를 통해 이루어진다.)values를 호출할 때마다 option에서 설명한 상응하는 selectedOption.control.setValue() 구독을 호출하고 옵션의 값을 로컬valueChanges$ 그룹에 추가합니다.
    우리 그것의 일을 좀 봅시다
    @Component({
      selector: 'app-root',
      template: `
        <multi-check-field [formControl]="multiCheckControl">
          <simple-check-option *ngFor="let subject of subjects"
            [value]="subject" [label]="subject.label">
          </simple-check-option>
        </multi-check-field>
        <button (click)="setTestValue()">Set Test Value</button>
        Control value: <pre>{{ multiCheckControl.value | json }}</pre>
      `,
    })
    export class AppComponent {
    
      public subjects = [
        { code: '001', label: 'Math' },
        { code: '002', label: 'Science' },
        { code: '003', label: 'History' },
      ];
    
      public multiCheckControl = new FormControl();
    
      setTestValue() {
        const testValue = [this.subjects[0], this.subjects[1]];
        this.multiCheckControl.setValue(testValue);
      }
    
    }
    

    레지스터 변경 (fn: 모두)


    UI에서 필드 값이 변경될 때 호출해야 하는 함수를 등록합니다.제공된 함수를 호출하면 보기의 값이 모델에 업데이트됩니다.
    우리의 예에서 옵션을 선택하거나 취소할 때마다 모델 값을 업데이트해야 합니다.
    export class MultiCheckFieldComponent implements ControlValueAccessor {
    
      _onChange: (_: any) => void;
    
      registerOnChange(fn: any): void {
        this._onChange = fn;
      }
    
      private add(value: any): void {
        this.selectedValues.push(value);
        this._onChange(this.selectedValues);
      }
    
      private remove(value: any): void {
        const idx = this.selectedValues.findIndex(v => v === value);
        if (idx >= 0) {
          this.selectedValues.splice(idx, 1);
          this._onChange(this.selectedValues);
        }
      }
      ...
    }
    

    레지스터 터치 (fn: 모두)


    앞의 방법과 마찬가지로, 컨트롤러가 검증을 촉발할 수 있도록 터치 필드에 호출할 함수를 등록해야 합니다.
    우리는 이런 방법의 실현을 본 강좌의 범위에서 제외할 것이다.

    비활성화 상태를 설정합니까?(isDisabled: 부울)


    마지막으로 가장 중요하지 않은 것은 ngAfterContentInit 방법이다.필드를 프로그래밍 방식으로 사용하거나 사용하지 않을 때 이 함수를 호출합니다.예를 들어, 다음 작업을 수행할 때
    multiCheckControl = new FormControl({
      value: TEST_INITIAL_VALUE,
      disabled: true
    });
    multiCheckControl.disabled();
    multiCheckControl.enabled();
    
    이 방법도 본 강좌의 범위 내에 있지 않을 것이다.

    마지막 한마디


    우리는 다중 검사 기능을 제공하는 구성 요소를 만들려고 노력했지만 다음과 같은 기능도 제공했다.
  • 모든 논리가 구성 요소에 봉인되어 있기 때문에 모든 형식을 다시 작성할 필요가 없습니다.
  • 간단하다. 왜냐하면 사용법이 매우 간단하기 때문이다.로컬 selectedValues 과 유사하며 내부에 setDisabledState 개의 라벨이 있습니다.
  • 옵션은 필요에 따라 스타일을 설계할 수 있기 때문에 재사용이 가능합니다.
  • 호환성, 각도 형식의 통합을 지원합니다.
  • 프레젠테이션 시간🌋


    한층 더 개선하다


    아직 많은 개선 공간이 있다.코드를 작성해야 할 경우에 대비해 나는 여기에 몇 가지 생각을 열거했다.스토리지 라이브러리에 솔루션을 통합하기 위해 PR을 원활하게 실행합니다.
  • 초기화 시 전달되는 값 지원(ngAfter ContentInit 이전에 writeValue)✅
  • 예측 옵션의 변경을 지원합니다(DOM에서 이 옵션을 추가하거나 삭제할 때)
  • registerOnTouched 및 setDisableState 지원 방법
  • minValuesLength 및 maxValuesLength 검증기 작성
  • 구성 요소 전달 대신 템플릿을 옵션으로 지원
  • 코드 저장소 링크

  • 전체 소스 코드를 찾을 수 있음here
  • this branch에서 위에서 건의한 개선 사항을 찾을 수 있습니다
  • 좋은 웹페이지 즐겨찾기