angular
March 16

Передача родительского инпута в дочерний. Создание своих контролов для формы.

Иногда требуется перенаправить и использовать существующий элемент формы, а не создавать избыточную оболочку для доступа к значениям. Одним из распространенных случаев использования является создание, например, пользовательских компонентов ввода.

На следующем изображении описано ожидаемое поведение:

Нам нужно использовать элементы управления формой, переданные через директивы 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>

Это вольный перевод материала