อินพุตแบบฟอร์มที่กำหนดเองเชิงมุม 2


93

ฉันจะสร้างส่วนประกอบที่กำหนดเองซึ่งจะทำงานเหมือนกับ<input>แท็กเนทีฟได้อย่างไร ฉันต้องการให้คอนโทรลฟอร์มที่กำหนดเองของฉันสามารถรองรับ ngControl, ngForm, [(ngModel)]

ตามที่ฉันเข้าใจฉันต้องใช้อินเทอร์เฟซบางอย่างเพื่อให้การควบคุมฟอร์มของฉันเองทำงานเหมือนกับเนทีฟ

นอกจากนี้ดูเหมือนว่าคำสั่ง ngForm ผูกไว้สำหรับ<input>แท็กเท่านั้นใช่ไหม ฉันจะจัดการกับสิ่งนั้นได้อย่างไร?


ให้ฉันอธิบายว่าทำไมฉันถึงต้องการสิ่งนี้เลย ฉันต้องการรวมองค์ประกอบอินพุตหลายรายการเพื่อให้สามารถทำงานร่วมกันเป็นอินพุตเดียวได้ มีวิธีอื่นในการจัดการกับสิ่งนั้นหรือไม่? ขออีกครั้ง: ฉันต้องการให้การควบคุมนี้เหมือนกับเนทีฟ การตรวจสอบความถูกต้อง ngForm การผูกแบบสองทาง ngModel และอื่น ๆ

ps: ฉันใช้ 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>? วิธีนี้เป็น "เชิงมุม" หรือไม่?
Maksim Fomin

1
ฉันจะบอกว่าถ้าคุณต้องการใช้ฟิลด์ของคุณเองในรูปแบบ (สิ่งที่กำหนดเอง) ให้ใช้แนวทางนี้ หรือใช้องค์ประกอบ HTML ดั้งเดิม ที่กล่าวว่าหากคุณต้องการโมดูลาร์วิธีการแสดงอินพุต / textarea / เลือก (เช่นกับ Bootstrap3) คุณสามารถใช้ประโยชน์จากเนื้อหา ng ได้ ดูคำตอบนี้: stackoverflow.com/questions/34950950/…
Thierry Templier

3
ข้างต้นไม่มีรหัสและมีความคลาดเคลื่อนบางอย่างเช่น "removeLabel" แทนที่จะเป็น "removeLabel" ดูที่นี่เช่นการทำงานที่สมบูรณ์ ขอบคุณ Thierry ที่นำตัวอย่างเบื้องต้นมาให้ดู!
สีน้ำเงิน

1
พบแล้วให้นำเข้าจาก @ angular / form แทน @ angular / common และใช้งานได้ นำเข้า {NG_VALUE_ACCESSOR, ControlValueAccessor} จาก "@ angular / form";
Cagatay Civici

1
นี้การเชื่อมโยงยังควรจะเป็นประโยชน์ ..
refactor

112

ฉันไม่เข้าใจว่าทำไมทุกตัวอย่างที่ฉันพบในอินเทอร์เน็ตถึงต้องซับซ้อนขนาดนี้ เมื่ออธิบายแนวคิดใหม่ฉันคิดว่าควรมีตัวอย่างที่เรียบง่ายและใช้งานได้ดีที่สุดเสมอ ฉันกลั่นมันลงเล็กน้อย:

HTML สำหรับรูปแบบภายนอกโดยใช้ส่วนประกอบที่ใช้ ngModel:

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

ส่วนประกอบที่มีอยู่ในตัว (ไม่มีคลาส 'accessor' แยกต่างหาก - บางทีฉันอาจจะพลาด):

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 แน่นอนหนึ่งวินาที
David

6
เพื่อให้ใช้งานได้กับ@angular/formsการนำเข้าใหม่เพียงอัปเดต: import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
ulfryk

6
ไม่รองรับผู้ให้บริการ () ใน Angular2 Final ให้ MakeProvider () ส่งคืน {ให้: NG_VALUE_ACCESSOR, useExisting: forwardRef (() => type), multi: true};
DSoa

2
คุณไม่จำเป็นต้องนำเข้าCORE_DIRECTIVESและเพิ่มใน@Componentอีกต่อไปเนื่องจากมีให้โดยค่าเริ่มต้นตั้งแต่ตอนนี้ Angular2 สุดท้าย อย่างไรก็ตามตาม IDE ของฉัน "คอนสตรัคเตอร์สำหรับคลาสที่ได้รับต้องมีการเรียก 'super' ดังนั้นฉันจึงต้องเพิ่มตัว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
แม้ว่าลิงก์นี้อาจตอบคำถามได้ แต่ควรรวมส่วนสำคัญของคำตอบไว้ที่นี่และระบุลิงก์เพื่อการอ้างอิง คำตอบแบบลิงก์เท่านั้นอาจไม่ถูกต้องหากหน้าที่เชื่อมโยงเปลี่ยนไป
Maximilian 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

คุณยังแก้ปัญหานี้ได้ด้วยคำสั่ง @ViewChild สิ่งนี้ช่วยให้ผู้ปกครองสามารถเข้าถึงตัวแปรสมาชิกทั้งหมดและฟังก์ชันของลูกที่ถูกฉีดเข้าไป

ดู: วิธีเข้าถึงช่องป้อนข้อมูลของส่วนประกอบฟอร์มที่ฉีดเข้าไป


ฟังดูเหมือนแฮ็ค :(
realappie

-1

เหตุใดจึงต้องสร้างตัวเข้าถึงค่าใหม่เมื่อคุณสามารถใช้ ngModel ภายในได้ เมื่อใดก็ตามที่คุณกำลังสร้างคอมโพเนนต์แบบกำหนดเองซึ่งมีอินพุต [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>

แม้ว่าสิ่งนี้จะดูดีเนื่องจากคุณกำลังโทรหาซุปเปอร์ แต่ก็มีคำว่า "ขยาย" ที่ขาดหายไป
Dave Nottage

1
ใช่ฉันไม่ได้คัดลอกรหัสทั้งหมดของฉันที่นี่และลืมลบ super ()
Nishant

9
นอกจากนี้ outerNgModel มาจากไหน? คำตอบนี้จะตอบสนองได้ดีกว่าด้วยรหัสที่สมบูรณ์
Dave Nottage

อ้างอิงจากangular.io/docs/ts/latest/api/core/index/… innerNgModelถูกกำหนดไว้ในngAfterViewInit
Matteo Suppo

2
นี่ใช้ไม่ได้เลย innerNgModel จะไม่ถูกเตรียมใช้งานโดยจะไม่ประกาศ outerNgModel และจะไม่ใช้ ngModel ที่ส่งผ่านไปยัง constructor
user2350838

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.