Ответ 1
Я вижу два варианта:
- Распространять ошибки из компонента
FormControl
на<select>
FormControl
всякий раз, когдаFormControl
значение<select>
FormControl
- Распространять валидаторы из компонента
FormControl
в<select>
FormControl
Ниже доступны следующие переменные:
-
selectModel
- этоNgModel
<select>
-
formControl
- этоFormControl
компонента, полученного как аргумент
Вариант 1: распространять ошибки
ngAfterViewInit(): void {
this.selectModel.control.valueChanges.subscribe(() => {
this.selectModel.control.setErrors(this.formControl.errors);
});
}
Вариант 2: распространение валидаторов
ngAfterViewInit(): void {
this.selectModel.control.setValidators(this.formControl.validator);
this.selectModel.control.setAsyncValidators(this.formControl.asyncValidator);
}
Разница между ними заключается в том, что распространение ошибок означает наличие уже ошибок, в то время как опция секунд включает выполнение валидаторов во второй раз. Некоторые из них, например, асинхронные валидаторы, могут оказаться слишком дорогостоящими для выполнения.
Распространение всех свойств?
Нет общего решения для распространения всех свойств. Различные свойства устанавливаются различными директивами или другими способами, таким образом, имеют различный жизненный цикл, а это означает, что требуется особая обработка. Текущее решение касается распространения ошибок проверки и валидаторов. Там доступно много объектов.
Обратите внимание, что вы можете получить разные изменения статуса из экземпляра FormControl
, подписавшись на FormControl.statusChanges()
. Таким образом вы можете получить, является ли элемент управления VALID
, INVALID
, DISABLED
или PENDING
(асинхронная проверка еще выполняется).
Как валидация работает под капотом?
Под капотом валидаторы применяются с использованием директив (проверьте исходный код). Директивы имеют providers: [REQUIRED_VALIDATOR]
что означает, что для регистрации этого экземпляра валидатора используется собственный иерархический инжектор. Поэтому в зависимости от атрибутов, применяемых к элементу, директивы будут добавлять экземпляры проверки на инжектор, связанные с целевым элементом.
Затем эти валидаторы извлекаются NgModel
и FormControlDirective
.
Валидаторы, а также атрибуты доступа извлекаются следующим образом:
constructor(@Optional() @Host() parent: ControlContainer,
@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
и соответственно:
constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
@Optional() @Self() @Inject(NG_ASYNC_VALIDATORS) asyncValidators: Array<AsyncValidator|AsyncValidatorFn>,
@Optional() @Self() @Inject(NG_VALUE_ACCESSOR)
valueAccessors: ControlValueAccessor[])
Обратите внимание, что используется @Self()
, поэтому для получения зависимостей используется собственный инжектор (элемента, к которому применяется директива).
NgModel
и FormControlDirective
есть экземпляр FormControl
который фактически обновляет значение и выполняет валидаторы.
Поэтому основным моментом для взаимодействия является экземпляр FormControl
.
Также в инжекторе элемента, к которому они применяются, регистрируются все валидаторы или аксессоры значений. Это означает, что родитель не должен получить доступ к этому инжектору. Поэтому было бы плохой практикой получить доступ к текущему компоненту инжектора, предоставленному <select>
.
Пример кода для Варианта 1 (легко заменяемый Вариантом 2)
Следующий образец имеет два валидатора: один, который требуется, а другой - шаблон, который заставляет опцию соответствовать "опции 3".
options.component.ts
import {AfterViewInit, Component, forwardRef, Input, OnInit, ViewChild} from '@angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';
import {SettingsService} from '../settings.service';
const OPTIONS_VALUE_ACCESSOR: any = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => OptionsComponent)
};
@Component({
providers: [OPTIONS_VALUE_ACCESSOR],
selector: 'inf-select[name]',
templateUrl: './options.component.html',
styleUrls: ['./options.component.scss']
})
export class OptionsComponent implements ControlValueAccessor, OnInit, AfterViewInit {
@ViewChild('selectModel') selectModel: NgModel;
@Input() formControl: FormControl;
@Input() name: string;
@Input() disabled = false;
private propagateChange: Function;
private onTouched: Function;
private settingsService: SettingsService;
selectedValue: any;
constructor(settingsService: SettingsService) {
this.settingsService = settingsService;
}
ngOnInit(): void {
if (!this.name) {
throw new Error('Option name is required. eg.: <options [name]="myOption"></options>>');
}
}
ngAfterViewInit(): void {
this.selectModel.control.valueChanges.subscribe(() => {
this.selectModel.control.setErrors(this.formControl.errors);
});
}
writeValue(obj: any): void {
this.selectedValue = obj;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
options.component.html
<select #selectModel="ngModel"
class="form-control"
[disabled]="disabled"
[(ngModel)]="selectedValue"
(ngModelChange)="propagateChange($event)">
<option value="">Select an option</option>
<option *ngFor="let option of settingsService.getOption(name)" [value]="option.description">
{{option.description}}
</option>
</select>
options.component.scss
:host {
display: inline-block;
border: 5px solid transparent;
&.ng-invalid {
border-color: purple;
}
select {
border: 5px solid transparent;
&.ng-invalid {
border-color: red;
}
}
}
использование
Определите экземпляр FormControl
:
export class AppComponent implements OnInit {
public control: FormControl;
constructor() {
this.control = new FormControl('', Validators.compose([Validators.pattern(/^option 3$/), Validators.required]));
}
...
Привяжите экземпляр FormControl
к компоненту:
<inf-select name="myName" [formControl]="control"></inf-select>
Настройки макета
/**
* TODO remove this class, added just to make injection work
*/
export class SettingsService {
public getOption(name: string): [{ description: string }] {
return [
{ description: 'option 1' },
{ description: 'option 2' },
{ description: 'option 3' },
{ description: 'option 4' },
{ description: 'option 5' },
];
}
}