Saturday, 30 June 2012

HTML Comments in Visualforce Pages

After seeing the presentation from Wes Nolte at Cloudstock London, I've been playing around with knockout.js in my spare time over the last couple of weeks and am very impressed.  I've been building some apps using JQuery Mobile and an area of concern has been the way that I've been striping HTML through javascript functions in order to update page sections with the latest data - using knockout.js I don't have to worry about this any more as I can bind the data and respond to user interaction via an almost Apex controller-like javascript object.

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.



6 comments:

  1. "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."

    Check the layout="none" attribute on apex:component, new in v25. It ensures the component is not wrapped in a div or span.

    http://www.salesforce.com/us/developer/docs/pages/Content/pages_compref_component.htm

    ReplyDelete
  2. Bob, great post! I was trying to figure out how to do this with visualforce. Idea voted up!

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Haha, clever workaround. Also, +10 vote.

    ReplyDelete
  5. Just voted your idea up Bob! Thanks again for coming up with this workaround. I've detailed a blog post on how to take your workaround and wrap it in a Visualforce Component at http://www.jonathanbroquist.com/blog/2013/02/using-knockout-js-without-a-container-elemen/

    ReplyDelete
  6. Bob..I think IE comments are now retained..

    https://www.salesforce.com/us/developer/docs/pages/Content/pages_html_features_ie_conditional_comments.htm

    ReplyDelete