Angular 2 사용자 지정 양식 입력


93

네이티브 <input>태그 처럼 작동하는 사용자 지정 구성 요소를 어떻게 만들 수 있습니까? 사용자 지정 양식 컨트롤이 ngControl, ngForm, [(ngModel)]을 지원할 수 있도록 만들고 싶습니다.

알다시피, 고유 한 폼 컨트롤이 네이티브 컨트롤처럼 작동하도록하려면 일부 인터페이스를 구현해야합니다.

또한 ngForm 지시문 <input>이 태그 에만 바인딩되는 것처럼 보이지만 이것이 맞습니까? 어떻게 처리 할 수 ​​있습니까?


왜 이것이 필요한지 설명하겠습니다. 여러 입력 요소를 래핑하여 단일 입력으로 함께 작동 할 수 있도록하고 싶습니다. 그것을 처리하는 다른 방법이 있습니까? 한 번 더 :이 컨트롤을 네이티브 컨트롤처럼 만들고 싶습니다. 유효성 검사, ngForm, ngModel 양방향 바인딩 및 기타.

추신 : Typescript를 사용합니다.


1
대부분의 답변은 현재 Angular 버전과 관련하여 구식입니다. 한 번 봐 가지고 stackoverflow.com/a/41353306/2176962
hgoebl

답변:


85

실제로 구현해야 할 두 가지가 있습니다.

  • 양식 구성 요소의 논리를 제공하는 구성 요소입니다. ngModel자체적 으로 제공되므로 입력이 필요하지 않습니다.
  • ControlValueAccessor이 구성 요소와 ngModel/ 사이의 브리지를 구현할 사용자 지정ngControl

샘플을 봅시다. 회사의 태그 목록을 관리하는 구성 요소를 구현하고 싶습니다. 구성 요소는 태그를 추가 및 제거 할 수 있습니다. 태그 목록이 비어 있지 않은지 확인하기 위해 유효성 검사를 추가하고 싶습니다. 아래에 설명 된대로 구성 요소에서 정의합니다.

(...)
import {TagsComponent} from './app.tags.ngform';
import {TagsValueAccessor} from './app.tags.ngform.accessor';

function notEmpty(control) {
  if(control.value == null || control.value.length===0) {
    return {
      notEmpty: true
    }
  }

  return null;
}

@Component({
  selector: 'company-details',
  directives: [ FormFieldComponent, TagsComponent, TagsValueAccessor ],
  template: `
    <form [ngFormModel]="companyForm">
      Name: <input [(ngModel)]="company.name"
         [ngFormControl]="companyForm.controls.name"/>
      Tags: <tags [(ngModel)]="company.tags" 
         [ngFormControl]="companyForm.controls.tags"></tags>
    </form>
  `
})
export class DetailsComponent implements OnInit {
  constructor(_builder:FormBuilder) {
    this.company = new Company('companyid',
            'some name', [ 'tag1', 'tag2' ]);
    this.companyForm = _builder.group({
       name: ['', Validators.required],
       tags: ['', notEmpty]
    });
  }
}

TagsComponent구성 요소를 추가하고 요소 제거하는 로직 정의 tags목록을.

@Component({
  selector: 'tags',
  template: `
    <div *ngIf="tags">
      <span *ngFor="#tag of tags" style="font-size:14px"
         class="label label-default" (click)="removeTag(tag)">
        {{label}} <span class="glyphicon glyphicon-remove"
                        aria-  hidden="true"></span>
      </span>
      <span>&nbsp;|&nbsp;</span>
      <span style="display:inline-block;">
        <input [(ngModel)]="tagToAdd"
           style="width: 50px; font-size: 14px;" class="custom"/>
        <em class="glyphicon glyphicon-ok" aria-hidden="true" 
            (click)="addTag(tagToAdd)"></em>
      </span>
    </div>
  `
})
export class TagsComponent {
  @Output()
  tagsChange: EventEmitter;

  constructor() {
    this.tagsChange = new EventEmitter();
  }

  setValue(value) {
    this.tags = value;
  }

  removeLabel(tag:string) {
    var index = this.tags.indexOf(tag, 0);
    if (index != undefined) {
      this.tags.splice(index, 1);
      this.tagsChange.emit(this.tags);
    }
  }

  addLabel(label:string) {
    this.tags.push(this.tagToAdd);
    this.tagsChange.emit(this.tags);
    this.tagToAdd = '';
  }
}

보시다시피이 구성 요소에는 입력이 없지만 setValue하나입니다 (여기서 이름은 중요하지 않음). 나중에 ngModel에서 구성 요소로 값을 제공하는 데 사용합니다 . 이 구성 요소는 구성 요소 (태그 목록)의 상태가 업데이트 될 때 알리는 이벤트를 정의합니다.

이제이 구성 요소와 ngModel/ 간의 링크를 구현해 보겠습니다 ngControl. 이는 ControlValueAccessor인터페이스 를 구현하는 지시문에 해당합니다 . NG_VALUE_ACCESSOR토큰 에 대해이 값 접근 자에 대해 공급자를 정의해야합니다 ( forwardRef지시문이 이후에 정의되므로 사용하는 것을 잊지 마십시오 ).

지시문은 tagsChange호스트 의 이벤트에 이벤트 리스너를 첨부합니다 (예 : 지시문이 첨부 된 구성 요소, 즉 TagsComponent). onChange이벤트가 발생할 때 메서드가 호출됩니다. 이 방법은 Angular2에서 등록한 방법에 해당합니다. 이렇게하면 관련 양식 컨트롤에 따라 변경 및 업데이트를 인식하게됩니다.

writeValue바인딩 된 값 ngForm이 업데이트 되면이 호출 됩니다. 첨부 된 컴포넌트 (예 : TagsComponent)를 삽입 한 후이 값을 전달하기 위해 호출 할 수 있습니다 (이전 setValue메서드 참조 ).

CUSTOM_VALUE_ACCESSOR지시문의 바인딩에 를 제공하는 것을 잊지 마십시오 .

다음은 사용자 정의의 전체 코드입니다 ControlValueAccessor.

import {TagsComponent} from './app.tags.ngform';

const CUSTOM_VALUE_ACCESSOR = CONST_EXPR(new Provider(
  NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => TagsValueAccessor), multi: true}));

@Directive({
  selector: 'tags',
  host: {'(tagsChange)': 'onChange($event)'},
  providers: [CUSTOM_VALUE_ACCESSOR]
})
export class TagsValueAccessor implements ControlValueAccessor {
  onChange = (_) => {};
  onTouched = () => {};

  constructor(private host: TagsComponent) { }

  writeValue(value: any): void {
    this.host.setValue(value);
  }

  registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
  registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}

이렇게 tags하면 회사 전체를 제거 valid하면 companyForm.controls.tags컨트롤 의 속성 이 false자동으로 나타납니다.

자세한 내용은이 문서 ( "NgModel 호환 구성 요소"섹션)를 참조하십시오.


감사! 당신은 대단합니다! 어떻게 생각하세요-이 방법이 실제로 괜찮습니까? 내 말은 : 입력 요소를 사용하지 말고 다음과 같은 컨트롤을 만들지 마십시오. <textfield>, <dropdown>? 이 "각진"방식입니까?
막심 Fomin

1
양식 (사용자 정의)으로 자신의 필드를 구현하려면이 접근 방식을 사용하십시오. 그렇지 않으면 기본 HTML 요소를 사용하십시오. 즉, 입력 / 텍스트 영역 / 선택을 표시하는 방법을 모듈화하려는 경우 (예 : Bootstrap3 사용) ng-content를 활용할 수 있습니다. 이 대답을 참조하십시오 stackoverflow.com/questions/34950950/...
티에리 Templier

3
위의 코드는 누락되었으며 'removeLabel'대신 'removeLabel'과 같은 일부 불일치가 있습니다. 완전한 작동 예는 여기 를 참조 하십시오 . 초기 예제를 넣어 주신 Thierry에게 감사드립니다!
Blue

1
그것을 찾았고 @ angular / common 대신 @ angular / forms에서 가져 오면 작동합니다. '@ angular / forms'에서 {NG_VALUE_ACCESSOR, ControlValueAccessor} 가져 오기;
Cagatay Civici

1
링크도 도움 될 것입니다 ..
리팩터링

110

인터넷에서 내가 찾은 모든 예가 왜 그렇게 복잡해야하는지 이해가 안 돼요. 새로운 개념을 설명 할 때 가능한 가장 간단하고 실제적인 예제를 사용하는 것이 항상 최선이라고 생각합니다. 나는 그것을 약간 추출했습니다.

ngModel을 구현하는 구성 요소를 사용하는 외부 양식 용 HTML :

EmailExternal=<input [(ngModel)]="email">
<inputfield [(ngModel)]="email"></inputfield>

자체 포함 된 구성 요소 (별도의 '접근 자'클래스 없음-요점이 누락되었을 수 있음) :

import {Component, Provider, forwardRef, Input} from "@angular/core";
import {ControlValueAccessor, NG_VALUE_ACCESSOR, CORE_DIRECTIVES} from "@angular/common";

const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR = new Provider(
  NG_VALUE_ACCESSOR, {
    useExisting: forwardRef(() => InputField),
    multi: true
  });

@Component({
  selector : 'inputfield',
  template: `<input [(ngModel)]="value">`,
  directives: [CORE_DIRECTIVES],
  providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class InputField implements ControlValueAccessor {
  private _value: any = '';
  get value(): any { return this._value; };

  set value(v: any) {
    if (v !== this._value) {
      this._value = v;
      this.onChange(v);
    }
  }

    writeValue(value: any) {
      this._value = value;
      this.onChange(value);
    }

    onChange = (_) => {};
    onTouched = () => {};
    registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
    registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}

사실, 저는이 모든 것을 추상 클래스로 추상화했습니다. 이제 ngModel을 사용하는 데 필요한 모든 구성 요소로 확장합니다. 나에게 이것은 내가 없이도 할 수있는 엄청난 양의 오버 헤드와 상용구 코드입니다.

편집 : 여기 있습니다 :

import { forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export abstract class AbstractValueAccessor implements ControlValueAccessor {
    _value: any = '';
    get value(): any { return this._value; };
    set value(v: any) {
      if (v !== this._value) {
        this._value = v;
        this.onChange(v);
      }
    }

    writeValue(value: any) {
      this._value = value;
      // warning: comment below if only want to emit on user intervention
      this.onChange(value);
    }

    onChange = (_) => {};
    onTouched = () => {};
    registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
    registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}

export function MakeProvider(type : any){
  return {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => type),
    multi: true
  };
}

다음은이를 사용하는 구성 요소입니다. (TS) :

import {Component, Input} from "@angular/core";
import {CORE_DIRECTIVES} from "@angular/common";
import {AbstractValueAccessor, MakeProvider} from "../abstractValueAcessor";

@Component({
  selector : 'inputfield',
  template: require('./genericinput.component.ng2.html'),
  directives: [CORE_DIRECTIVES],
  providers: [MakeProvider(InputField)]
})
export class InputField extends AbstractValueAccessor {
  @Input('displaytext') displaytext: string;
  @Input('placeholder') placeholder: string;
}

HTML :

<div class="form-group">
  <label class="control-label" >{{displaytext}}</label>
  <input [(ngModel)]="value" type="text" placeholder="{{placeholder}}" class="form-control input-md">
</div>

1
흥미롭게도, 받아 들여진 대답은 RC2 이후로 작동을 멈춘 것 같습니다. 나는이 접근 방식을 시도했지만 그 이유는 확실하지 않습니다.
3urdoch

1
@ 3urdoch 물론, 1 초
David

6
@angular/forms업데이트 가져 오기 와 함께 작동하도록하려면 : import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk

6
Provider ()는 Angular2 Final에서 지원되지 않습니다. 대신 MakeProvider ()가 {제공 : NG_VALUE_ACCESSOR, useExisting : forwardRef (() => type), multi : true};
DSoa

2
Angular2 최종 이후 기본적으로 제공되므로 더 이상 가져 와서 CORE_DIRECTIVES추가 할 필요가 없습니다 @Component. 그러나 내 IDE에 따르면 "파생 클래스의 생성자는 '수퍼'호출을 포함해야합니다."라고했기 때문에 super();구성 요소의 생성자에 추가 해야했습니다.
Joseph Webber

16

이 링크에는 RC5 버전에 대한 예제가 있습니다. http://almerosteyn.com/2016/04/linkup-custom-control-to-ngcontrol-ngmodel

import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';

const noop = () => {
};

export const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomInputComponent),
    multi: true
};

