"No value accessor for..." 采用模型驱动形式

"No value accessor for..." with model-driven form

我正在尝试使用 Material 设计组件构建一个临时下拉菜单,但无法弄清楚为什么我的 ControlValueAccessor 无法正常工作。这是代码的相关部分:

import {
    AfterViewInit,
    Component,
    ElementRef,
    forwardRef,
    Input,
    OnInit,
    Provider,
    ViewChild
} from '@angular/core';

import {
    NG_VALUE_ACCESSOR,
    ControlValueAccessor,
    CORE_DIRECTIVES
} from '@angular/common';

import { MdCard } from '@angular2-material/card';

import {
    MdInput,
    MD_INPUT_DIRECTIVES
} from '@angular2-material/input';

import { MD_LIST_DIRECTIVES } from '@angular2-material/list';

declare var module: {
    id: string;
};

export const MD_SELECT_VALUE_ACCESSOR = new Provider(
    NG_VALUE_ACCESSOR, { useExisting: forwardRef(() => MdSelect), multi: true });

const noop = () => {};

@Component({
    selector: 'md-select',
    moduleId: module.id,
    template: `
        <div>
            <md-input readOnly type="text" [placeholder]="placeholder" (click)="selectClick()">
                <i md-suffix class="fa fa-sort-desc"></i>
            </md-input>
            <md-card [ngClass]="{ visible: menuVisible }" (blur)="menuBlur()">
                <md-list>
                    <md-list-item class="md-option" *ngFor="let option of options" (click)="optionClick(option)" [ngClass]="{ 'selected': option.selected }">
                        {{option.name}}
                    </md-list-item>            
                </md-list>
            </md-card>
        </div>
    `,
    styleUrls: [
        'md-select.component.css'
    ],
    directives: [
        CORE_DIRECTIVES,
        MdCard,
        MdInput,
        MD_INPUT_DIRECTIVES,
        MD_LIST_DIRECTIVES
    ],
    providers: [MD_SELECT_VALUE_ACCESSOR]
})
export class MdSelect implements ControlValueAccessor {
    @Input() multiple: boolean;
    @Input() placeholder: string;
    private _value: string;
    onChanged: (_: any) => void = noop;
    onTouched: () => void = noop;
    options: MdOption[] = [];
    menuVisible: boolean = false;
    selectedOption: MdOption;
    private _selectedOptions: MdOption[] = [];
    addOption(option: MdOption) {
        this.options.push(option);
        if (option.selected && (!this.selectedOption || this.multiple)) {
            this.selectedOption = option;
            this.value = this.selectedOption.name;
        }
    }
    selectClick() {
        if (!this.menuVisible) {
            this.menuVisible = true;
        }
    }
    optionClick(option: MdOption) {
        if (option) {
            if (this.multiple) {
                option.selected = !option.selected;
            } else {
                this.options.filter(option => option.selected).forEach(option => option.selected = false);
                option.selected = true;
            }
            this.onChanged('value');
        }
        this.menuBlur();
    }
    menuBlur() {
        this.menuVisible = false;
    }
    get value(): string {
        return this.options.filter(option => option.selected).map(option => option.name).join(', ')
    }
    set value(value: string) {
        if (value !== this._value) {
            this._value = value; // TODO
            this.onChanged('value');
        }
    }
    writeValue(value: any): void {
        console.log('writeValue("' + value + '")')
        this.value = value;
    }
    registerOnChange(fn: (_: any) => void): void {
        this.onChanged = (_: any) => { console.log('onChange("' + _ + '")'); fn(_); };
    }
    registerOnTouched(fn: () => void): void {
        this.onTouched = () => { console.log('onTouched()'); fn(); }
    }
}

@Component({
    selector: 'md-option',
    template: `
        <div #wrapper>
            <ng-content></ng-content>
        </div>    
    `
})
export class MdOption implements AfterViewInit {
    @ViewChild('wrapper') wrapper: ElementRef;
    @Input() disabled: boolean;
    name: string;
    @Input() selected: boolean;
    @Input() value: string;
    constructor(private select: MdSelect) { }
    ngAfterViewInit() {
        if (this.wrapper) {
            let name = this.wrapper.nativeElement.innerHTML;
            this.name = name ? name.trim() : 'EMPTY';
        }
        this.select.addOption(this);
    }
}

这是模板中使用它的部分

<div class="md-form-control">
    <md-select placeholder="Shift" class="shift" formControlName="shift">
        <md-option *ngFor="let s of shifts" [value]="s.id" [ngValue]="s.id"
            [selected]="s.id === shift.id">
            {{s.name}}
        </md-option>  
    </md-select>
</div>

这是表单设置

private initForm() {
    ...
    this.form = this.formBuilder.group({
        shift: [this.shift.name],
        ...
    })
}

如果我尝试 运行 这个代码,我会得到

platform-browser.umd.js:1900 ORIGINAL EXCEPTION: No value accessor for 'shift'

我错过了什么?

我在实现 MD textarea 控件并使用 @angular2-material 中的 MdInput 检查时找到了原因。

目前有 2 个桶包含 NG_VALUE_ACCCESSORControlValueAccessor@angular/common@angular/forms。我猜想 新形式 他们将访问器的东西移到了 formscommon 中的旧实现仍然留给那些还没有切换的人。

但是,如果您碰巧导入了错误的组件,则不会有任何警告。

因此,如果您正在使用新表单模块,解决方案是更改

import {
    NG_VALUE_ACCESSOR,
    ControlValueAccessor,
    DefaultValueAccessor
} from '@angular/common'; 

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

您可能还需要更改

export const YOUR_CUSTOM_CONTROL_ACCESSOR = new Provider(
    NG_VALUE_ACCESSOR, { useExisting: forwardRef(() => YourCustomControlAccessor), multi: true });

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