Передача родительского инпута в дочерний. Создание своих контролов для формы.
Иногда требуется перенаправить и использовать существующий элемент формы, а не создавать избыточную оболочку для доступа к значениям. Одним из распространенных случаев использования является создание, например, пользовательских компонентов ввода.
На следующем изображении описано ожидаемое поведение:
Нам нужно использовать элементы управления формой, переданные через директивы formControl
, formControlName
и ngModel
в нашем новом компоненте и передать их нашему внутреннему элементу ввода. Рассмотрим, как этого можно добиться:
Реализация интерфейса ControlValueAccessor
Первый вариант, который мы можем выбрать, - использовать 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 }); ... }
Однако это не сработает, потому что мы получим круговую зависимость (circular dependency).
Можно решить эту проблему, удалив провайдер, внедрив NgControl
и явно установив свойство valueAccessor
в пустой адаптер значений, так как нам не важно значение; мы просто хотим "удовлетворить" 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(`...`); }
Далее остается только использовать компонент:
@Component({ selector: 'app-input', standalone: true, imports: [ReactiveFormsModule], template: `<input [formControl]="ngControl.control" /> `, }) export class InputComponent { ngControl = injectNgControl(); }
Использование директивы Host
Первый метод работает, но он кажется "костыльным", поскольку мы делаем работу Angular, устанавливая свойство адаптера значений. Другим вариантом является использование директивы host
, чтобы получить тот же результат. Сначала мы создадим директиву 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
, как и ранее, без установки свойства value accessor:
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('...'); }
Далее остается только использовать компонент:
@Component({ selector: 'app-input', standalone: true, // 👇👇👇 hostDirectives: [NoopValueAccessorDirective], imports: [ReactiveFormsModule], template: ` <input [formControl]="ngControl.control" /> `, }) export class InputComponent { ngControl = injectNgControl(); }
Теперь мы можем использовать получившийся компонент ввода с любым управляющим элементом, который нам нужен:
<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>
Это вольный перевод материала