knockout-switch-case
Version:
Powerful control flow bindings for Knockout
210 lines (182 loc) • 9.5 kB
JavaScript
/**
* @license SWITCH/CASE binding for Knockout http://knockoutjs.com/
* (c) Michael Best
* License: MIT (http://www.opensource.org/licenses/mit-license.php)
* Version 2.1.0
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(['knockout'], factory);
} else {
// Browser globals
factory(root.ko);
}
}(this, function(ko) {
var undefined;
if (!ko.virtualElements)
throw Error('Switch-case requires at least Knockout 2.1');
var ko_virtualElements = ko.virtualElements;
var ko_bindingFlags = ko.bindingFlags || {};
var ko_bindingRewriteValidators = ko.bindingRewriteValidators || ko.jsonExpressionRewriting.bindingRewriteValidators;
var ko_unwrap = ko.utils.unwrapObservable;
var ko_bindingHandlers = ko.bindingHandlers;
var defaultvalue = {};
ko_bindingHandlers['switch'] = {
flags: ko_bindingFlags.contentBind | ko_bindingFlags.canUseVirtual | ko_bindingFlags.noValue,
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var switchSkipNextArray = [],
switchBindings = {
// these properties are internal
$switchSkipNextArray: switchSkipNextArray,
$switchValueAccessor: valueAccessor,
$switchDefault: ko.observable(true),
// these properties are public
$default: defaultvalue,
$else: defaultvalue
},
contexts = [];
// Update $value in each context when it changes
ko.computed(function() {
var value = ko_unwrap(valueAccessor());
switchBindings.$value = value;
ko.utils.arrayForEach(contexts, function(context) {
context.$value = value;
});
}, null, {disposeWhenNodeIsRemoved: element});
// Each child element gets a new binding context so it can set its own $switchIndex property.
// The other properties will be shared since they're objects.
var node, nextInQueue = ko_virtualElements.firstChild(element);
while (node = nextInQueue) {
nextInQueue = ko_virtualElements.nextSibling(node);
switch (node.nodeType) {
case 1: case 8:
var newContext = bindingContext.extend(switchBindings);
// Set initial value of context.$switchIndex to undefined
newContext.$switchIndex = undefined;
ko.applyBindings(newContext, node);
// Add the context to the list to be updated if this section contained a case binding
if (newContext.$switchIndex !== undefined) {
contexts.push(newContext);
}
break;
}
}
return { controlsDescendantBindings: true };
},
preprocess: function(value) {
return value || 'true';
}
};
ko_bindingRewriteValidators['switch'] = false; // Can't rewrite control flow bindings
ko_virtualElements.allowedBindings['switch'] = true;
function checkCase(value, bindingContext) {
// Check value and determine result:
// If the control value is boolean, the result is the matching truthiness of the value
// If value is boolean, the result is the value (allows expressions instead of just simple matching)
// If value is an array, the result is true if the control value matches (strict) an item in the array
// Otherwise, the result is true if value matches the control value (loose)
var switchValue = ko_unwrap(bindingContext.$switchValueAccessor());
return (typeof switchValue == 'boolean')
? (value ? switchValue : !switchValue)
: (typeof value == 'boolean')
? value
: (value instanceof Array)
? (ko.utils.arrayIndexOf(value, switchValue) !== -1)
: (value == switchValue);
}
function checkNotCase(value, bindingContext) {
return !checkCase(value, bindingContext);
}
function defaultMakeValueAccessor(ifValue) {
return function() { return ifValue };
}
function makeCaseHandler(binding, isNot, makeValueAccessor) {
var checkFunction = isNot ? checkNotCase : checkCase;
binding || (binding = 'if');
makeValueAccessor || (makeValueAccessor = defaultMakeValueAccessor);
return {
// Inherit flags from the binding we're wrapping
flags: ko_bindingHandlers[binding].flags,
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
if (!bindingContext.$switchSkipNextArray)
throw Error("case binding must only be used with a switch binding");
if (bindingContext.$switchIndex !== undefined)
throw Error("case binding cannot be nested");
// Initialize $switchIndex and push a new observable to $switchSkipNextArray
bindingContext.$switchIndex = bindingContext.$switchSkipNextArray.push(ko.observable(false)) - 1;
bindingContext.$caseValue = ko.observable();
ko.computed(function() {
var index = bindingContext.$switchIndex,
isLast = (index === bindingContext.$switchSkipNextArray.length - 1),
result, skipNext, noDefault;
if (index && bindingContext.$switchSkipNextArray[index-1]()) {
// An earlier case binding matched: skip this one (and subsequent ones)
result = false;
skipNext = true;
} else {
var value = ko_unwrap(valueAccessor());
if (value === bindingContext.$else) {
// If value is the special object $else, the result depends on the other case values.
// If we're the last *case* item, the value must be true. $switchDefault will get
// updated to *true* below, but that won't necessarily update us because it would
// require a recursive update.
result = bindingContext.$switchDefault() || isLast;
skipNext = false;
} else {
// If result is true, we will skip the subsequent cases (and any default cases)
noDefault = skipNext = result = checkFunction(value, bindingContext);
}
}
// Set the observable used by the wrapped binding function
bindingContext.$caseValue(result);
// Update the observable "skip next" value; if the value is changed, this will update the
// subsequent case item.
bindingContext.$switchSkipNextArray[index](skipNext);
// Update $switchDefault to false if a non-default case item has matched.
// Update it to true if we're the last item and none of items have matched.
// (Initially, every item will be the last, but it doesn't matter.)
if (noDefault)
bindingContext.$switchDefault(false);
else if (!skipNext && isLast)
bindingContext.$switchDefault(true);
},
null, { disposeWhenNodeIsRemoved: element });
// Call init with the observable result value
if (ko_bindingHandlers[binding].init) {
return ko_bindingHandlers[binding].init(element,
makeValueAccessor(bindingContext.$caseValue), allBindings, viewModel, bindingContext);
}
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
// Call update with the observable result value
if (ko_bindingHandlers[binding].update) {
return ko_bindingHandlers[binding].update(element,
makeValueAccessor(bindingContext.$caseValue), allBindings, viewModel, bindingContext);
}
}
};
}
// Support dynamically creating new case binding when using Punches plugin
function getNamespacedHandler(bindingName, namespace, bindingKey) {
if (ko_virtualElements.allowedBindings[bindingName])
ko_virtualElements.allowedBindings[bindingKey] = true;
return makeCaseHandler(bindingName, namespace === 'casenot');
}
// Support dynamically creating new case binding when using key.subkey plugin
function makeSubkeyHandler(baseKey, subKey, bindingKey) {
return getNamespacedHandler(subKey, baseKey, bindingKey);
}
function makeBaseHandler(name, isNot) {
ko_bindingHandlers[name] = makeCaseHandler('if', isNot);
ko_bindingRewriteValidators[name] = false; // Can't rewrite control flow bindings
ko_virtualElements.allowedBindings[name] = true;
ko_bindingHandlers[name].makeSubkeyHandler = makeSubkeyHandler;
ko_bindingHandlers[name].getNamespacedHandler = getNamespacedHandler;
}
makeBaseHandler('case');
makeBaseHandler('casenot', true /*isNot*/);
ko_bindingHandlers['case.visible'] = makeCaseHandler('visible');
ko_bindingHandlers['casenot.visible'] = makeCaseHandler('visible', true /*isNot*/);
ko_bindingHandlers['switch'].makeCaseHandler = makeCaseHandler;
}));