Looping through data to display cards?

Is it possible to loop through an array data source in Framework, passing each element to a block in the Markdown file?

I have tabular data that I’d like to display in a nicer manner, ideally with a grid of stacked charts, each chart taking from the elements in order.

So far I haven’t figured out how to do this.

Any help or guidance (or just letting me know this isn’t possible) would be great!

Could I use something like Handlebars for this?

Maybe use FileAttachment to load a template from a file and pass to the template compiler, and then use DomParser to turn that into a DOM Document and pass that to display() ?

You could use htl.html; for example to display a line chart for each of three series:

```js echo
const series = [42, 22, 0.5].map((x) =>
  d3.cumsum(Array.from({length: 100}), d3.randomNormal.source(d3.randomLcg(x))())
);
```

```js echo
for (const data of series) {
  display(html`<div class="card">${Plot.lineY(data).plot()}</div>`);
}
```

Yeah I’ve tried this approach but it falls apart when I’m trying to do something more complex like a dynamic layout where the number of row depends on what is in the data. Think base object but then N number of child objects, and the markup has to change based on N.

The html literal template doesn’t work for dynamically generating that markup as well as the stuff between the tags.

It feels more and more like I need to dynamically construct a DOM document node by node…

Using Handlebars to construct a template-based system doesn’t work because I can’t dynamically construct a path to pass to FileAttachment, which I assume is because of the static site generation constraints.

So I have to create a component for every template.

Even when I do that, and I return a compiled template as a string, I try this:

display(
  document
    .createRange()
    .createContextualFragment(
      cardTemplate({ cardTitle: "Card Title" })
    ).firstChild
)

In my output it returns

[object Promise]

The createContextualFragment() method should return a DocumentFragment so I’m confused by this now.

Even if I remove the display() wrapper it’s the same thing.

If just do

cardTemplate({ cardTitle: "Card Title" })

It will properly output a string that contains all my escaped markup as a string literal, but of course that’s not what I want. So the template seems to be properly generated as a string but parsing it into a DOM document fragment doesn’t work.

(Sticking “await” in front of the display call also doesn’t do anything)

I suspect you might need

await cardTemplate({ cardTitle: "Card Title" })

Can you share your implementation of cardTemplate?

That totally worked, thank you! The cardTemplate component was indeed an async method because I have to await FileAttachment().

So in the spirt of sharing, for anyone else looking to use something like Handlebars to manage templates in Observable Framework, here’s how I’ve netted out.

I have a components/templates.js file that looks like this:

import Handlebars from "npm:handlebars"
import { FileAttachment } from "npm:@observablehq/stdlib"

function renderTemplate(contents, data) {
  const template = Handlebars.compile(contents)
  return document.createRange().createContextualFragment(template(data)).firstChild;
}

export async function cardTemplate(data) {
  const templateContents = await FileAttachment("../data/templates/app_card.hbs").text();
  return renderTemplate(templateContents, data);
}

export async function boxTemplate(data) {
  const templateContents = await FileAttachment("../data/templates/app_box.hbs").text();
  return renderTemplate(templateContents, data);
}

// an exported method for each of your other templates ....

As mentioned earlier in the thread, because of how FileAttachment works you can’t dynamically build the path to the template file (it will throw an error about the method needing a Literal parameter), so you do need a function for each template you have, as far as I can tell.

The renderTemplate method then compiles and returns the Handlebar template as a string, which is then turned into a DOM fragement and returned.

Your templates can probably be anywhere but I put them in a folder under data.

The app_card.hbs template in this case is super simple:

<h2>{{ cardTitle }}</h2>

Then in my Markdown file, I have this:


```js
import { cardTemplate, boxTemplate } from "./components/templates.js"
```

<div class="grid">
  <div class="card">${cardTemplate({ cardTitle: "Card Title" })}</div>
</div>

And that works! Obviously you can create much more complex templates here that include looping logic and more.

Hope this helps someone, I spent a good day figuring this all out.

1 Like

You may not be aware that htl.html accepts arrays. Here is an example that renders the penguins dataset into a table:

htl.html`<div class="mytable">
  <table>
    <tr>${Object.keys(penguins[0]).map(d => htl.html`<th>${d}`)}</tr>
    ${penguins.map(
      d => htl.html`<tr>${ Object.values(d).map(v => htl.html`<td>${v}`) }`
    )}
  </table>
  <style>
    .mytable { overflow: auto; width: fit-content; height: 300px; resize: vertical }
    .mytable table { margin: 0 8px }
    .mytable tr:first-child { position:sticky;top:0;background:#fff }
`

That is a very interesting approach, thank you…

I also hadn’t thought about nesting html`` template literals inside each other using the ${} syntax… that’s very cool…

In case you haven’t seen it yet, you can also define properties as objects (and even define them inline at the same time):

htl.html`<button class="my-button" ${{
  style: {background: "lightgrey"},
  onclick() { this.style.background = "lime" }
}}>Click me`

With a little workaround you can even define tags dynamically while still benefitting from htl’s escaping:

tag = (name, content) => htl.html({raw: [`<${name}>`, ``]}, content)