knockout if and text binding - javascript

I'm trying to create a simple component in Knockout (3.3.0):
ko.components.register('test', {
viewModel: function() {
this.test = 'hello'
},
template:
`<span data-bind='if: 1, text: test'></span>`
});
ko.applyBindings();
See fiddle.
Now when I instantiate a <test></test> somewhere else, I get an error:
Multiple bindings (if and text) are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.
Is this really not possible? This would be like the most basic functionality to incorporate if you ask me. I know I could use <!-- ko text --> but what about setting other attributes like src and using if at the same time?

OK I found the (or at least, one possible) solution: Use <!-- ko if --><!-- /ko -->. This way, the template can be written like
<!-- ko if: 1-->
<span data-bind='text: test'></span>
<!-- /ko -->
I still don't think it's perfect, in Vue I would just do <span v-if='1'>{{text}}</span> bam done, but I guess not everything in this world can be as awesome as Vue...

Related

knockout.js can you access the binding context from a script?

I have a javascript object graph, an HTML form, and knockout bindings connecting the two. The form is complex, and sometimes the form needs to add some computed observables to some sub-object in our object graph, and I want to do that locally in the the HTML element that has the data-bind which relies on this, I don't want such knowledge somewhere in some global script.
<div class="widget" data-bind="foreach: subThing">
<script type="text/javascript">
$data._scratchpad = ko.computedObservable( ... );
</script>
...
<input data-bind="value: _scratchpad"/>
...
</div>
Now in the context of this script, the binding context is of course not yet set up, so the $data property is not yet available.
But is there some event that I might put on the element or something so I can catch when the bindings are first initialized so I can add the necessary things before the actual data-bind expressions want to refer to them?
I came up with a solution which is just a little ugly, but actually practically correct. Instead of this script element above, I just use a virtual element that contains nothing and whose only point is to get an if: condition evaluated, where then we put the statements into the body of a function that gets evaluated:
<div class="widget" data-bind="foreach: subThing">
<!-- ko if: (function() { if(!$data._scratchpad) {
$data._scratchpad = ko.computedObservable( ... );
}})() --> <!-- /ko -->
...
<input data-bind="value: _scratchpad"/>
...
</div>
The nice thing is that required no modification of the source code. And while it is just a little ugly with the boiler-plate code:
<!-- ko if: (function() { if(!...) {
...
}})() --> <!-- /ko -->
I could potentially use a custom binding's preprocessor to wrap this function around and say instead simply:
<!-- ko setup:
...
--> <!-- /ko -->
this is almost neat, but really not so much better that it's worth it.
It's kind-a handy that this virtual element definition is already in a comment, so there won't be any worries with the javascript code using special characters.

Knockout bind function in IF statement

I need to get a value from the ViewModel using a value from an object located in a loop in the view/page.
<!-- ko foreach: ExtendedItems() -->
<tr>
<!-- ko foreach: PriceGroups() -->
<!-- ko if: DeductibleAmount() === $root.FindDeductibleValue($parent.Provider()) -->
<td> --Content-- </td>
<!-- /ko -->
<!-- /ko -->
</tr>
<!-- /ko -->
I have a function in the ViewModel that finds the correct value:
self.FindDeductibleValue = function (provider) {
return self.SelectedDeductibles.findObs('Provider', { Provider: provider }).Value();
}
This function works fine when I call it from the ViewModel, but I get "Cannot read property 'Value' of null at viewModel.self.FindDeductibleValue" when I try to use it in the view/page in the IF statement.
I've replaced $parent.Provider() with a number just to make sure that $parent.Provider() is not null, but it doesn't change the error I receive.
Is it not possible to use a function in an IF statement this way?
The issue here is "binding context". Couple things first though.
EDIT:
I just noticed that the method FindDeductibleValue() receives
something other than the currently iterated item, like knockout does
automatically. And the code within it seems a little strange. Can you
please post the full view models you're working with? I'm not
convinced there's enough context to answer the problem correctly here.
I would use the $parents array not $root in case the depth or pattern ever changes.
You don't need to invoke your observables in your bindings, knockout will do this for you. <!-- ko foreach: ExtendedItems --> instead of <!-- ko foreach: ExtendedItems() -->.
The main error you're encountering is a binding context issue, whereby when invoking the method from within the loop, you are essentially within a child context of the viewmodel. Unfortunately, JavaScript isn't able to tell you this. But it is telling you the problem. The child item doesn't have a Value property. In order to fix this you need to bind to the correct context I believe the code would be:
$parents[0].FindDeductibleValue.bind(null, $parent.Provider())

