Angular 角度8:使用formControlName和ControlValueAccessor选择列表下拉列表

Angular 角度8:使用formControlName和ControlValueAccessor选择列表下拉列表,angular,typescript,angular-material,angular8,angular-reactive-forms,Angular,Typescript,Angular Material,Angular8,Angular Reactive Forms,我正在尝试创建一个材质下拉包装(mat select dropdown),它将与formControlName一起使用。如果有人在他们的图书室里,他们可以发布他们的Stackblitz吗?您可以自由地从头开始,创建自己的答案,无论什么都可以满足需求 要求: 1) 需要使用formControlName。我们有一个带有formBuilder/的父组件表单,它的验证器正试图引用这个子包装器。作为典型场景,父组件formbuilder还有许多其他表单字段 2) 如果数据不符合父FormBuilder验

我正在尝试创建一个材质下拉包装(mat select dropdown),它将与formControlName一起使用。如果有人在他们的图书室里,他们可以发布他们的Stackblitz吗?您可以自由地从头开始,创建自己的答案,无论什么都可以满足需求

要求:

1) 需要使用formControlName。我们有一个带有formBuilder/的父组件表单,它的验证器正试图引用这个子包装器。作为典型场景,父组件formbuilder还有许多其他表单字段

2) 如果数据不符合父FormBuilder验证程序的要求,则需要显示错误红色“无效”

3) a)不仅需要使用formControlName/patchValue(patchValue应使用整个类);b) 如果有人将数据放入@Input()SelectedValueId Id号,也可以选择此选项。我可以和这两个人一起工作

正在尝试使其工作,但尚未成功, 有人有代码来修复这个问题吗

需要工作的stackblitz,为了成功的回答

在本例中,Id是地址Id的来源

export class SourceOfAddressDto implements ISourceOfAddressDto {
    sourceOfAddressId: number | undefined;  // should work with this Id
    sourceOfAddressCode: string | undefined;
    sourceOfAddressDescription: string | undefined;
类型脚本:

@Component({
    selector: 'app-address-source-dropdown',
    templateUrl: './address-source-dropdown.component.html',
    styleUrls: ['./address-source-dropdown.component.scss'],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AddressSourceDropdownComponent),
            multi: true
        }
    ]
})
export class AddressSourceDropdownComponent implements OnInit, OnChanges {

    dataList: any[] = []; 
    @Input() Label = 'Address Source';
    @Input() sourceOfAddressDefaultItem: SourceOfAddressDto = SourceOfAddressDefault;
    @Input() selectedSourceOfAddress: any;
    @Input() TxtValue = 'sourceOfAddressId';
    @Input() TxtField = 'sourceOfAddressDescription';
    @Input() Disabled: boolean;
    @Input() valuesToExclude: number[] = [];
    @Input() Hint = '';
    @Input() styles: string;
    @Input() defaultSourceOfAddressCode: any;
    @Output() addressSourceChange = new EventEmitter<any>();

    private _selectedValueId: number;

    @Input() set selectedValueId(value: number) {
        this._selectedValueId = value;

        let outputData: any;
        if (this.selectedValueId == this.sourceOfAddressDefaultItem[this.TxtValue]) {
            outputData = null;
        } else {
            outputData = this.dataList.find(x => x[this.TxtValue] == this.selectedValueId);
        }

        this.onChange(outputData);
    }
    get selectedValueId(): any {
        return this._selectedValueId;
    }
    @Input() errors: any = null;
    disabled: boolean;
    control: FormControl;
    writeValue(value: any) {
        this.selectedValueId = value ? value : '';
    }
    onChange = (_: any) => { };
    onTouched: any = () => { };
    registerOnChange(fn: any) { this.onChange = fn; }
    registerOnTouched(fn: any) { this.onTouched = fn; }
    setDisabledState(isDisabled) { this.disabled = isDisabled; }

    constructor(
        public injector: Injector,
        private AddressService: AddressServiceProxy,
    ) { }

    ngOnInit() {
        this.loadDataList();
    }

    ngOnChanges() { }