@Component({
    selector: 'custom-input',
    template: `<div class="form-group">
                    <label>
                        <ng-content></ng-content>
                        <input [(ngModel)]="value"
                                class="form-control"
                                (blur)="onBlur()" >
                    </label>
                </div>`,
    providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class CustomInputComponent implements ControlValueAccessor {

    //The internal data model
    private innerValue: any = '';

    //Placeholders for the callbacks which are later providesd
    //by the Control Value Accessor
    private onTouchedCallback: () => void = noop;
    private onChangeCallback: (_: any) => void = noop;

    //get accessor
    get value(): any {
        return this.innerValue;
    };

    //set accessor including call the onchange callback
    set value(v: any) {
        if (v !== this.innerValue) {
            this.innerValue = v;
            this.onChangeCallback(v);
        }
    }

    //Set touched on blur
    onBlur() {
        this.onTouchedCallback();
    }

    //From ControlValueAccessor interface
    writeValue(value: any) {
        if (value !== this.innerValue) {
            this.innerValue = value;
        }
    }

    //From ControlValueAccessor interface
    registerOnChange(fn: any) {
        this.onChangeCallback = fn;
    }

    //From ControlValueAccessor interface
    registerOnTouched(fn: any) {
        this.onTouchedCallback = fn;
    }

}

그런 다음이 사용자 지정 컨트롤을 다음과 같이 사용할 수 있습니다.

<form>
  <custom-input name="someValue"
                [(ngModel)]="dataModel">
    Enter data:
  </custom-input>
</form>

4
이 링크가 질문에 답할 수 있지만 여기에 답변의 필수 부분을 포함하고 참조 용 링크를 제공하는 것이 좋습니다. 링크 된 페이지가 변경되면 링크 전용 답변이 무효화 될 수 있습니다.
막시밀리안 AST

5

Thierry의 예가 도움이됩니다. 다음은 TagsValueAccessor를 실행하는 데 필요한 가져 오기입니다.

import {Directive, Provider} from 'angular2/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR } from 'angular2/common';
import {CONST_EXPR} from 'angular2/src/facade/lang';
import {forwardRef} from 'angular2/src/core/di';

1

이 경우에 대한 일부 상용구를 줄이는 데 도움이되는 라이브러리를 작성했습니다 s-ng-utils. 다른 답변 중 일부는 단일 양식 컨트롤 을 래핑하는 예를 제공 합니다. 다음을 사용하여 s-ng-utils매우 간단하게 사용할 수 있습니다 WrappedFormControlSuperclass.

@Component({
    template: `
      <!-- any fancy wrapping you want in the template -->
      <input [formControl]="formControl">
    `,
    providers: [provideValueAccessor(StringComponent)],
})
class StringComponent extends WrappedFormControlSuperclass<string> {
  // This looks unnecessary, but is required for Angular to provide `Injector`
  constructor(injector: Injector) {
    super(injector);
  }
}

게시물에서 여러 양식 컨트롤을 단일 구성 요소로 래핑하고 싶다고 언급했습니다. 다음은 FormControlSuperclass.

import { Component, Injector } from "@angular/core";
import { FormControlSuperclass, provideValueAccessor } from "s-ng-utils";

interface Location {
  city: string;
  country: string;
}

@Component({
  selector: "app-location",
  template: `
    City:
    <input
      [ngModel]="location.city"
      (ngModelChange)="modifyLocation('city', $event)"
    />
    Country:
    <input
      [ngModel]="location.country"
      (ngModelChange)="modifyLocation('country', $event)"
    />
  `,
  providers: [provideValueAccessor(LocationComponent)],
})
export class LocationComponent extends FormControlSuperclass<Location> {
  location!: Location;

  // This looks unnecessary, but is required for Angular to provide `Injector`
  constructor(injector: Injector) {
    super(injector);
  }

  handleIncomingValue(value: Location) {
    this.location = value;
  }

  modifyLocation<K extends keyof Location>(field: K, value: Location[K]) {
    this.location = { ...this.location, [field]: value };
    this.emitOutgoingValue(this.location);
  }
}

그런 다음 사용할 수 있습니다 <app-location>[(ngModel)], [formControl]사용자 정의 유효성 검사기, - 모든 당신이 밖으로 상자의 컨트롤과 각도 지원을 할 수 있습니다.



-1

내부 ngModel을 사용할 수 있는데 새 값 접근자를 만드는 이유. input [ngModel]이있는 사용자 지정 구성 요소를 만들 때마다 이미 ControlValueAccessor를 인스턴스화하고 있습니다. 이것이 우리가 필요로하는 접근 자입니다.

주형:

<div class="form-group" [ngClass]="{'has-error' : hasError}">
    <div><label>{{label}}</label></div>
    <input type="text" [placeholder]="placeholder" ngModel [ngClass]="{invalid: (invalid | async)}" [id]="identifier"        name="{{name}}-input" />    
</div>

구성 요소:

export class MyInputComponent {
    @ViewChild(NgModel) innerNgModel: NgModel;

    constructor(ngModel: NgModel) {
        //First set the valueAccessor of the outerNgModel
        this.outerNgModel.valueAccessor = this.innerNgModel.valueAccessor;

        //Set the innerNgModel to the outerNgModel
        //This will copy all properties like validators, change-events etc.
        this.innerNgModel = this.outerNgModel;
    }
}

로 사용:

<my-input class="col-sm-6" label="First Name" name="firstname" 
    [(ngModel)]="user.name" required 
    minlength="5" maxlength="20"></my-input>

이 모습을 약속하는 동안 당신은 슈퍼 호출하고 있기 때문에, A는 실종 "확장"있다
데이브 Nottage

1
예, 여기에 전체 코드를 복사하지 않았고 super ()를 제거하는 것을 잊었습니다.
Nishant

9
또한 outerNgModel은 어디에서 왔습니까? 이 답변은 완전한 코드로 더 잘 제공 될 것입니다
Dave Nottage

에 따르면 angular.io/docs/ts/latest/api/core/index/...하기 innerNgModel 에 정의되어 있습니다ngAfterViewInit
마테오 Suppo

2
이것은 전혀 작동하지 않습니다. innerNgModel은 초기화되지 않고 outerNgModel은 선언되지 않으며 생성자에 전달 된 ngModel은 사용되지 않습니다.
user2350838

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.