Angular2-like registration of components and dependencies for knockoutjs
Good day.
I liked the attribute registration of components in angular2 and wanted to do something similar in a project with knockoutjs.
The components in the knockout appeared a long time ago. However, the lack of built-in support for dependency injection, as well as the need for separate component registration, was somewhat annoying.
In this article I do not want to talk about component loaders and how to use them to add DI support. I can only say that in the end I modified this package.
Using:
params are those parameters that were passed to the component through the markup.
The problem here is that registering the components is not very convenient. Easy to seal up and easy to forget to register a service; I also wanted to make the dependencies more explicit.
To implement the idea, you need to understand how decorators work in typescript. In short, it’s just a function or factory that will be called at some point in time (which one can be found in the documentation).
Component Registration Decorator:
As you can see, the decorator does nothing special. All the magic in the getFactory function :
Here, with the help of Reflect.getMetadata ("design: paramtypes", target), we pulled out information about the types of accepted arguments in the component constructor (for this to work, you need to enable the option in the typeScript transpiler - more on that below) and then simply collected Factory for IoC from type.name .
Now a little more about injectParamateres . What if we want to inject not some class instance, but just Object, for example, application configuration or params passed to the component? In angular2 it uses decorator the Inject , applied to the parameters of the constructor:
Here is its implementation:
And finally, the service registration decorator:
Since decorators are not yet standard, to use them in the tsconfig.json file, you need to enable experimentalDecorators and emitDecoratorMetadata .
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 .
I liked the attribute registration of components in angular2 and wanted to do something similar 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 {
constructor(@Inject("params") params, streetNameService: StreetNameService) {
super(params, streetNameService);
}
location = ko.observable()
}
The components in the knockout appeared a long time ago. However, the lack of built-in support for dependency injection, as well as the need for separate component registration, was somewhat annoying.
Dependency injection
In this article I do not want to talk about component loaders and how to use them to add DI support. I can only 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: ''
});
params are those parameters that were passed to the component through the markup.
The problem here is that registering the components is not very convenient. Easy to seal up 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 decorators work in typescript. In short, it’s just a function or factory that will be called at some point in time (which one can be found 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, with the help of Reflect.getMetadata ("design: paramtypes", target), we pulled out information about the types of accepted arguments in the component constructor (for this to work, you need to enable the option in the typeScript transpiler - more on that below) and then simply collected Factory for IoC from type.name .
Now a little more about injectParamateres . What if we want to inject not some class instance, but just Object, for example, application configuration or params passed to the component? In angular2 it uses decorator the Inject , 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 service registration decorator:
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 make it all?
Since decorators are not yet standard, to use them in the tsconfig.json file, you need to enable experimentalDecorators and emitDecoratorMetadata .
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 .