Search code examples
htmlgoogle-closure-templatessoy-templates

How can I render a tr with soy templates?


I've got this soy template

{template .myRowTemplate}
  <tr><td>Hello</td></tr>
{/template}

and I want to do something like

var myTable = goog.dom.createElement("table");
goog.dom.appendChild(myTable, goog.soy.renderAsFragment(mytemplates.myRowTemplate));
goog.dom.appendChild(myTable, goog.soy.renderAsFragment(mytemplates.myRowTemplate));

But that causes

Uncaught goog.asserts.AssertionError
Assertion failed: This template starts with a <tr>,
which cannot be a child of a <div>, as required by soy internals. 
Consider using goog.soy.renderElement instead.
Template output: <tr><td>Hello</td></tr>

What's the best way to do this?


Solution

  • Why it fails

    Right, the documentation of renderAsFragment is a bit confusing; it reads:

    Renders a Soy template into a single node or a document fragment. If the rendered HTML string represents a single node, then that node is returned

    However, the (simplified) implementation of renderAsFragment is:

      var output = template(opt_templateData);
      var html = goog.soy.ensureTemplateOutputHtml_(output);
      goog.soy.assertFirstTagValid_(html); // This is your failure
      var safeHtml = output.toSafeHtml();
      return dom.safeHtmlToNode(safeHtml);
    

    So why do the closure author assert that the first tag is not <tr>?

    That's because, internally, safeHtmlToNode places safeHtml in a temporary div, before deciding if it should return the div wrappper (general case) or the only child (if the rendered HTML represents only one Node). Once again simplified, the code of safeHtmlToNode is:

      var tempDiv = goog.dom.createElement_(doc, goog.dom.TagName.DIV);
      goog.dom.safe.setInnerHtml(tempDiv, html);
      if (tempDiv.childNodes.length == 1) {
        return tempDiv.removeChild(tempDiv.firstChild);
      } else {
        var fragment = doc.createDocumentFragment();
        while (tempDiv.firstChild) {
          fragment.appendChild(tempDiv.firstChild);
        }
        return fragment;
      }
    

    renderAsElement won't work either

    And I'm unsure what you are asking for fragments, but unfortunately goog.soy.renderAsElement() will behave the same because it also uses a temporary div to render the DOM.

    renderElement cannot loop

    The error message suggests goog.soy.renderElement, but that will only work if your table has one row, since it replaces content, and doesn't append children nodes.

    Recommended approach

    So usually, we do the for loop in the template:

      {template .myTable}
        <table>
        {foreach $person in $data.persons}
          <tr><td>Hello {$person.name}</td></tr>
        {/foreach}
        </table>
      {/template}
    

    Of course, we can keep the simple template you have for one row and call it from the larger template.