snappymail/vendors/knockout/src/components/loaderRegistry.js

137 lines
6.5 KiB
JavaScript

(() => {
var loadingSubscribablesCache = Object.create(null), // Tracks component loads that are currently in flight
loadedDefinitionsCache = Object.create(null); // Tracks component loads that have already completed
ko.components = {
get: (componentName, callback) => {
var cachedDefinition = loadedDefinitionsCache[componentName];
if (cachedDefinition) {
ko.tasks.schedule(() => callback(cachedDefinition.definition) );
} else {
// Join the loading process that is already underway, or start a new one.
var subscribable = loadingSubscribablesCache[componentName],
completedAsync;
if (subscribable) {
subscribable.subscribe(callback);
} else {
// It's not started loading yet. Start loading, and when it's done, move it to loadedDefinitionsCache.
subscribable = loadingSubscribablesCache[componentName] = new ko.subscribable();
subscribable.subscribe(callback);
beginLoadingComponent(componentName, definition => {
loadedDefinitionsCache[componentName] = { definition: definition };
delete loadingSubscribablesCache[componentName];
// For API consistency, all loads complete asynchronously. However we want to avoid
// adding an extra task schedule if it's unnecessary (i.e., the completion is already
// async).
//
// You can bypass the 'always asynchronous' feature by putting the synchronous:true
// flag on your component configuration when you register it.
if (completedAsync) {
// Note that notifySubscribers ignores any dependencies read within the callback.
// See comment in loaderRegistryBehaviors.js for reasoning
subscribable['notifySubscribers'](definition);
} else {
ko.tasks.schedule(() => subscribable['notifySubscribers'](definition));
}
});
completedAsync = true;
}
}
},
clearCachedDefinition: componentName =>
delete loadedDefinitionsCache[componentName],
register: (componentName, config) => {
if (!config) {
throw new Error('Invalid configuration for ' + componentName);
}
if (defaultConfigRegistry[componentName]) {
throw new Error('Component ' + componentName + ' is already registered');
}
defaultConfigRegistry[componentName] = config;
}
};
// 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 = Object.create(null);
var createViewModelKey = 'createViewModel';
// Takes a config object of the form { template: ..., viewModel: ... }, and asynchronously convert it
// into the standard component definition format:
// { template: <ArrayOfDomNodes>, 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 loadComponent(componentName, callback) {
var result = {},
config = defaultConfigRegistry[componentName] || {},
templateConfig = config['template'],
viewModelConfig = config['viewModel'];
if (templateConfig) {
if (!templateConfig['element']) {
throwError(componentName, 'Unknown template value: ' + templateConfig);
}
// Element ID - find it, then copy its child nodes
var element = templateConfig['element'];
var elemInstance = document.getElementById(element);
if (!elemInstance) {
throwError(componentName, 'Cannot find element with ID ' + element);
}
if (!elemInstance.matches('TEMPLATE')) {
throwError(componentName, 'Template Source Element not a <template>');
}
// For browsers with proper <template> element support (i.e., where the .content property
// gives a document fragment), use that document fragment.
result['template'] = ko.utils.cloneNodes(elemInstance.content.childNodes);
}
if (viewModelConfig) {
if (typeof viewModelConfig[createViewModelKey] !== 'function') {
throwError(componentName, 'Unknown viewModel value: ' + viewModelConfig);
}
// Already a factory function - use it as-is
result[createViewModelKey] = viewModelConfig[createViewModelKey];
}
if (result['template'] && result[createViewModelKey]) {
callback(result);
} else {
callback(null);
}
}
function throwError(componentName, message) {
throw new Error(`Component '${componentName}': ${message}`)
}
function beginLoadingComponent(componentName, callback) {
// Try the candidates
var found = false;
loadComponent(componentName, result => {
// This candidate returned a value. Use it.
(found = result != null) && callback(result);
});
if (!found) {
// No candidates returned a value
callback(null);
}
}
ko.exportSymbol('components', ko.components);
ko.exportSymbol('components.register', ko.components.register);
})();