Например есть такая задача. Есть компонент, и на него же вешается директива.
<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 задачи:
- Из директивы надо иметь возможность получать доступ к компоненту на котором эта директива висит
- Директива должна уметь работать с любым компонентом, то есть директива не может знать о конкретных типах компонент на этапе 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>
- директива formControl получила доступ к компоненту на котором она висит.
- В тоже время она ссылается на компонент через интерфейс, таким образом директиву можно повесить на любой компонент, который реализует IControlValueAccessor и зарегистрирует себя же в DI/IoC.
Таким образом реализованы ngModel, formControl, input обвязки в Angular.