One of the feature of knockout.js is the ability to iterate an array of data via a foreach. With HTML containers that naturally contain a repeating body (e.g. TBODY or UL) the data can be bound directly to that element. However, if you have a block of markup that you want to repeat without a container (in my case it was simply a DIV element per entry in the array), you make use of HTML comments to wrap the body. An example of this is shown below, where there is a single static list item and the rest are generated from an array:
<ul> <li><strong>Days of week:</strong></li> <!-- ko foreach: daysOfWeek --> <li> <span data-bind="text: $data"></span> </li> <!-- /ko --> </ul> <script type="text/javascript"> function viewModel() { var self = this; self.daysOfWeek = ko.observableArray([ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ]); }; ko.applyBindings(new viewModel()); </script>
In this case the <!-- ko foreach: daysOfWeek --> comment indicates that everything between it and the <!-- /ko --> end comment should be repeated for each element in the daysOfWeek array of Strings, and the <span> inside each list element should be populated with the array element via the data-bind="text: $data" attribute.
After saving this markup in my Visualforce page, I viewed the page to be disappointed by the sight of a single list item with the text 'null' in the embedded span. As I was green to this framework, my initial assumption was that I'd done something wrong. Observable elements need to be accessed as a function rather than an attribute, so maybe I needed some additional brackets in there? That just made things worse. I then added in some debug to output the size of the array in case I'd made a mistake there, and that showed that there were 7 elements as expected. To check that there wasn't an issue with the javascript file I'd downloaded, I rewrote it to use the UL element as a container and that worked fine. Some googling showed that nobody else was having this problem, so it was clearly something that I was doing. After some more head scratching and unsuccessful tweaks a synapse fired and I remembered having a similar issue when attempting to use CSS conditional comments for IE. These comments are interpreted by IE and ignored by all other browsers, so allow specific code or includes to be executed for the IE browser:
<!--[if IE 6]> Special instructions for IE 6 here <![endif]-->
I couldn't remember the exact issue, but I'd ended up writing a controller that inspected the USER-AGENT header as I couldn't make it work in the page. Once I'd found the notes it came flooding back - Visualforce removes HTML comments. Checking the source of my rendered page confirmed this - my bounding comments were nowhere to be seen. Moving the logic to the controller wasn't an option here, so I started looking for a workaround.
My first thought was to place the comments inside outputtext tags with the escape attribute set to false:
<apex:outputText escape="false" value="< -- ko foreach: daysOfWeek -->" />
While this was successful in outputting an HTML comment, the body was replaced with asterisks, hardly helpful:
<-- ********************** -->
I really don't understand why this happens - either leave the comment alone or remove it. Mangling helps nobody and adds unnecessary characters to the page.
After a bit more experimentation I came up with the following notation:
<apex:outputText value="<" escape="false"/>!-- ko foreach: daysOfWeek --<apex:outputText value=">" escape="false"/>
It looks pretty ugly but does the trick. By the way, if you are thinking that this could be made more readable using a custom component, I thought the same, but custom components get wrapped in a bonus <span> element, which broke things.
Upon changing my example markup to:
<ul> <li><strong>Days of week:</strong></li> <apex:outputText value="<" escape="false"/>!-- ko foreach: daysOfWeek --<apex:outputText value=">" escape="false"/> <li> <span data-bind="text: $data"></span> </li> <apex:outputText value="<" escape="false"/>!-- /ko --<apex:outputText value=">" escape="false"/> </ul> <script type="text/javascript"> function viewModel() { var self = this; self.daysOfWeek = ko.observableArray([ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ]); }; ko.applyBindings(new viewModel()); </script>
Everything started working as expected and my faith in knockout.js was restored. If you agree that HTML comments should be left in the resulting page, please vote up my idea.
Follow @bob_buzzard