observablehq / stdlib

The Observable standard library.
https://observablehq.com/@observablehq/standard-library
ISC License
966 stars 83 forks source link

Structured HTML templates! #6

Closed mbostock closed 6 years ago

mbostock commented 6 years ago

TODO:

image

mbostock commented 6 years ago

This also changes an html tagged template literal that contains multiple root nodes to return a DocumentFragment rather than a DIV element.

mbostock commented 6 years ago

Figured out some nice improvements! So before, you could embed expressions in an html tagged template literal, and those expressions would simply get concatenated into the source before rendering.

html`My name is ${"<b>Bob</b>"}.`

That’s still true in the new implementation. But what I’ve changed is that it’s now also true if the string occurs within an array expression:

html`My name is ${["<b>Bob</b>"]}.`

This is good for consistency. (The earlier implementation of this PR would create a Text node for the array case, which would mean you’d see “\<b>Bob\</b>” in the output!) It also means that in the simple cases, you can embed arrays of HTML-in-HTML (or Markdown-in-Markdown, etc.). So this:

html`<table>
  <thead>
    <tr><th>Name</th><th>RGB</th></tr>
  </thead>
  <tbody>${["red", "green", "blue"].map(color =>`
    <tr><td>${color}</td><td>${d3.rgb(color)}</td></tr>`)}
  </tbody>
</table>`

Produces the same result as this:

html`<table>
  <thead>
    <tr><th>Name</th><th>RGB</th></tr>
  </thead>
  <tbody>${["red", "green", "blue"].map(color =>`
    <tr><td>${color}</td><td>${d3.rgb(color)}</td></tr>`).join("")}
  </tbody>
</table>`

And the same result as this:

html`<table>
  <thead>
    <tr><th>Name</th><th>RGB</th></tr>
  </thead>
  <tbody>${["red", "green", "blue"].map(color => html`
    <tr><td>${color}</td><td>${d3.rgb(color)}</td></tr>`)}
  </tbody>
</table>`

The last approach is probably a little slower since it renders the TR elements separately, and then embeds them into the outer HTML using a comment placeholder. But the difference probably isn’t noticeable in most cases.

But, you would need to use the html tagged template literal in the inner loop if you, say, wanted to embed a TeX expression within:

html`<table>
  <thead>
    <tr><th>Name</th><th>RGB</th></tr>
  </thead>
  <tbody>${["red", "green", "blue"].map(color => {
    const {r, g, b} = d3.rgb(color);
    return html`<tr>
      <td>${color}</td>
      <td>${tex`\\langle${r},${g},${b}\\rangle`}</td>
    </tr>`;
  })}
  </tbody>
</table>`

image

That’s because if you didn’t have an html tagged template literal on the inner loop, then it’d be a normal template literal, and so the generated KaTeX would be string-coerced resulting in “[object HTMLSpanElement]”. You could use the outerHTML trick, of course, but being able to embed nodes in HTML template literal expressions is faster and more expressive.

So, that’s cool. 😁

mbostock commented 6 years ago

I’ve also been thinking about whether we could make this more JSX-y: to strip whitespace, to escape text and attribute names, etc. But that starts to diverge pretty far from the original behavior of the html tagged template literal, so I think it’s probably better not to. Also, without parsing the HTML ourselves, it’d be pretty difficult to know when something’s an attribute and when it’s text, and how to allow things like dynamic attribute or tag names. lit-html uses a regular expression to determine if the placeholder is in an attribute context but that seems a little frightening.

jashkenas commented 6 years ago

Still looks great — I'm looking forward to playing around with the dynamic SVG generation after this lands...

mbostock commented 6 years ago

image