📜 ⬆️ ⬇️

Angular2-like registration of components and dependencies for knockoutjs

Good day.

I liked the attribute registration of components in angular2 and wanted to do this in a project with knockoutjs.

@Component({ selector: "setup-add-edit-street-name", template: require("text!./AddEditStreetName.tmpl.html"), directives: [BeatSelector] }) export class AddEditStreetNameComponent extends AddEditModalBaseComponent<StreetNameViewModel> { constructor(@Inject("params") params, streetNameService: StreetNameService) { super(params, streetNameService); } location = ko.observable() } 

Components in knockout appeared quite a long time. However, the lack of built-in support for dependency injection, as well as the need for separate registration of components, was somewhat annoying.

Dependency Injection


In this article, I do not want to talk about component loaders and how to use them to add support for DI. Let me just say that in the end I modified this package.
')
Using:
 //   kontainer.registerFactory('taskService', ['http', 'fileUploadService', (http, fileUploadService) => new TaskService(http, fileUploadService)); //    ko.components.register('task-component', { viewModel: ['params', 'taskService', (service) => new TaskComponent(params, service) ], template: '<p data-bind="text: name"></p>' }); 

params are the parameters that were passed to the component through markup.

The problem here is that the registration component is not very convenient. Easily sealed and easy to forget to register a service; I also wanted to make the dependencies more explicit.

Decision


To implement the idea you need to understand how typescript decorators work. In short, this is just some kind of function or factory that will be called at some point in time (which of them can be read in the documentation).

Component registration decorator:
 export interface ComponentParams { selector: string; template?: string; templateUrl?: string; directives?: Function[]; } export function Component(options: ComponentParams) { return (target: { new (...args) }) => { if (!ko.components.isRegistered(options.selector)) { if (!options.template && !options.templateUrl) { throw Error(`Component ${target.name} must have template`); } const factory = getFactory(target); const config = { template: options.template || { require: options.templateUrl }, viewModel: factory }; ko.components.register(options.selector, config); } }; } 

As you can see, the decorator does nothing special. All the magic in the getFactory function :
 interface InjectParam { index: number; dependency: string; } const injectMetadataKey = Symbol("inject"); function getFactory(target: { new (...args) }) { const deps = Reflect.getMetadata("design:paramtypes", target).map(type => type.name); const injectParameters: InjectParam[] = Reflect.getOwnMetadata(injectMetadataKey, target) || []; for (const param of injectParameters) { deps[param.index] = param.dependency; } const factory = (...args) => new target(...args); return [...deps, factory]; } 

Here, using Reflect.getMetadata ("design: paramtypes", target), we pulled out information about the types of arguments taken in the component's constructor (in order for this to work, you need to enable the option in the typeScript transpiler - about this below) and then just compiled factory for IoC from type.name .
Now a little more about injectParamateres . What if we want to inject not an instance of a class, but simply an Object, for example, an application configuration or params passed to a component? In Angular 2 , the Inject decorator is used for this, applied to the parameters of the constructor:
  constructor(@Inject("params") params, streetNameService: StreetNameService) { super(params, streetNameService); } 

Here is its implementation:
 interface InjectParam { index: number; dependency: string; } const injectMetadataKey = Symbol("inject"); export function Inject(token: string) { return (target: Object, propertyKey: string | symbol, parameterIndex: number) => { const existingInjectParameters: InjectParam[] = Reflect.getOwnMetadata(injectMetadataKey, target, propertyKey) || []; existingInjectParameters.push({ index: parameterIndex, dependency: token }); Reflect.defineMetadata(injectMetadataKey, existingInjectParameters, target, propertyKey); }; } 

And finally, the decorator registration service:
 export function Injectable() { return (target: { new (...args) }) => { if (!kontainer.isRegistered(target.name)) { const factory = getFactory(target); kontainer.registerFactory(target.name, factory); } }; } //  @Injectable() export class StreetNameService { constructor(config: AppConfig, @Inject("ApiClientFactory") apiClientFactory: ApiClientFactory) { this._apiClient = apiClientFactory(config.endpoints.streetName); } // ... } 


How to get it all?


Since decorators are not yet included in the standard, you must enable experimentalDecorators and emitDecoratorMetadata to use them in the tsconfig.json file.
Also, since we rely on the names of constructor functions when registering dependencies, it is important to enable the keep_fnames option in the UglifyJS settings.

Source code can be found here .

Source: https://habr.com/ru/post/305828/


All Articles