@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
115 lines (102 loc) • 6.45 kB
JavaScript
(function (undefined) {
// Overridable API for determining which component name applies to a given node. By overriding this,
// you can for example map specific tagNames to components that are not preregistered.
ko.components['getComponentNameForNode'] = function(node) {
var tagNameLower = ko.utils.tagNameLower(node);
if (ko.components.isRegistered(tagNameLower)) {
// Try to determine that this node can be considered a *custom* element; see https://github.com/knockout/knockout/issues/1603
if (tagNameLower.indexOf('-') != -1 || ('' + node) == "[object HTMLUnknownElement]" || (ko.utils.ieVersion <= 8 && node.tagName === tagNameLower)) {
return tagNameLower;
}
}
};
ko.components.addBindingsForCustomElement = function(allBindings, node, bindingContext, valueAccessors) {
// Determine if it's really a custom element matching a component
if (node.nodeType === 1) {
var componentName = ko.components['getComponentNameForNode'](node);
if (componentName) {
// It does represent a component, so add a component binding for it
allBindings = allBindings || {};
if (allBindings['component']) {
// Avoid silently overwriting some other 'component' binding that may already be on the element
throw new Error('Cannot use the "component" binding on a custom element matching a component');
}
var componentBindingValue = { 'name': componentName, 'params': getComponentParamsFromCustomElement(node, bindingContext) };
allBindings['component'] = valueAccessors
? function() { return componentBindingValue; }
: componentBindingValue;
}
}
return allBindings;
}
var nativeBindingProviderInstance = new ko.bindingProvider();
function getComponentParamsFromCustomElement(elem, bindingContext) {
var paramsAttribute = elem.getAttribute('params');
if (paramsAttribute) {
var params = nativeBindingProviderInstance['parseBindingsString'](paramsAttribute, bindingContext, elem, { 'valueAccessors': true, 'bindingParams': true }),
rawParamComputedValues = ko.utils.objectMap(params, function(paramValue, paramName) {
return ko.computed(paramValue, null, { disposeWhenNodeIsRemoved: elem });
}),
result = ko.utils.objectMap(rawParamComputedValues, function(paramValueComputed, paramName) {
var paramValue = paramValueComputed.peek();
// Does the evaluation of the parameter value unwrap any observables?
if (!paramValueComputed.isActive()) {
// No it doesn't, so there's no need for any computed wrapper. Just pass through the supplied value directly.
// Example: "someVal: firstName, age: 123" (whether or not firstName is an observable/computed)
return paramValue;
} else {
// Yes it does. Supply a computed property that unwraps both the outer (binding expression)
// level of observability, and any inner (resulting model value) level of observability.
// This means the component doesn't have to worry about multiple unwrapping. If the value is a
// writable observable, the computed will also be writable and pass the value on to the observable.
return ko.computed({
'read': function() {
return ko.utils.unwrapObservable(paramValueComputed());
},
'write': ko.isWriteableObservable(paramValue) && function(value) {
paramValueComputed()(value);
},
disposeWhenNodeIsRemoved: elem
});
}
});
// Give access to the raw computeds, as long as that wouldn't overwrite any custom param also called '$raw'
// This is in case the developer wants to react to outer (binding) observability separately from inner
// (model value) observability, or in case the model value observable has subobservables.
if (!result.hasOwnProperty('$raw')) {
result['$raw'] = rawParamComputedValues;
}
return result;
} else {
// For consistency, absence of a "params" attribute is treated the same as the presence of
// any empty one. Otherwise component viewmodels need special code to check whether or not
// 'params' or 'params.$raw' is null/undefined before reading subproperties, which is annoying.
return { '$raw': {} };
}
}
// --------------------------------------------------------------------------------
// Compatibility code for older (pre-HTML5) IE browsers
if (ko.utils.ieVersion < 9) {
// Whenever you preregister a component, enable it as a custom element in the current document
ko.components['register'] = (function(originalFunction) {
return function(componentName) {
document.createElement(componentName); // Allows IE<9 to parse markup containing the custom element
return originalFunction.apply(this, arguments);
}
})(ko.components['register']);
// Whenever you create a document fragment, enable all preregistered component names as custom elements
// This is needed to make innerShiv/jQuery HTML parsing correctly handle the custom elements
document.createDocumentFragment = (function(originalFunction) {
return function() {
var newDocFrag = originalFunction(),
allComponents = ko.components._allRegisteredComponents;
for (var componentName in allComponents) {
if (allComponents.hasOwnProperty(componentName)) {
newDocFrag.createElement(componentName);
}
}
return newDocFrag;
};
})(document.createDocumentFragment);
}
})();