UNPKG

@paperbits/core

Version:
133 lines (103 loc) 5.67 kB
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 }