Angular 8: раскрывающийся список выбора матов с помощью formControlName и ControlValueAccessor - PullRequest
0 голосов
/ 20 января 2020

Я пытаюсь создать упаковку выпадающего меню Material (mat-select dropdown), которая будет работать с formControlName. Может кто-нибудь опубликовать свой Stackblitz, если он есть в их библиотеке? Не стесняйтесь начинать с нуля и создавать собственный ответ, независимо от того, что отвечает требованиям.

Требования:

1) Необходимо работать с formControlName. У нас есть родительский компонент form с formBuilder / и его валидаторами, которые пытаются ссылаться на эту дочернюю оболочку. Родительский компонент formbuilder также имеет много других полей формы, как и типичный сценарий ios.

2) Необходимо отображать ошибку красного цвета как недопустимую, если данные не соответствуют требованиям родительских валидаторов FormBuilder.

3) а) Должен работать не только с formControlName / patchValue (patchValue должен работать со всем классом); б) при желании также, если кто-то помещает данные в номер идентификатора @Input () SelectedValueId. может работать с двумя

Попытка заставить это работать, но пока не удалось. У кого-нибудь есть код, чтобы это исправить?

Нужен рабочий стек, для успешного ответа

В этом случае Id is sourceOfAddressId

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);
        });
    }

}

HTML:

<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>
  • также, надеюсь, работает со значениями по умолчанию, даже если API иногда отстает, и значение по умолчанию вставляется первым в @Input SelectedValueId

Ответы [ 4 ]

2 голосов
/ 01 февраля 2020

Другие отметили, что вам необходимо явно реализовать интерфейс ControlValueAccessor. Хотя это определенно хорошая практика, в TypeScript это не требуется, поскольку все, что удовлетворяет интерфейсу, также неявно реализует его, что вы делаете с помощью методов writeValue, registerOnChange и registerOnTouched (а также * 1005). * но это необязательно).

Так что самая большая проблема в этом случае - это специфика вашей реализации. Angular полагается на реализацию этого интерфейса для выполнения магического двухстороннего связывания formControlNameformControl в этом отношении), где родитель может одновременно слушать и установленные значения для дочернего компонента.

Ваши writeValue и registerOnTouched в порядке. Ваш registerOnChange нет. Здесь вы берете изменения локально для вашего компонента и «регистрируете» их, что означает, что вы подключаете обычную функцию события Angular valueChanges к своему собственному пользовательскому значению valueChanges.

Типичный способ реализовать это, используя элемент управления формы :

control = new FormControl('');

registerOnChange(fn: (value: string) => void) {
    this.control.valueChanges
        .subscribe(fn);
}

Итак, когда вы сделаете что-то похожее, вы будете получать изменения вверх и вниз от родительского компонента.

Теперь, чтобы удовлетворить все ваши запросы, вы будете нужен еще немного пользовательского кода. Я реализовал нечто очень похожее некоторое время назад, и это прекрасно работает, и я воссоздал это в stackblitz .

Надеюсь, этого достаточно, чтобы вы начали.

1 голос
/ 31 января 2020

Вероятно, вам также следует реализовать интерфейс ControlValueAccessor, чтобы angular знал, что вы хотите использовать реактивные формы

export class AddressSourceDropdownComponent implements OnInit, OnChanges, ControlValueAccessor { ...
0 голосов
/ 06 февраля 2020

Я сделал это недавно для реализации элемента управления angular для ckeditor. Этот код вам понадобится для ControlValueAccessor , он будет работать с FormControl, FormControlName, а также с ngModel и (если у вас все еще была старая angularJs ng-модель). Не слишком много, в основном, когда значение установлено, вызовите вашу функцию, которую я поместил в DoAnyCodeYouNeedToDoWhenTheValueChanges для вас. Я устанавливал значение в ckeditor, когда когда-либо это вызывали. Затем, когда ckeditor обновится, я обновлю this._Value и вызову this.onChangeCallback , чтобы все остальные знали об изменении.

//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 {
    //console.warn("Get Value", this._Value);
    return this._Value;
};

//set accessor including call the onchange callback
set value(v: any) {
    //console.warn("Set Value", v);
    if (!this.destoryInitiated) { // was running into an issue with executing after it was destroyed
        if (v !== this._Value) {
            this._Value = v || "";
            this.DoAnyCodeYouNeedToDoWhenTheValueChanges(this._Value)
            this.onChangeCallback(v); // make sure other stuff knows about the change
        }
    }
}

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

//From ControlValueAccessor interface
writeValue(value: any) {
    //console.warn("Write Value", value);
    if (!this.destoryInitiated) {
        this._Value = value || "";
        this.DoAnyCodeYouNeedToDoWhenTheValueChanges(this._Value)
    }
}

//From ControlValueAccessor interface
registerOnChange(fn: any) {
    //console.warn("register on change", fn);
    this.onChangeCallback = fn;
}

//From ControlValueAccessor interface
registerOnTouched(fn: any) {
    //console.warn("register on touched", fn);
    this.onTouchedCallback = fn;
}
0 голосов
/ 02 февраля 2020

Чтобы элемент управления работал в mat-form-field , необходимо выполнить дополнительный шаг реализации интерфейса MatFormFieldControl . Официальная документация по материалам содержит хорошее пошаговое руководство и пример кода, как это сделать здесь: Создание пользовательского элемента управления полем формы . Обратите внимание, что в примере не реализован ControlValueAccessor, что вам все равно понадобится.

...