Example usage:
<!-- ko if: allowSorting -->
<th data-bind="text: headerText, click: sortByColumn"></th>
<!-- else -->
<th data-bind="text: headerText"></th>
<!-- /ko -->
This would also work for ifnot
because it shares the same code.
Here, I'm proposing a simple "else" functionality, not "else if". Previous discussion at #283.
:heart:
+1
:+1:
+1
Standalone implementation (compatible with Knockout 3.0.0pre or 2.2+ with Freedom plugin):
function cloneNodes(nodesArray, shouldCleanNodes) {
for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) {
var clonedNode = nodesArray[i].cloneNode(true);
newNodesArray.push(shouldCleanNodes ? ko.cleanNode(clonedNode) : clonedNode);
}
return newNodesArray;
};
ko.bindingHandlers['if'] = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var trueNodes = [], falseNodes = [], isFirstRender = true, wasLastTrue;
function saveTemplates() {
var target = trueNodes;
ko.utils.arrayForEach(ko.virtualElements.childNodes(element), function(node) {
if (node.nodeType == 8 && (node.text ? /<!--\s*else\s*-->/.test(node.text) : /\s*else\s*/.test(node.nodeValue))) {
target = falseNodes;
} else {
target.push(node);
}
});
trueNodes = cloneNodes(trueNodes, true /* shouldCleanNodes */);
falseNodes = cloneNodes(falseNodes, true /* shouldCleanNodes */);
}
ko.computed(function() {
var dataValue = ko.utils.unwrapObservable(valueAccessor()),
isTrue = !!dataValue, // Cast value to boolean
renderNodes = isTrue ? trueNodes : falseNodes; // Set before templates are saved so that the initial bind is to the original nodes
if (isFirstRender || isTrue !== wasLastTrue) {
if (isFirstRender) {
saveTemplates();
} else {
renderNodes = cloneNodes(renderNodes);
}
ko.virtualElements.setDomNodeChildren(element, renderNodes);
ko.applyBindingsToDescendants(bindingContext, element);
wasLastTrue = isTrue;
}
isFirstRender = false;
}, null, { disposeWhenNodeIsRemoved: element });
return { controlsDescendantBindings: true };
}
};
:+1:
@mbest Thanks so much for writing this. I did have trouble with nested if/else, but it seems to work after I made the following modifications:
ko.bindingHandlers['if'] = (function () {
var cloneNodes = function (nodesArray, shouldCleanNodes) {
for (var i = 0, j = nodesArray.length, newNodesArray = []; i < j; i++) {
var clonedNode = nodesArray[i].cloneNode(true);
newNodesArray.push(shouldCleanNodes ? ko.cleanNode(clonedNode) : clonedNode);
}
return newNodesArray;
};
return {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var trueNodes = [], falseNodes = [], isFirstRender = true, wasLastTrue;
function saveTemplates() {
var depth = 0;
var target = trueNodes;
ko.utils.arrayForEach(ko.virtualElements.childNodes(element), function (node) {
if (node.nodeType === 8) {
if (node.text) {
if (/<!--\s*ko\b/.test(node.text)) {
depth += 1;
} else if (/<!--\s*\/ko\b/.test(node.text)) {
depth -= 1;
} else if (depth === 0 && /<!--\s*else\s*-->/.test(node.text)) {
target = falseNodes;
return;
}
} else {
if (/^\s*ko\b/.test(node.nodeValue)) {
depth += 1;
} else if (/^\s*\/ko\b/.test(node.nodeValue)) {
depth -= 1;
} else if (depth === 0 && /\s*else\s*/.test(node.nodeValue)) {
target = falseNodes;
return;
}
}
}
target.push(node);
});
trueNodes = cloneNodes(trueNodes, true /* shouldCleanNodes */);
falseNodes = cloneNodes(falseNodes, true /* shouldCleanNodes */);
}
ko.computed(function () {
var dataValue = ko.utils.unwrapObservable(valueAccessor()),
isTrue = !!dataValue, // Cast value to boolean
renderNodes = isTrue ? trueNodes : falseNodes; // Set before templates are saved so that the initial bind is to the original nodes
if (isFirstRender || isTrue !== wasLastTrue) {
if (isFirstRender) {
saveTemplates();
} else {
renderNodes = cloneNodes(renderNodes);
}
ko.virtualElements.setDomNodeChildren(element, renderNodes);
ko.applyBindingsToDescendants(bindingContext, element);
wasLastTrue = isTrue;
}
isFirstRender = false;
}, null, { disposeWhenNodeIsRemoved: element });
return { controlsDescendantBindings: true };
}
};
}());
@coryandrew1988 - I'm glad you found it useful!
Are there plans to add if/else/elseif functionality to knockout?
Is there a plugin out there? I am all up for modularity, but frankly having if without else, and requiring a plugin to get that functionality is pushing it a bit too far (imagine if you had to import a module in python to do if/else).
Is there any plan to release this as standard with knockout? It really would be a great feature.
With a preprocessor like knockout.punches it might work with something like:
{{ #if: X }}
{{ else }}
{{ /if }}
where {{ else }}
is converted to {{ /if }}{{ ifnot: X }}
. One just has to make sure {{ /if }}
is a substitute for or translated to {{ /ifnot }}
.
One could also have an <!-- ko else -->
preprocessor, without punches, but the idea is easier to illustrate with the punches syntax I think.
This obviously has no performance benefit in that X
has to be evaluated twice, but there are several potential ways to optimize it in future — and in any case it is no slower than the current solution (notwithstanding the switches
plugin).
Hmm. I am mulling how one mind deal with the following using the above:
{{ #if: X }}
{{ #if: Y }}
{{ /if }}
{{ else }}
{{ /if }}
The problem is determining the element to which the else
attaches; one would seem to have to look through peer nodes. Mulling. :)
EDIT The a jsFiddle: Knockout Else Binding now appears to work as expected. It's just an alternative or complement to altering the if
binding as @mbest proposed above.
I have also added foreach
/ else
that shows the content of the else
block when the loop is empty. It should not be too hard to add a general template
binding as well (with foreach/if bindings).
EDIT Rather than mulling through the below, you can get an idea of where I was headed with this in a jsFiddle: Knockout Else Binding. Still needs some work re. virtual elements, but the idea is pretty straightforward.
I believe, it makes sense to account for the following scenarios:
<!-- A. Two element nodes -->
<div data-bind='if: X'></div>
<div data-bind='else'></div>
<!-- B. A leading comment node -->
<!-- ko if: X -->
<!-- /ko -->
<div data-bind='else'></div>
<!-- C. A trailing comment node -->
<div data-bind='if: X'></div>
<!-- ko else -->
<!-- /ko -->
<!-- D. Adjacent comment nodes -->
<!-- ko if: X -->
<!-- /ko -->
<!-- ko else -->
<!-- /ko -->
One should be able to substitute ifnot
for if
, and perhaps also in the future an if
argument to the template
binding.`
This technique would have the advantage of working for heterogeneous comment / element nodes, and not requiring any changes to the builtin nodes.
The <!-- ko if: X --><!-- else --><!-- /ko -->
syntax does not (yet?) work as @mbest illustrated it.
Just food for thought.
Looking forward to a future when components may access and use their initial contents (v3.3?), this could be implemented without ambiguity like this:
<ko-switch params="case: viewModel">
<case value="1">
<div>One!</div>
</case>
<case value="2">
<div>Two!</div>
</case>
</ko-switch>
I'm showing a switch
, but if - else
would work the same way (basically it's a switch on a boolean value).
Less verbose variations are possible with a little bit of imagination and different conventions:
<ko-switch params="case: viewModel">
<div data-case="1">One!</div>
<div data-case="2">Two!</div>
</ko-switch>
@jods4 You can essentially accomplish this now with the knockout-switch-case plugin. Not to take away from the usefulness of accessing the initial contents of components' nodes, but the switch/case itself can be done cleanly with the plugin, to some extent.
Cheers
@brianmhunt I know!
My remark was made in the context of the discussion about what the syntax for this ought to be. I find most of the propositions above unintuitive, inconsistent and sometimes ambiguous (with respect to nested ifs).
Here's a variation:
<!-- ko ifelse: condition -->
<div data-true>True</div>
<div data-false>False</div>
<!-- /ko -->
I was using switch
rather than if
before because in the end if
is just a special case for switch
on a boolean value. As such I'm not sure about the usefulness of an if-else
binding if we include a more useful switch
binding in the core KO.
I created a plugin knockout-else that solves the _else_ problem in the way I described in my comments above. With some unit tests for good measure.
@mbest, could this plugin design resolve this issue for the use case(s) you had been thinking of?
The one use case it does not solve is the <div data-bind='if: x'><!-- else --></div>
(i.e. splitting an if), though I have some thoughts on how one might do that in the plugin, but would be very grateful for your thoughts and insight, if you think it is a key goal to this.
@jods4 I agree a switch
binding could be very handy, though I think there remains a valid use-case for the if - else
(whether the way I have done it or otherwise). Since switch
is a bit off-topic here, would you be able to start another issue (and link this discussion) on how the knockout-switch you are thinking of would differ from knockout-switch-case? I think that would be very helpful.
Cheers.
I have since added "inline" else/else-if bindings to knockout-else
i.e. the following ought to work as expected:
<div data-bind='if:x'>
X
<!-- elseif:y -->
Y
<!-- else -->
Z
</div>
+1
Why does this not exist already?
For a programmer, if...else
should be inherently a built-in feature.
This is in tko now, as both the else
binding (like knockout-else) plus <!-- else -->
inside of an if/ifnot/unless
.
@jods4
I couldn't agree with you more on the intuitiveness of your design. Very clear on what it is your looking for in the markup.
@brianmhunt are there any chances that this library is included in the ko standard?
+1
Most helpful comment
Why does this not exist already?