Skip to content

Instantly share code, notes, and snippets.

@thiagomajesk
Last active July 3, 2019 20:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thiagomajesk/6bead491e1b062b10be0559b6dce9c6c to your computer and use it in GitHub Desktop.
Save thiagomajesk/6bead491e1b062b10be0559b6dce9c6c to your computer and use it in GitHub Desktop.
Knockout & Typescript & Webpack bootstrap code
// Component definition (holds information about template and view model)
class ClickCounterComponent extends Component {
constructor(name: string, viewModel: ViewModelClass) {
super(name, viewModel, template)
}
}
// Actual view model (This separation allows us to reuse view models for components)
class ClickCounterViewModel extends ViewModel {
public count: ko.Observable<number> = ko.observable<number>(0);
constructor(params: any) {
super(params);
}
public increment(): void {
this.count(this.count() + 1);
}
}
// Standard view model
class DashboardViewModel extends ViewModel
{
public number: ko.Observable<number>;
constructor(params: number) {
super(params);
this.number = ko.observable<number>(params);
}
public increase(): void {
this.number(this.number()+1);
}
}
/*
* Setup code for the application (usually the webpack entry point)
*/
const application = new Application();
application.registerViewModel("dashboard", DashboardViewModel);
application.registerComponent("click-counter", ClickCounterComponent, ClickCounterViewModel);
application.start();
<body>
<!-- Standard view model data bind usage -->
<div data-bind="viewModel: 'dashboard'">
<p data-bind="text: number"></p>
</div>
<!-- Allows passing parameters -->
<div data-bind="viewModel: { name: 'dashboard', params: 100}">
<p data-bind="text: number"></p>
</div>
<!-- Can still be used with components -->
<click-counter></click-counter>
</body>
import ko, { BindingHandler } from "knockout";
import $ from "jquery";
import { Md5 } from 'ts-md5';
export class Application {
public readonly viewModels: Array<ViewModelDefinition> = [];
public readonly components: Array<ComponentDefinition> = [];
public masterViewModel: ViewModel;
constructor() {
let bindingHandlers: any = ko.bindingHandlers;
bindingHandlers.viewModel = new ViewModelBindingHandler(this);
this.masterViewModel = new ViewModel();
}
public registerViewModel(name: string, viewModel: ViewModelClass) {
let definition = { name, class: viewModel };
if (this.viewModels.includes(definition)) return;
this.viewModels.push(definition);
}
public registerComponent(name: string, component: ComponentClass, viewModel: ViewModelClass) {
let definition = { name, class: component, viewModel };
if (this.components.includes(definition)) return;
this.components.push(definition);
}
public start(): void {
$(document).ready(() => {
this.bootstrapComponents();
this.bootstrapViewModels();
});
}
private bootstrapViewModels(): void {
ko.applyBindings(this.masterViewModel);
(<any>window)["__masterViewModel__"] = this.masterViewModel;
}
private bootstrapComponents(): void {
this.components.forEach((definition) => {
let component = new definition.class(definition.name, definition.viewModel);
component.register();
})
}
}
interface ViewModelDefinition {
name: string,
class: ViewModelClass;
}
interface ComponentDefinition {
name: string;
class: ComponentClass,
viewModel: ViewModelClass
}
interface ViewModelBindingHandlerStructure {
name: string;
params: any;
}
class ViewModelBindingHandler implements BindingHandler<string>{
constructor(private readonly application: Application) { }
public init = (element: HTMLElement, valueAccessor: () => string,
allBindings: ko.AllBindings, viewModel: any,
bindingContext: ko.BindingContext<string>): ko.BindingHandlerControlsDescendant => {
let [name, params] = this.unwrap(valueAccessor());
let definition = this.application.viewModels.find(d => d.name == name);
if (definition == null) throw new Error(`${name} is not registered`);
let vm: any = new definition.class(params);
this.attachToMasterViewModel(vm, name, element);
let innerBindingContext = bindingContext.createChildContext(vm);
ko.applyBindingsToDescendants(innerBindingContext, element);
return { controlsDescendantBindings: true };
}
private attachToMasterViewModel = async (viewModel: ViewModel, name: string, element: HTMLElement): Promise<void> => {
return new Promise((resolve) => {
let masterViewModel: any = this.application.masterViewModel;
masterViewModel.slaveViewModels = masterViewModel.slaveViewModels || {};
let hash = Md5.hashStr(JSON.stringify(element));
let propertyName = `${name}+${hash}`;
$(element).attr('data-id', hash as string);
masterViewModel.slaveViewModels[propertyName] = viewModel;
resolve();
});
}
private isObject(value: string | ViewModelBindingHandlerStructure): value is ViewModelBindingHandlerStructure {
let object = value as ViewModelBindingHandlerStructure;
return object.name != undefined && object.params != undefined;
}
private unwrap(value: string | ViewModelBindingHandlerStructure): [string, any] {
let object = ko.unwrap(value);
if (typeof object == "string") return [object as string, null];
if (this.isObject(object)) { return [object.name, object.params] }
return [null, null];
}
}
export interface ViewModelClass {
new(params?: any): ViewModel;
}
export class ViewModel {
constructor(private readonly params?: any) { }
}
export interface ComponentClass {
new(name?: string, viewModel?: ViewModelClass): Component;
}
export class Component {
constructor(private readonly name?: string,
private readonly viewModel?: ViewModelClass,
private readonly template?: string) { }
public register() {
if (ko.components.isRegistered(this.name)) return;
ko.components.register(this.name, {
template: this.template,
viewModel: this.getViewModel
});
}
private getViewModel = (params: any): ViewModel => {
return new this.viewModel(params);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment