Элементы управления пользовательской формы Angular с элементом доступа к контрольному значению
В этой статье мы собираемся изучить использование пользовательских компонентов Angular с реактивными формами. Для этого воспользуемся встроенной директивой Angular «control value accessor».
При разработке сложных приложений Angular разработчики, как правило, создают большие компоненты, которые охватывают всю форму и могут затруднить эффективное повторное использование. В этой ситуации выдвигается концепция многоразовых / нестандартных компонентов. Элементы управления настраиваемой формы - типичный шаблон в сложных приложениях Angular. Часто требуется инкапсулировать HTML, CSS и специальные возможности во входном компоненте, чтобы упростить его использование в формах во всем приложении.
Control Value Accessor Directives
Control Value Accessor - это встроенная директива в Angular, которая будет отвечать за отслеживание значения дочернего поля и передачу его обратно в родительскую форму.
В приведенном ниже примере мы собираемся создать собственный счетчик числовых значений, который содержит две кнопки для увеличения или уменьшения значения. Кроме того, мы предлагаем min
и max
диапазоны значений для этого счетчика, где она действует.
Прежде всего, мы создаем новый пользовательский компонент Angular с именем «custom counter», который действует как родительский компонент для этой практики. Вот реализованный кастомный компонент.
@Component({
selector: 'custom-counter',
template: 'ccustom-counter.component.html',
styleUrls: ["custom-counter.component.scss"]
})
export class CustomCounterComponent {
counter = 0;
@Input()
increment: number;
onAdd() {
this.counter+= this.increment;
}
onRemove() {
this.counter-= this.increment;
}
}
В приведенном выше фрагменте вы можете видеть, что мы реализовали методы onAdd
и onRemove
для увеличения и уменьшения значения счетчика. Также ниже показан шаблон компонента настраиваемого счетчика.
<div class="custom-counter">
<mat-icon (click)="onRemove()" color="primary">minus_box</mat-icon>
<div class="counter">{{counter}}</div>
<mat-icon (click)="onAdd()" color="primary">add_box</mat-icon>
</div>
Следующим шагом является реализация дочернего компонента, который использует компонент настраиваемого счетчика. Мы назвали дочерний компонент формой количества и добавили встроенные валидаторы для добавления значений диапазона min
и max
.
@Component(...)
export class QuantityForm implements OnInit {
form: FormGroup;
this.form = this.fb.group({
quantity: [60, [Validators.required, Validators.max(100), Validators.min(0)]]
});
constructor(private fb: FormBuilder) {}
}
Шаблон компонента формы количества, как показано ниже:
<div [formGroup]="form">
<coustom-counter [increment]="10" formControlName="quantity"></coustom-counter>
</div>
Понимание Control Value Accessor Interface
Тем не менее, мы не использовали фактическое использование интерфейса Control Value Accessor. Следовательно, следующим шагом является понимание применимости интерфейса Accessor Control Value.
Интерфейс Control Value Accessor предоставляет несколько методов, и все эти методы предназначены для вызова только модулем Forms во время выполнения, и они предназначены для облегчения связи между нашим элементом управления формой и родительской формой.
writeValue
: модуль Forms записывает значение в элемент управления формы, используя этот метод.registerOnChange
: если значение формы изменяется из-за ввода данных пользователем, нам нужно сообщить значение обратно в родительскую форму. Это делается путем вызова обратного вызова, который изначально был зарегистрирован в элементе управления с помощью методаregisterOnChange
.registerOnTouched
: когда пользователь впервые взаимодействует с элементом управления формы, считается, что этот элемент имеет статус затронутого, что полезно для стилизации. Чтобы сообщить родительской форме о касании элемента управления, нам нужно использовать обратный вызов, зарегистрированный с помощью методаregisterOnToched
.setDisabledState
: элементы управления формы можно включать и отключать с помощью Forms API. Это состояние можно передать элементу управления формой с помощью методаsetDisabledState
.
В качестве следующего шага давайте обсудим эти методы один за другим с точки зрения их применимости.
Реализация writeValue
Этот метод используется в модуле форм Angular всякий раз, когда родительский компонент должен установить значение для дочернего элемента управления. Мы можем сделать это, как показано ниже.
writeValue(counter: number) {
this.counter = counter;
}
Реализация registerOnChange
Используя метод writeValue, родительский компонент может установить значение для дочернего элемента управления. Но критическая ситуация заключается в том, что когда значение счетчика изменяется путем увеличения или уменьшения, родительский компонент должен знать об этом, чтобы захватить изменяющиеся значения.
Там дочерний компонент может уведомить родителя об этом новом значении с помощью функции обратного вызова. Для этого родительский компонент должен зарегистрировать функцию обратного вызова с помощью метода registerOnChange
, как показано ниже.
onChange = (counter) => {};
registerOnChange(onChange: any) {
this.onChange = onChange;
}
Когда вызывается метод registerOnChange
, измененное значение сохраняется в переменной onChange
. В этой реализации мы объявили переменную onChange
как функцию, и изначально она имеет пустое тело. Таким образом, если программа по какой-либо причине вызывает функцию до того, как был сделан вызов registerOnChange
, она не столкнется с какими-либо ошибками.
Когда мы увеличиваем или уменьшаем значение счетчика, мы можем уведомить родительский компонент, вызвав функцию обратного вызова, как показано ниже.
onAdd() {
this.counter+= this.counter;
this.onChange(this.counter);
}
onRemove() {
this.counter-= this.counter;
this.onChange(this.counter);
}
Реализация registerOnTouched
Когда форма инициализируется, считается, что элементы управления формы находятся в нетронутом состоянии, и CSS класс ng-untouched
применяется к группе форм, а также к каждому из ее отдельных дочерних элементов управления.
В предыдущих разделах мы устанавливали и получали изменения значений родительского компонента. Кроме того, нам нужно уведомить, что элемент управления формы был затронут пользователем родительского компонента. Поэтому мы используем метод registerOnTouched
.
Если пользователь действительно взаимодействует с элементом управления формой, это означает, что CSS класс ng-untouched
будет применен к элементу управления формой.
Прежде всего, нам нужно зарегистрировать функцию обратного вызова как предыдущую функцию, как показано ниже.
registerOnTouched(onTouched: any) {
this.onTouched = onTouched;
}
Затем нам нужно вызвать этот обратный вызов в ситуациях, когда значение счетчика увеличивается или уменьшается пользователем. Там статус элемента управления формы будет изменен как нажатый.
touched = false;
onAdd() {
this.markAsTouched();
this.counter+= this.increment;
this.onChange(this.counter);
}
onRemove() {
this.markAsTouched();
this.counter-= this.increment;
this.onChange(this.counter);
}
markAsTouched() {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
Реализация setDisabledState
Мы можем использовать метод setDisabledState
, чтобы отключить или включить элемент управления дочерней формы. Там мы инициализируем отключенную переменную как false
и меняем ее значение, вызывая метод setDisabledState
.
disabled = false;
setDisabledState(disabled: boolean) {
this.disabled = disabled;
}
Настройка интерфейса ControlValueAccessor
Это основной шаг этой реализации, который заключается в регистрации компонента настраиваемого счетчика как известного средства доступа к значению в системе внедрения зависимостей с помощью интерфейса ControlValueAccessor.
@Component({
selector: 'custom-counter',
template: 'ccustom-counter.component.html',
styleUrls: ["custom-counter.component.scss"],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi:true,
useExisting: CustomCounterComponent
}
]
})
export class CustomCounterComponent implements ControlValueAccessor{
}
В качестве конфигураций мы предоставляем NG_VALUE_ACCESSOR
для регистрации этого компонента, добавляя его в список известных средств доступа к значениям. Обратите внимание, что для флага multi
установлено значение true
, это означает, что эта зависимость предоставляет список значений, а не только одно значение. Теперь наш пользовательский компонент может устанавливать значение свойства в форме. Кроме того, наш компонент теперь способен участвовать в процессе валидации формы с required
и min
, max
.
Что, если нам нужно настроить пользовательскую проверку для элемента управления?
Если у компонента должны быть свои собственные встроенные правила проверки, нам нужно реализовать собственный класс счетчика с помощью интерфейса Validator
. Этот интерфейс содержит два метода:
validate
: этот метод используется для проверки текущего значения элемента управления формы. Этот метод будет вызываться всякий раз, когда новое значение передается в родительскую форму. Этот метод возвращает значениеnull
, если ошибок не обнаружено.registerOnValidatorChange
: это зарегистрирует обратный вызов, который позволит нам запускать проверку пользовательского элемента управления по запросу. Здесь нам не нужно запускать этот метод всякий раз, когда новое значение вводится в элемент управления, но это необходимо, если форма решит вызвать проверки (что-то изменилось снаружи, форма инициализирована и т.д.)
Для объяснения предположим, что мы удалили встроенный валидатор min
из элемента управления и вместо этого реализуем пользовательский валидатор для проверки значения счетчика ниже нуля. Мы можем сделать это, как показано ниже:
validate(control: AbstractControl): ValidationErrors | null {
const counter = control.value;
if (counter <= 0) {
return {
mustBePositive: {
counter
}
};
}
}
В этом фрагменте кода мы возвращаем null
, если значение счетчика действительное, в противном случае мы возвращаем объект ошибки. Для этого нам не нужен метод registerOnValidatorChange
, поскольку мы используем только текущее значение элемента управления.
Для завершения этой реализации нам нужно зарегистрировать наш компонент как инъекцию NG_VALIDATORS
.
@Component({
selector: 'custom-counter',
template: 'ccustom-counter.component.html',
styleUrls: ["custom-counter.component.scss"],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi:true,
useExisting: CustomCounterComponent
},
{
provide: NG_VALIDATORS,
multi:true,
useExisting: CustomCounterComponent
}
]
})
export class CustomCounterComponent implements ControlValueAccessor, Validatro {
}
Теперь мы создали полностью функциональный настраиваемый элемент управления, способный как устанавливать значение свойства формы, так и участвовать в процессе проверки формы. Завершенный код, как показано ниже.
@Component({
selector: 'custom-counter',
templateUrl: "custom-counter.component.html",
styleUrls: ["custom-counter.component.scss"],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi:true,
useExisting: CustomCounterComponent
},
{
provide: NG_VALIDATORS,
multi: true,
useExisting: CustomCounterComponent
}
]
})
export class CustomCounter implements ControlValueAccessor, Validator {
counter = 0;
@Input()
increment: number;
onChange = (counter) => {};
onTouched = () => {};
touched = false;
disabled = false;
onAdd() {
this.markAsTouched();
if (!this.disabled) {
this.counter+= this.increment;
this.onChange(this.counter);
}
}
onRemove() {
this.markAsTouched();
if (!this.disabled) {
this.counter-= this.increment;
this.onChange(this.counter);
}
}
writeValue(counter: number) {
this.counter = counter;
}
registerOnChange(onChange: any) {
this.onChange = onChange;
}
registerOnTouched(onTouched: any) {
this.onTouched = onTouched;
}
markAsTouched() {
if (!this.touched) {
this.onTouched();
this.touched = true;
}
}
setDisabledState(disabled: boolean) {
this.disabled = disabled;
}
validate(control: AbstractControl): ValidationErrors | null {
const counter = control.value;
if (counter <= 0) {
return {
mustBePositive: {
counter
}
};
}
}
}