    loadDataList() {
        this.AddressService.getSourceOfAddressAll().subscribe(res => {
            this.dataList = res.body.filter(q => q.sourceOfAddressId !== -1);
        });
    }

}
<div class="dropdown-cont">
  <mat-form-field appearance="outline">
    <mat-label>{{Label}}</mat-label>
    <mat-select 
      disableOptionCentering 
      [disabled]="Disabled" 
      [ngStyle]="styles" 

      (ngModelChange)="selectedValueId=$event"
        required>
      <mat-option [value]="sourceOfAddressDefaultItem[TxtValue]">{{sourceOfAddressDefaultItem[TxtField]}}</mat-option>
      <mat-option *ngFor="let item of dataList" [value]="item[TxtValue]">
        {{item[TxtField]}}
      </mat-option>
    </mat-select>
    <mat-hint>{{Hint}}</mat-hint>
  </mat-form-field>
</div>
@组件({
选择器:“应用程序地址源下拉列表”,
templateUrl:'./地址源下拉列表.component.html',
styleUrls:['./地址源下拉列表.component.scss'],
供应商:[
{
提供:NG_值访问器,
useExisting:forwardRef(()=>AddressSourceDropdownComponent),
多:真的
}
]
})
导出类AddressSourceDropdownComponent实现OnInit、OnChanges{
数据列表:任意[]=[];
@Input()标签='地址源';
@Input()sourceOfAddressDefaultItem:SourceOfAddressDto=SourceOfAddressDefault;
@Input()selectedSourceOfAddress:any;
@Input()TxtValue='sourceOfAddressId';
@Input()TxtField='sourceOfAddressDescription';
@Input()已禁用:布尔值;
@Input()值排除:数字[]=[];
@输入()提示=“”;
@Input()样式:字符串;
@Input()defaultSourceOfAddressCode:任意;
@Output()addressSourceChange=新的EventEmitter();
private _selectedValueId:number;
@Input()设置selectedValueId(值:数字){
这是。\ u selectedValueId=value;
让outputData:任意;
如果(this.selectedValueId==this.sourceOfAddressDefaultItem[this.TxtValue]){
outputData=null;
}否则{
outputData=this.dataList.find(x=>x[this.TxtValue]==this.selectedValueId);
}
此.onChange(输出数据);
}
获取selectedValueId():任意{
返回此项。\u selectedValueId;
}
@Input()错误:any=null;
禁用:布尔值;
控制:FormControl;
writeValue(值:任意){
this.selectedValueId=value?值:“”;
}
onChange=(uquo:any)=>{};
onTouched:any=()=>{};
registerOnChange(fn:any){this.onChange=fn;}
registerOnTouched(fn:any){this.onTouched=fn;}
setDisabledState(isDisabled){this.disabled=isDisabled;}
建造师(
公共注入器:注入器,
专用AddressService:AddressServiceProxy,
) { }
恩戈尼尼特(){
这是loadDataList();
}
ngOnChanges(){}
loadDataList(){
此.AddressService.GetSourceOfAddressSall().subscribe(res=>{
this.dataList=res.body.filter(q=>q.sourceofaddress!=-1);
});
}
}
HTML:

@Component({
    selector: 'app-address-source-dropdown',
    templateUrl: './address-source-dropdown.component.html',
    styleUrls: ['./address-source-dropdown.component.scss'],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AddressSourceDropdownComponent),
            multi: true
        }
    ]
})
export class AddressSourceDropdownComponent implements OnInit, OnChanges {

    dataList: any[] = []; 
    @Input() Label = 'Address Source';
    @Input() sourceOfAddressDefaultItem: SourceOfAddressDto = SourceOfAddressDefault;
    @Input() selectedSourceOfAddress: any;
    @Input() TxtValue = 'sourceOfAddressId';
    @Input() TxtField = 'sourceOfAddressDescription';
    @Input() Disabled: boolean;
    @Input() valuesToExclude: number[] = [];
    @Input() Hint = '';
    @Input() styles: string;
    @Input() defaultSourceOfAddressCode: any;
    @Output() addressSourceChange = new EventEmitter<any>();

    private _selectedValueId: number;

    @Input() set selectedValueId(value: number) {
        this._selectedValueId = value;

        let outputData: any;
        if (this.selectedValueId == this.sourceOfAddressDefaultItem[this.TxtValue]) {
            outputData = null;
        } else {
            outputData = this.dataList.find(x => x[this.TxtValue] == this.selectedValueId);
        }

        this.onChange(outputData);
    }
    get selectedValueId(): any {
        return this._selectedValueId;
    }
    @Input() errors: any = null;
    disabled: boolean;
    control: FormControl;
    writeValue(value: any) {
        this.selectedValueId = value ? value : '';
    }
    onChange = (_: any) => { };
    onTouched: any = () => { };
    registerOnChange(fn: any) { this.onChange = fn; }
    registerOnTouched(fn: any) { this.onTouched = fn; }
    setDisabledState(isDisabled) { this.disabled = isDisabled; }

    constructor(
        public injector: Injector,
        private AddressService: AddressServiceProxy,
    ) { }

    ngOnInit() {
        this.loadDataList();
    }

    ngOnChanges() { }

    loadDataList() {
        this.AddressService.getSourceOfAddressAll().subscribe(res => {
            this.dataList = res.body.filter(q => q.sourceOfAddressId !== -1);
        });
    }

}
<div class="dropdown-cont">
  <mat-form-field appearance="outline">
    <mat-label>{{Label}}</mat-label>
    <mat-select 
      disableOptionCentering 
      [disabled]="Disabled" 
      [ngStyle]="styles" 

      (ngModelChange)="selectedValueId=$event"
        required>
      <mat-option [value]="sourceOfAddressDefaultItem[TxtValue]">{{sourceOfAddressDefaultItem[TxtField]}}</mat-option>
      <mat-option *ngFor="let item of dataList" [value]="item[TxtValue]">
        {{item[TxtField]}}
      </mat-option>
    </mat-select>
    <mat-hint>{{Hint}}</mat-hint>
  </mat-form-field>
</div>

{{Label}}
{{sourceOfAddressDefaultItem[TxtField]}
{{item[TxtField]}
{{Hint}}
  • 即使API有时滞后,也希望可以使用默认值,并且默认值首先插入@Input SelectedValueId中

您可能还应该实现
ControlValueAccessor
接口,让angular知道您想要使用反应式表单

export class AddressSourceDropdownComponent implements OnInit, OnChanges, ControlValueAccessor { ...

其他人提到,您需要显式实现
ControlValueAccessor
接口。虽然这绝对是一个好的实践,但在TypeScript中并不需要它,因为满足接口的任何东西都会隐式实现它,这是您使用
writeValue
registerOnChange
registerOnTouched
方法(以及
setDisabledState
执行的,但该方法是可选的)

因此,最大的问题是实现的细节。Angular依赖于此接口的实现来执行
formControlName
(以及
formControl
)的神奇双向绑定,其中父组件可以侦听并设置子组件上的值

您的
writeValue
register
很好。您的
注册表更改
无效。在这里,您可以对组件进行本地更改并“注册”它们,这意味着您可以将通常的Angular valueChanges事件函数挂接到您自己的自定义valueChanges中

使用表单控件实现该功能的典型方法:

control = new FormControl('');

registerOnChange(fn: (value: string) => void) {
    this.control.valueChanges
        .subscribe(fn);
}
因此,一旦您执行了类似的操作,您将从父组件得到上下变化

现在,为了满足您的所有请求,您需要更多的自定义代码。不久前,我碰巧实现了一些非常类似的东西,它工作得很好,我在一个新的环境中重新创建了它


希望这足够让你走了

要使控件在mat form字段中工作,需要执行MatFormFieldControl接口的额外步骤。官方材料文档提供了一个很好的演练和代码示例,说明了如何在此处执行此操作:。请注意,该示例没有实现ControlValueAccessor,您仍然需要这样做。

我最近为实现ckeditor的角度控件而做了这一步。您需要将此代码用于ControlValueAccessor这将与FormControl、FormControlName以及ngModel和