How can I render a tr with soy templates?

1.7k Views Asked by At

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?

1

There are 1 best solutions below

1
On BEST ANSWER

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.