@paperbits/core
Version:
Paperbits core components.
133 lines (103 loc) • 5.67 kB
text/typescript
import * as ko from "knockout";
import { ComponentDefinition } from "../componentDefinition";
const makeArray = (arrayLikeObject) => {
const result = [];
for (let i = 0, j = arrayLikeObject.length; i < j; i++) {
result.push(arrayLikeObject[i]);
}
return result;
};
let componentLoadingOperationUniqueId = 0;
ko.bindingHandlers["component"] = {
init: function (element: any, valueAccessor: any, ignored1: any, ignored2: any, bindingContext: any): any {
let currentViewModel,
currentLoadingOperationId,
afterRenderSub;
const disposeAssociatedComponentViewModel = function (): any {
if (currentViewModel) {
const onDestroyedMethodDescriptions = Reflect.getMetadata("ondestroyed", currentViewModel.constructor);
if (onDestroyedMethodDescriptions) {
onDestroyedMethodDescriptions.forEach(methodDescription => {
const methodReference = currentViewModel[methodDescription];
if (methodReference) {
methodReference();
}
});
}
else {
const currentViewModelDispose = currentViewModel["dispose"];
if (typeof currentViewModelDispose === "function") {
currentViewModelDispose.call(currentViewModel);
}
}
}
if (afterRenderSub) {
afterRenderSub.dispose();
}
afterRenderSub = null;
currentViewModel = null;
// Any in-flight loading operation is no longer relevant, so make sure we ignore its completion
currentLoadingOperationId = null;
};
const originalChildNodes = makeArray(ko.virtualElements.childNodes(element));
ko.virtualElements.emptyNode(element);
ko.utils.domNodeDisposal.addDisposeCallback(element, disposeAssociatedComponentViewModel);
ko.computed(function (): void {
const value = ko.utils.unwrapObservable(valueAccessor());
let componentName, componentParams;
if (typeof value === "string") {
componentName = value;
} else {
componentName = ko.utils.unwrapObservable(value["name"]);
componentParams = ko.utils.unwrapObservable(value["params"]);
}
if (!componentName) {
throw new Error("No component name specified");
}
const asyncContext = ko.bindingEvent.startPossiblyAsyncContentBinding(element, bindingContext);
const loadingOperationId = currentLoadingOperationId = ++componentLoadingOperationUniqueId;
ko.components.get(componentName, function (componentDefinition: ComponentDefinition): any {
// If this is not the current load operation for this element, ignore it.
if (currentLoadingOperationId !== loadingOperationId) {
return;
}
// Clean up previous state
disposeAssociatedComponentViewModel();
// Instantiate and bind new component. Implicitly this cleans any old DOM nodes.
if (!componentDefinition) {
throw new Error("Unknown component '" + componentName + "'");
}
cloneTemplateIntoElement(componentName, componentDefinition, element);
const componentInfo = { element: element, templateNodes: originalChildNodes };
const componentViewModel = createViewModel(componentDefinition, componentParams, componentInfo),
childBindingContext = asyncContext["createChildContext"](componentViewModel, {
extend: function (ctx: any): any {
ctx["$component"] = componentViewModel;
ctx["$componentTemplateNodes"] = originalChildNodes;
}
});
if (componentViewModel && componentViewModel["koDescendantsComplete"]) {
afterRenderSub = ko.bindingEvent.subscribe(element, ko.bindingEvent["descendantsComplete"], componentViewModel["koDescendantsComplete"], componentViewModel);
}
currentViewModel = componentViewModel;
ko.applyBindingsToDescendants(childBindingContext, element);
});
}, null, { disposeWhenNodeIsRemoved: element });
return { controlsDescendantBindings: true };
}
};
ko.virtualElements.allowedBindings["component"] = true;
function cloneTemplateIntoElement(componentName: string, componentDefinition: ComponentDefinition, element: HTMLElement): any {
const template = componentDefinition["template"];
if (!template) {
throw new Error(`Component "${componentName}" has no template.`);
}
const clonedNodesArray = ko.utils["cloneNodes"](template);
ko.virtualElements.setDomNodeChildren(element, clonedNodesArray);
}
function createViewModel(componentDefinition: ComponentDefinition, componentParams: any, componentInfo: any): any {
const componentViewModelFactory = componentDefinition["createViewModel"];
return componentViewModelFactory
? componentViewModelFactory.call(componentDefinition, componentParams, componentInfo)
: componentParams; // Template-only component
}