Как получить пользовательский MatFormFieldControl для отображения недопустимого состояния
Я следил за документацией по угловым материалам, в которой рассматривается создание пользовательского элемента управления полем формы: https://material.angular.io/guide/creating-a-custom-form-field-control
Он удобно пропускает шаблон и полный пример реактивных форм, так что я копался в попытке соединить все это.
У меня был удар, с переменным успехом. Хотя есть и другие проблемы, я хотел бы сначала понять, как я могу заставить это настраиваемое поле распознавать, когда оно invalid
так что я могу выполнить <mat-error>
Вы видите ниже (я удалил *ngIf
просто чтобы я мог видеть состояние invalid
). {{symbolInput.invalid}}
всегда false
когда на самом деле это должно быть true
как поле обязательно для заполнения!
Использование пользовательского шаблона MatFormFieldControl:
<mat-form-field class="symbol">
<symbol-input
name="symbol"
placeholder="Symbol"
ngModel
#symbolInput="ngModel"
[(ngModel)]="symbol"
required></symbol-input>
<button
mat-button matSuffix mat-icon-button
*ngIf="symbol && (symbol.asset1 || symbol.asset2)"
aria-label="Clear"
(click)="clearSymbol()">
<mat-icon>close</mat-icon>
</button>
<mat-error >{{symbolInput.invalid}}</mat-error>
</mat-form-field>
Пользовательский класс MatFormFieldControl:
export interface AssetSymbol {
asset1: string, asset2: string
}
@Component({
selector: 'symbol-input',
templateUrl: './symbol-input.component.html',
styleUrls: ['./symbol-input.component.css'],
providers: [{ provide: MatFormFieldControl, useExisting: SymbolInputComponent}]
})
export class SymbolInputComponent implements MatFormFieldControl<AssetSymbol>, OnDestroy {
static nextId = 0;
stateChanges = new Subject<void>();
parts: FormGroup;
focused = false;
errorState = false;
controlType = 'symbol-input';
onChangeCallback;
@HostBinding() id = `symbol-input-${SymbolInputComponent.nextId++}`;
@HostBinding('class.floating')
get shouldLabelFloat() {
return this.focused || !this.empty;
}
@HostBinding('attr.aria-describedby')
describedBy = '';
setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}
get empty() {
let n = this.parts.value;
return !n.asset1 && !n.asset2;
}
@Input()
get value(): AssetSymbol | null {
let n = this.parts.value;
return { asset1: n.asset1, asset2: n.asset2};
}
set value(symbol: AssetSymbol | null) {
symbol = symbol || { asset1: "", asset2: ""};
this.parts.setValue({asset1: symbol.asset1, asset2: symbol.asset2});
this.stateChanges.next();
}
@Input()
get placeholder() {
return this._placeholder;
}
set placeholder(plh) {
this._placeholder = plh;
this.stateChanges.next();
}
private _placeholder: string;
@Input()
get required() {
return this._required;
}
set required(req) {
this._required = coerceBooleanProperty(req);
this.stateChanges.next();
}
private _required = false;
@Input()
get disabled() {
return this._disabled;
}
set disabled(dis) {
this._disabled = coerceBooleanProperty(dis);
this.stateChanges.next();
}
private _disabled = false;
constructor(
fb: FormBuilder,
@Optional() @Self() public ngControl: NgControl,
private fm: FocusMonitor,
private elRef: ElementRef<HTMLElement>) {
this.parts = fb.group({'asset1': '', 'asset2': ''});
// Setting the value accessor directly (instead of using
// the providers) to avoid running into a circular import.
if (this.ngControl != null) this.ngControl.valueAccessor = this;
fm.monitor(elRef.nativeElement, true).subscribe(origin => {
this.focused = !!origin;
this.stateChanges.next();
});
this.stateChanges.subscribe(() => {
this.expandInput(this.value.asset1.length);
if (this.onChangeCallback) {
this.onChangeCallback(this.value);
if (this.required) {
const symbol = this.value;
if (!symbol.asset1 || !symbol.asset2) {
this.errorState = true;
} else {
this.errorState = false;
}
}
}
});
}
onContainerClick(event: MouseEvent) {
if ((event.target as Element).tagName.toLowerCase() != 'input') {
this.elRef.nativeElement.querySelector('input').focus();
}
}
ngOnDestroy() {
this.stateChanges.complete();
this.fm.stopMonitoring(this.elRef.nativeElement);
}
onKeyup() {
this.stateChanges.next();
}
static ASSET1_INPUT_SIZE = 2;
asset1InputSize = SymbolInputComponent.ASSET1_INPUT_SIZE;
expandInput(currentSize) {
//const currentSize = (event.target as HTMLInputElement).value.length;
if (currentSize >= 3) {
this.asset1InputSize = currentSize;
} else {
this.asset1InputSize = SymbolInputComponent.ASSET1_INPUT_SIZE;
}
}
writeValue(value: any) {
this.value = value;
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
}
}
Символ-input.component.html:
<div [formGroup]="parts" >
<input class="asset asset1" formControlName="asset1" (keyup)="onKeyup()" [size]="asset1InputSize" maxlength="5">
<span class="input-spacer">⁄</span>
<input class="asset asset2" formControlName="asset2" size="6" maxlength="5">
</div>
Будет ли кто-то достаточно любезен, чтобы указать мне правильное направление?
** ОБНОВЛЕНО ** symbolInput.invalid
флаг теперь устанавливается после подписки на this.ngControl.valueChanges
и настройка this.ngControl.control.setErrors
:
constructor(
fb: FormBuilder,
@Optional() @Self() public ngControl: NgControl,
private fm: FocusMonitor,
private elRef: ElementRef<HTMLElement>) {
this.parts = fb.group({'asset1': ['',[Validators.required]], 'asset2': ['',[Validators.required]]});
if (this.ngControl != null) this.ngControl.valueAccessor = this;
fm.monitor(elRef.nativeElement, true).subscribe(origin => {
this.focused = !!origin;
this.stateChanges.next();
});
this.ngControl.valueChanges.subscribe(()=>{
this.expandInput(this.value.asset1.length);
if (this.required) {
if (this.parts.invalid) {
this.errorState = true;
this.ngControl.control.setErrors({ "invalidSymbol": true });
} else {
this.errorState = false;
this.ngControl.control.setErrors(null);
}
}
});
this.stateChanges.subscribe(() => {
if (this.onChangeCallback) {
this.onChangeCallback(this.value);
}
});
}
Пожалуйста, сообщите, если вы думаете, что это может быть улучшено.
2 ответа
Ваша реализация выглядит хорошо, и вы всегда получаете
invalid
какfalse
так как вы не добавили никакой проверки.
Вы можете добавить проверку для asset1
а также asset2
поэтому измените следующую строку
this.parts = fb.group({'asset1': '', 'asset2': ''});
в
this.parts = this.fb.group({
asset1: ['', Validators.required],
asset2: ['', Validators.required, Validators.minLength(6)]
});
Я сделал вот что:
get errorState(): boolean {
return (this.model.invalid && this.model.dirty) || (this.ngControl?.invalid && this.ngControl?.dirty);
}
Где
model
мой местный
FormControl
и
ngControl
это родительский элемент управления. Таким образом, он возвращает состояние ошибки, когда мой элемент управления имеет ошибку или родительский элемент недействителен.