Knockout 3.x: markup inside element referencing a named template is wiped out

In Knockout 2.x, one could write:
<div data-bind="template: { name: 'my-template', foreach: elements }">
<div class="placeholder"><- these are the array elements.</div>
</div>
This way, a placeholder element could be appended to the rendered elements.
But, as I see, Knockout 3.0 changed this behavior: "placeholder" markup is just wiped out of the container element.
Is this intentional? Any workarounds?
One solution that I can immediately think of is adding "afterRender" to the template definition, but this has (according to my experiments with KO 3.0) a weird glitch: it doesn't fire for empty "elements" array.
Thank you in advance for your answers.
In comparing similar usages of templates to that of using the foreach option, I could not get the placeholder markup to render except for the specific case identified. As such, I'd assume that might not have been an intended usage.
Since the foreach option to the template binding doesn't provide anything that can't be replicated, one option would be to move the elements into the data option of a regular template and then "manually" provide the foreach markup either within a "container" element or containerless.
Containerless option
<script type="text/html" id="containerless-regular-template">
<!-- ko foreach: $data -->
<div data-bind="text:$data"></div>
<!-- /ko -->
<div class="placeholder"><- these are the array elements.</div>
</script>
If you want to get a little more fancy due to more dynamic requirements for the "placeholder", you could nest another template call at the end of the foreach markup within a containerless if binding. The outermost container would be limited but the contents would be dynamic.
<script type="text/html" id="my-nested-template">
<!-- ko foreach: $data.foreach -->
<div data-bind="text:$data"></div>
<!-- /ko -->
<!-- ko if: $data.template -->
<div data-bind="template: { name: template.name, data: template.data }"></div>
<!--/ko -->
</script>
<div data-bind="template: { name: 'my-nested-template', data: {foreach: [], template: {name: 'another-template-name', data: {}}} }">
Fiddle

Knockout js nested control flow bindings not working

I'm just starting with Knockout and I'm not sure if I found a bug with the library or if I'm just noobing it up.
I have an array that I want to split between three lists for display purposes but it appears the start and end ul tags are messing stuff up for me.
Here is a jsfiddle of what I'm trying to do, it appears that only the first list item is being rendered for each list while the others are skipped.
http://jsfiddle.net/bn44e/2/
Here is a nearly identical jsfiddle where I replace the <ul></ul> tags with <p>start</p> and <p>stop</p> and all the list items are being rendered.
http://jsfiddle.net/Ue4C3/
This is not a bug, the opening and closing of ko virtual element binding must be in the same level of DOM.
Because what ko managed is DOM, it needs to dynamically create/delete all DOM (not string) content inside a virtual element. ko has no way to manage a single <ul> (or </ul>) String.
So in the first jsfiddle, ko treats your binding like this:
<!-- ko foreach: items -->
<!-- ko if: $index() === 0 || $index() === 10 || $index() === 20 -->
<ul>
<!-- ignored because there is no matching opening -->
<!-- /ko -->
<li>Index <span data-bind="text: $index()" style="color:blue;"></span></li>
<!-- ignored because there is no matching closing -->
<!-- ko if: $index() === 9 || $index() === 19 || $index() === 29 -->
</ul>
<!-- /ko --> <!-- this closing actually matches the first opening ko -->
<!-- /ko -->
UPDATE
It looks like the ignored virtual bindings messed up the binding context for the inner <span data-bind="text: $index()"></span>, have to remove the unmatched virtual bindings to make the internal $index working. http://jsfiddle.net/bn44e/1/
I don't know the reason behind the context problem, guess need to dig through knockout source code to find out the impact of unmatched virtual bindings.
But anyway, unmatched virtual bindings should not remain in production code.

How to run regular/normal JavaScript code with Knockout JS loops etc

lets say you have a simple Knockout loop
<!-- ko foreach: data -->
<div ... show something </div>
<!-- /ko -->
What is I would like to run some extra JavaScript code on each iteration of the loop (just want to put this code in the view and not the model). Not sure if using script tag is suitable, I don’t think that would work if the code was inside a knockout template as that would already be contained within a script tag.
There are a few other cases where I though been able to run normal JavaScript code inside knockout view would be useful. It is not something I would want to do often however it seems on the rare occasion it would be easier if I could just use regular JS rather than changing other things.
You shouldn't be doing that.
The reason is, you'd be mixing business logic and data, and that shouldn't happen.
You are able to execute a function during a foreach using the afterAdd handler
<!-- ko foreach: { data: data, afterAdd: myhandler } -->
<div ... show something </div>
<!-- /ko -->
Hope this helps.

Categories

Resources