(() => { // The default loader is responsible for two things: // 1. Maintaining the default in-memory registry of component configuration objects // (i.e., the thing you're writing to when you call ko.components.register(someName, ...)) // 2. Answering requests for components by fetching configuration objects // from that default in-memory registry and resolving them into standard // component definition objects (of the form { createViewModel: ..., template: ... }) // Custom loaders may override either of these facilities, i.e., // 1. To supply configuration objects from some other source (e.g., conventions) // 2. Or, to resolve configuration objects by loading viewmodels/templates via arbitrary logic. var defaultConfigRegistry = {}; ko.components.register = (componentName, config) => { if (!config) { throw new Error('Invalid configuration for ' + componentName); } if (ko.components.isRegistered(componentName)) { throw new Error('Component ' + componentName + ' is already registered'); } defaultConfigRegistry[componentName] = config; }; ko.components.isRegistered = componentName => Object.prototype.hasOwnProperty.call(defaultConfigRegistry, componentName); ko.components.unregister = componentName => { delete defaultConfigRegistry[componentName]; ko.components.clearCachedDefinition(componentName); }; ko.components.defaultLoader = { 'getConfig': (componentName, callback) => { var result = ko.components.isRegistered(componentName) ? defaultConfigRegistry[componentName] : null; callback(result); }, 'loadComponent': (componentName, config, callback) => { var errorCallback = makeErrorCallback(componentName); resolveConfig(componentName, errorCallback, config, callback); }, 'loadTemplate': (componentName, templateConfig, callback) => resolveTemplate(makeErrorCallback(componentName), templateConfig, callback) , 'loadViewModel': (componentName, viewModelConfig, callback) => resolveViewModel(makeErrorCallback(componentName), viewModelConfig, callback) }; var createViewModelKey = 'createViewModel'; // Takes a config object of the form { template: ..., viewModel: ... }, and asynchronously convert it // into the standard component definition format: // { template: , createViewModel: function(params, componentInfo) { ... } }. // Since both template and viewModel may need to be resolved asynchronously, both tasks are performed // in parallel, and the results joined when both are ready. We don't depend on any promises infrastructure, // so this is implemented manually below. function resolveConfig(componentName, errorCallback, config, callback) { var result = {}, makeCallBackWhenZero = 2, tryIssueCallback = () => { if (--makeCallBackWhenZero === 0) { callback(result); } }, templateConfig = config['template'], viewModelConfig = config['viewModel']; if (templateConfig) { ko.components._getFirstResultFromLoaders('loadTemplate', [componentName, templateConfig], resolvedTemplate => { result['template'] = resolvedTemplate; tryIssueCallback(); }); } else { tryIssueCallback(); } if (viewModelConfig) { ko.components._getFirstResultFromLoaders('loadViewModel', [componentName, viewModelConfig], resolvedViewModel => { result[createViewModelKey] = resolvedViewModel; tryIssueCallback(); }); } else { tryIssueCallback(); } } function resolveTemplate(errorCallback, templateConfig, callback) { if (templateConfig instanceof Array) { // Assume already an array of DOM nodes - pass through unchanged callback(templateConfig); } else if (templateConfig instanceof DocumentFragment) { // Document fragment - use its child nodes callback([...templateConfig.childNodes]); } else if (templateConfig['element']) { var element = templateConfig['element']; if (element instanceof HTMLElement) { // Element instance - copy its child nodes callback(cloneNodesFromTemplateSourceElement(element)); } else if (typeof element === 'string') { // Element ID - find it, then copy its child nodes var elemInstance = document.getElementById(element); if (elemInstance) { callback(cloneNodesFromTemplateSourceElement(elemInstance)); } else { errorCallback('Cannot find element with ID ' + element); } } else { errorCallback('Unknown element type: ' + element); } } else { errorCallback('Unknown template value: ' + templateConfig); } } function resolveViewModel(errorCallback, viewModelConfig, callback) { if (typeof viewModelConfig === 'function') { // Constructor - convert to standard factory function format // By design, this does *not* supply componentInfo to the constructor, as the intent is that // componentInfo contains non-viewmodel data (e.g., the component's element) that should only // be used in factory functions, not viewmodel constructors. callback(params => new viewModelConfig(params)); } else if (typeof viewModelConfig[createViewModelKey] === 'function') { // Already a factory function - use it as-is callback(viewModelConfig[createViewModelKey]); } else if ('instance' in viewModelConfig) { // Fixed object instance - promote to createViewModel format for API consistency var fixedInstance = viewModelConfig['instance']; callback(() => fixedInstance); } else if ('viewModel' in viewModelConfig) { // Resolved AMD module whose value is of the form { viewModel: ... } resolveViewModel(errorCallback, viewModelConfig['viewModel'], callback); } else { errorCallback('Unknown viewModel value: ' + viewModelConfig); } } function cloneNodesFromTemplateSourceElement(elemInstance) { if (elemInstance.matches('TEMPLATE')) { // For browsers with proper