Containerless control flow syntax - the templates don't render properly with IE8 - javascript

When using containerless syntax and calling a Knockoutjs template, the IE8 doesn't render properly a template inside a foreach control flow. The initialization works ok, but if the items are changed, then the rendering is wrong. This happens only on IE8, 9 is ok, even 7 works.
Model
function BrowseModel() {
var self = this;
self.items = ko.observableArray();
self.itemsStep = ko.observable(1);
self.repopulate = function() {
self.itemsStep(self.itemsStep() + 3);
return false;
};
ko.computed(function() {
var arr = [];
for (var i = self.itemsStep(); i <= self.itemsStep() + 5; i++) {
arr.push(i);
}
self.items(arr);
}, self);
}
ko.applyBindings(new BrowseModel());
View
Change items
<ul>
<!-- ko foreach: items -->
<!-- ko template: { name: 'product_template'} -->
<!-- /ko -->
<!-- /ko -->
</ul>
<ul>
<li data-bind="template: { foreach: items, name: 'product_template' }"></li>
</ul>
<br />
<div data-bind="text: ko.toJSON($data)"></div>
<script type="text/html" id="product_template">
<li data-bind="text: $data"></li>
</script>
I didn't find some pattern how IE 8 behaves. The rendering is kind random.
Beside not using the containerless control flow syntax, how can I fix this?
Fiddle
LE: I'm using F12 Developer Tools if this does matter

Fixed jsFiddle
I've added a to the inner binding and it seems to have fixed the problem. It seems like knockout in IE8 does not like nested containerless control bindings that have no content.
Note, in my experience, containerless control bindings tend to show erratic behavior in IE6-IE8. If you intend to support these browsers, I suggest you avoid containerless control bindings. Nearly all scenario's that involve containerless control bindings can be rewritten to an alternative with a HTML element with a data-bind expression.

I've had luck with this structure in IE8. When I tried to keep the containerless stuff separate as you show in the original post, IE8 complained. By using the syntax shown below, it works fine.
<table id="mam-listing-table" border="0" width="100%" cellpadding="2" cellspacing="0">
<thead>
<tr valign="top">
<th class="ms-vh" nowrap="">Team</th>
<th class="ms-vh" nowrap="" colspan="99">Note Count</th>
</tr>
<tr valign="top">
<th class="ms-vh" nowrap=""></th>
<!-- ko foreach: Months -->
<th class="ms-vh" nowrap="" data-bind="text: $data "></th>
<!-- /ko -->
<th class="ms-vh" nowrap="" data-bind="text: 'TOTAL as of ' + moment().format('MM/DD/YY')"></th>
</tr>
</thead>
<tbody>
<!-- ko template: { name: 'Site', foreach: Sites} -->
<!-- /ko -->
<!-- ko template: { name: 'Total'} -->
<!-- /ko -->
</tbody>
</table>

Related

Knockout bindings from JSON file issues

I am using Typescript to define my Knockout ViewModel.
I have a JSON file, the structure of which can be seen here (Github gist as It's a bit large to paste here).
The structure is basically:
OrderLine (the root) -> milestones -> factory_date
Or orally: (many) order lines have (many) milestones, which each have (one) factory date.
I am trying to build a ViewMOdel with the following:
var FactoryAppViewModel = (function () {
function FactoryAppViewModel(seasonID) {
var self = this;
self.seasonID = seasonID;
self.orderlines = ko.observableArray([]);
this.buildViewModel();
}
FactoryAppViewModel.prototype.buildViewModel = function () {
var self = this;
var getOrderLines = HTTP.get("/season/" + self.seasonID + "/orderlines").done(function (data) {
self.orderlines(JSON.parse(data));
}).fail(function () {
error("Could not get orderlines");
});
};
As far as I know, the JSON.parse on the data here will apply the values to the orderlines ko.observableArray([]), However I need to apply a ko.observable to the order lines children (milestones), and to a milestones child (factory_date) as well. And I don't know how to do this. Least of all from JSON.
I have read this but it didn't seem to help me.
I know that the observable isn't applied, because when i change a factory_date in the view, it doesn't update the viewmodel.
Any help would be appreciated. The javascript above is the compiled TypeScript.
EDIT:
Here is an example of the way I'm accessing the code in the view:
<tbody data-bind="foreach: orderlines">
<tr>
<td data-bind="text: factory.name"></td>
<!-- ko foreach: milestones -->
<!-- ko if: factory_date == null -->
<td>
<span>TBA</span>
</td>
<!-- /ko -->
<!-- ko if: factory_date !== null -->
<td>
<div class="wrapper-wrapper">
<div class="btn btn-primary dateChanger">
<span data-bind="text: moment(factory_date.milestone_date).format('DD-MM-YYYY')"></span>
</div>
<div class="date-update-wrapper text-center">
<input type="text" data-bind="attr: {value: moment(factory_date.milestone_date).format('DD-MM-YYYY')}" class="form-control datetimepicker">
<a class="save-date btn btn-success" data-bind="click: function(){$root.saveDate(factory_date, $parent)}"><i class="fa fa-check"></i></a>
<a class="cancel-date btn btn-danger"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
<!-- /ko -->
<!-- /ko -->
</tr>
</tbody>
The part that made me aware I had an issue was this part:
data-bind="click: function(){$root.saveDate(factory_date, $parent)}"
I made a simple saveDate method, which was console.log(factory_date.milestone_date), and it returned the default JSON data, despite me editing it in the view (using the datepicker).
Knockout does not map children in lists to observables by default. There is a plugin called ko.mapping that can help you achieve what you are looking for.
You can set up a "children" mapping, or loop through your children, manually making them observable (In this case from JavaScript):
var mappedChildren = [];
var children = someObj.children();
for (var i = 0; i < children.length; i++) {
var mappedChild = ko.mapping.fromJS(children[i], mappingFunction);
mappedChildren.push(mappedChild);
}
someObj.children(mappedChildren);

Knockout observable used in text binding not updating ui

This has been driving me nuts for hours now. I think I have tried everything on the javascript side. In the javascript side I have this value: resultItem.standingNo which is a knockout observable. This one works fine, it updates like it should. I've used subscribers and everything I can think of to see that the value is updated and also tried the same to update other observables to use instead of this one.
What happens. The initial value is rendered in the html when I create the observable. But the html bit never receives the updates. I have tried with and without .extend({ notify: 'always' }), I have tried observable, computed and pureComputed, none updates the ui. But all gets the new value. I have other observables in the same class which update fine. But they are not located in the same place in the html.
resultItem.shortName just below is not an observable and doesn't need to be updated.
So my question is, is there some limitation with the html that prevents some observables from updating? Because I can't see any problem with my code between the two "...". Where those are there is code that get observable updates with no problems. Any ideas?
<!-- ko foreach: { data: resultLists, as: 'resultList' } -->
<!-- ko if: resultList.visible -->
...
<!-- ko foreach: { data: resultList.resultItems, as: 'resultItem', afterRender: $parent.renderedHandler } -->
<!-- Competitor Info Row Start-->
<tr class="rowDetail" data-bind="toggleCompetitorDetails: resultItem.competitorSelected">
<td class="selected-competitor-content" colspan="100%">
<div class="selected-competitor-container" >
<div class="selected-competitor-image">
<div class="img" data-bind="attr: { style: 'background-image: url(' + resultList.getRandomImage() + ');' }"></div>
<div class="competitor-dot competitor-star" data-bind="attr: {class: resultItem.isTopCompetitor()}, style: { backgroundColor: resultItem.color.toString(), color: resultItem.color.toString() }"></div>
</div>
DOES NOTE UPDATE --> <div class="selected-competitor-position" data-bind="text: resultItem.standingNo"></div> <-- DOES NOTE UPDATE
<div class="selected-competitor-info">
<p class="selected-competitor-name" data-bind="text: resultItem.shortName"></p>
<p class="selected-competitor-captain" data-bind="text: resultItem.name"></p>
</div>
</div>
</td>
</tr>
<!-- Competitor Info Row End -->
...
<!-- /ko -->
<!-- /ko --><!-- ResultList visible end -->
<!-- /ko --><!-- ResultLists foreach End -->

Can Knockout.js bindings apply to container tags AND descendants both?

Let me setup the question with a simple case.
I have an HTML table, the rows of which are controlled by an observableArray. It works great.
If the observableArray has zero elements in it however, I want a single row to say so. I tried this markup, which "kind of" works:
<tbody data-bind="if: $root.data.contacts().length == 0">
<tr>
<td>There are no contacts specified yet.</td>
</tr>
</tbody>
<tbody data-bind="foreach: $root.data.contacts">
SNIP - a tbody with the rows is here when elements > zero
</tbody>
When I say "kind of", I mean VISIBLY. It really does show up at zero elements and really does go away at > zero elements like what you would expect. However when you open the DOM inspector (dev tools) and look at the DOM in memory, you find that there are TWO tbody sections, not one. Now one tbody is always empty of course, but two tbody tags is not HTML5 correct, so this must be fixed this is not the desired markup.
Being a Knockout newbie, I tried to fix this problem with a virtual element:
<!-- ko if: $root.data.contacts().length == 0 -->
<tbody>
<tr>
<td>There are no contacts specified yet.</td>
</tr>
</tbody>
<!-- /ko -->
Unfortunately this doesn't work for our build process: we minify HTML prior to compression and comments get eliminated.
I was under the impression that KO bindings applied to the CONTAINER ELEMENT ITSELF as well as descendants, but this seems to not be so. Is there a way to tell KO to apply to container elements as well as children, or do I need to change the markup in some way OTHER THAN a virtual container?
Like you, my first choice would be virtual tags for an if binding. But since that's not an option, how about swappable templates?
var vm = {
contacts: ko.observableArray()
};
ko.applyBindings(vm);
setTimeout(function() {
vm.contacts(['One', 'Two', 'Three']);
}, 2500);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<template id="empty-body">
<tbody>
<tr>
<td>There are no contacts specified yet.</td>
</tr>
</tbody>
</template>
<template id="normal-body">
<tbody data-bind="foreach: contacts">
<tr>
<td data-bind="text:$data"></td>
</tr>
</tbody>
</template>
<table data-bind="template: contacts().length === 0 ? 'empty-body' : 'normal-body'"></table>
The Knockout-Repeat binding applies the binding to the element itself. It does so by using a node preprocessor to wrap elements with the repeat binding in virtual (comment-based) elements at run time.
var vm = {
contacts: ko.observableArray()
};
ko.applyBindings(vm);
setTimeout(function() {
vm.contacts(['One', 'Two', 'Three']);
}, 2500);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
<script src="https://rawgit.com/mbest/knockout-repeat/master/knockout-repeat.js"></script>
<table>
<tbody data-bind="repeat: !contacts().length && 1">
<tr>
<td>There are no contacts specified yet.</td>
</tr>
</tbody>
<tbody data-bind="repeat: contacts().length && 1" data-repeat-bind="foreach: contacts">
<tr>
<td data-bind="text:$data"></td>
</tr>
</tbody>
</table>

Knockout conditional binding (but not the native "if" way)

I have a case that look like this (excessively simplified):
<!-- ko if: readOnly() -->
<a href="url" data-bind="click: ToggleReadOnly()" />
<!-- /ko -->
<!-- ko ifnot: readOnly() -->
<a href="url" data-bind="visible: someObservable" />
<!-- /ko -->
Because of multiple other things around that would multiply the tests and duplicate a lot of code, I'd need to be able to do this in one line, something like:
<a href="url" data-bind="if: readOnly() { click: ToggleReadOnly() } else: { visible: someObservable }" />
Is there a way to do that ?
There are a couple of approaches you could take to this. Each with it's own strengths and weaknesses. but I will focus on using templates.
Create a template for each state where it is rendered in readonly mode or not. You'll only need to add to your model a function that decides which template to use.
<script type="text/html" id="template-readonly-link">
ReadOnly
</script>
<script type="text/html" id="template-readwrite-link">
ReadWrite
</script>
<!-- ko template: { name: selectTemplate } --><!-- /ko -->
function ViewModel() {
this.readOnly = ko.observable(true);
this.someObservable = ko.observable(true);
this.ToggleReadOnly = function (data, event) {
this.readOnly(!this.readOnly());
return false;
}.bind(this);
this.selectTemplate = function (data) {
return this.readOnly()
? 'template-readonly-link'
: 'template-readwrite-link';
}.bind(this);
}
fiddle
You can explore other approaches such as custom components, custom bindings, etc. But this may be the easiest to implement.

knockout.js doesn't fire click event when click description in loop

I have next model code
<!-- ko foreach: {data: userAdminView.viewRoles, as: 'rrole'} -->
<tr>
<td class="userRolesRoleTitle"><b data-bind="text: rrole.role.name"></b><br/><i data-bind="text: rrole.role.description"></i></td>
<td class="userRolesRoleGroups">
<!-- ko foreach: {data: rrole.role.groups, as: 'group'} -->
<div class="usersGroupElement" data-bind="html: group.viewName"></div>
<!-- /ko -->
<a class="btn emb green" data-bind="click: userAdminView.addNewGroup,visible:(rrole.role.isNewGroupAccessible) , attr: { value: rrole }"><i class="icon16 plus"></i>add</a>
</td>
</tr>
<!-- /ko -->
and model event
function userAdminView(user) {
//some code
self.addNewGroup = function(data, event){};
//some code
}
all working fine but except userAdminView.addNewGroup event, it never fired when described in loop.
Why does it happens ?
Thanks
change this part data-bind="click: userAdminView.addNewGroup"
into this data-bind="click: $parent.addNewGroup"
check this out custom-bindings-controlling-descendant-bindings
Bindings such as with and foreach create extra levels in the binding
context hierarchy. This means that their descendants can access data
at outer levels by using $parent, $parents, $root, or $parentContext.

Categories

Resources