Skip to content

Instantly share code, notes, and snippets.

@samoshkin
Created February 27, 2020 17:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samoshkin/5bcc56ebea36cba43cb82ac96cb82274 to your computer and use it in GitHub Desktop.
Save samoshkin/5bcc56ebea36cba43cb82ac96cb82274 to your computer and use it in GitHub Desktop.
Angular DI и Forms. О чем вам не расскажут книги.

Например есть такая задача. Есть компонент, и на него же вешается директива.

<input [formControl]="controller.formControl"></input>

где component - в данном случае это родной [formControl] - это директива

Задача директивы это настроить связку между "input.value" (view) и controller.formControl (model) в две стороны:

  • отслеживать controller.formControl.onChanges, и менять input.value
  • отслеживать input.onChanges и менять controller.formController.value"

Директиву можно запедалить так, что она будет знать о конкретном компоненте, то есть знать как слушать "change event" от input и уметь сеттить "input.value". С другой стороны хочется чтобы директива была generic, и ее можно было повесить на любой компонент, чтобы тот мог участвовать в form data binding и интегрироваться с Angular Forms.

Например,

// родный компоненты
<select [formControl]="controller.formControl">
<input type="checkbox" [formControl]="controller.formControl">

// ваши кастомные компоненты
<colorpicker [formControl]="controller.formControl" />
<datatimeselect [formControl]="controller.formControl" />

Для этого нужно решить 2 задачи:

  1. Из директивы надо иметь возможность получать доступ к компоненту на котором эта директива висит
  2. Директива должна уметь работать с любым компонентом, то есть директива не может знать о конкретных типах компонент на этапе compile-time

Первое, создаем интерфейс, который должны реализовывать компоненты, если они хотят учавствовать в form data binding.

interface IControlValueAccessor {
  // setters
  writeValue(obj: any): void;

  // event emitters
  registerOnChange(fn: any): void;
  registerOnTouched(fn: any): void;
}

Далее в нашем компоненте реализовываем этот интерфейс.

@Сomponent({
  selector: '[ngModel],[formControl],[formControlName]',
  host: {
    '(input)': '$any(this)._handleInput($event.target.value)',
    '(blur)': 'onTouched()',
  },
  providers: [DEFAULT_VALUE_ACCESSOR]
})
export class InputComponent implements IControlValueAccessor {
   ...
   // компонент  представляет конкретный input и знает как с ним общаться, и как слушать change events
}

Чтобы наша formControl директива имела доступ к эту компоненту, компонент регистрирует себя же в DI/IoC через providers опцию.

В тоже время formControl директива должна ссылаться на компонент через интерфейс IControlValueAccessor, а не через конкретный класс InputComponent, поэтому в DI/IoC надо регистрировать компонент используя интерфейс в качестве ключа. С другой стороны, интерфейс в TypeScript это понятие которое существует только во время compile-time, а не runtime, поэтому напрямую интерфейс в runtime использовать не получиться.

В качестве workaround используем InjectionToken в качестве ключа, чтобы сделать регистрацию сервиса именно по интефейсу.

// injection token, acts as a key to index service in IoC
const NG_VALUE_ACCESSOR = new InjectionToken<IControlValueAccessor>('NgValueAccessor');

// service registration
export const DEFAULT_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,

  // component registers itself
  useExisting: forwardRef(() => InputComponent),
  multi: true
};

Далее в директиве нужно получить доступ к IControlValueAccessor через constructor injection. @Inject() метадата указывает по какому ключу искать сервис в контейнере. @Self() метадата указывает что мы должны ограничить поиск только в контейнере на уровне текущего компонента (child injector).

@Directive({selector: '[formControl]', exportAs: 'ngForm'})
export class FormControlDirective extends NgControl implements OnChanges {
  @Input('formControl') form !: FormControl;

  constructor(@Optional() @Self() @Inject(NG_VALIDATORS) validators: Array<Validator|ValidatorFn>,
              @Optional() @Self() @Inject(NG_VALUE_ACCESSOR) valueAccessors: ControlValueAccessor[]) {
                super();
                this._rawValidators = validators || [];
                this._rawAsyncValidators = asyncValidators || [];
                this.valueAccessor = selectValueAccessor(this, valueAccessors);
              }

   // примерная логика этой директивы
   logic(){
      // model to view pipeline
      form.onChange(modelValue => {
         this.valueAccessor.writeValue(modelValue);
      });

      // view to model pipeline
      this.valueAccessor.onChange(viewValue => {
         this.form.setValue(viewValue);
      })
   }
}

Таким образом, в этой конфигурации

<input [formControl]="controller.formControl"></input>
  1. директива formControl получила доступ к компоненту на котором она висит.
  2. В тоже время она ссылается на компонент через интерфейс, таким образом директиву можно повесить на любой компонент, который реализует IControlValueAccessor и зарегистрирует себя же в DI/IoC.

Таким образом реализованы ngModel, formControl, input обвязки в Angular.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment