Иногда мы хотим перенаправить и использовать существующий элемент управления формы, а не создавать избыточную оболочку средства доступа к значению. Одним из распространенных вариантов использования является создание, например, пользовательских компонентов input
. Следующее изображение описывает нашу цель:
Мы хотим использовать элементы управления формы, переданные через директивы formControl
, formControlName
и ngModel
, в нашем пользовательском компоненте input
и перенаправить их в наш внутренний элемент input
. Давайте рассмотрим два способа, которыми мы можем это сделать:
Установка свойства Control valueAccessor
Первый вариант, который мы могли бы выбрать, — это использовать NodeInjector
и получить ссылку на наш элемент управления с помощью провайдера NgControl
:
@Component({ selector: 'app-input', template: `<input />`, providers: [{ provide: NG_VALUE_ACCESSOR, multi: true, useExisting: InputComponent }] }) export class InputComponent implements ControlValueAccessor { const ngControl = inject(NgControl, { self: true }); ... }
Однако это не сработает, потому что мы получим ошибку циклической зависимости.
Это можно решить, удалив провайдера, внедрив NgControl
и явно установив для свойства valueAccessor
метод доступа к значению noop
, поскольку нам не важно значение; мы просто хотим «удовлетворить» Angular.
class NoopValueAccessor implements ControlValueAccessor { writeValue() {} registerOnChange() {} registerOnTouched() {} } function injectNgControl() { const ngControl = inject(NgControl, { self: true, optional: true }); if (!ngControl) throw new Error('...'); if ( ngControl instanceof FormControlDirective || ngControl instanceof FormControlName || ngControl instanceof NgModel ) { // 👇👇👇 ngControl.valueAccessor = new NoopValueAccessor(); return ngControl; } throw new Error(`...`); }
Теперь мы можем использовать его в нашем компоненте input
:
@Component({ selector: 'app-input', standalone: true, imports: [ReactiveFormsModule], template: `<input [formControl]="ngControl.control" /> `, }) export class InputComponent { ngControl = injectNgControl(); }
Использование директив хоста
Первый метод работает, но он всегда кажется «хакерским», поскольку мы, по сути, выполняем работу Angular, устанавливая свойство доступа к значению. Теперь мы можем использовать функцию директив хоста, чтобы получить тот же результат. Сначала мы создадим NoopValueAccessorDirective
:
@Directive({ standalone: true, providers: [ { provide: NG_VALUE_ACCESSOR, multi: true, useExisting: NoopValueAccessorDirective, }, ], }) export class NoopValueAccessorDirective implements ControlValueAccessor { writeValue(obj: any): void {} registerOnChange(fn: any): void {} registerOnTouched(fn: any): void {} }
Следующий шаг — создать ту же функцию injectNgControl
, что и раньше, без установки свойства доступа к значению:
export function injectNgControl() { const ngControl = inject(NgControl, { self: true, optional: true }); if (!ngControl) throw new Error('...'); if ( ngControl instanceof FormControlDirective || ngControl instanceof FormControlName || ngControl instanceof NgModel ) { return ngControl; } throw new Error('...'); }
Наконец, мы будем использовать его в нашем компоненте input
:
@Component({ selector: 'app-input', standalone: true, // 👇👇👇 hostDirectives: [NoopValueAccessorDirective], imports: [ReactiveFormsModule], template: ` <input [formControl]="ngControl.control" /> `, }) export class InputComponent { ngControl = injectNgControl(); }
Теперь мы можем использовать наш компонент input
с любым control
, который захотим:
<app-input [formControl]="control" /> <form [formGroup]="form"> <app-input formControlName="name"></app-input> <ng-container formArrayName="skills"> <app-input [formControlName]="index" *ngFor="let c of skills.controls; index as index"></app-input> </ng-container> </form>
Подпишитесь на меня в Medium или Twitter, чтобы узнать больше об Angular и JS!