Svelte components in Quarto (dynamic)

Author

James Goldie

Tip

This work has been turned into a Quarto extension: Sverto!

Background

Okay, so we’re using this blog post on compiling Svelte components as Web Components, along with with this SO answer on making Svelte components as browser bundles, to see if we can write Svelte components that can be dropped into plain HTML, or even Quarto documents.

The end goal is to have something that can be instantiated in a Quarto document and then dynamically update its props (using reactive OJS code) without destroying the component - for example, to have a chart that transitions elements as the incoming data changes.

Component tag name

I’ve changed the tag attribute at the top of Circles.svelte from "animated-circles" to {null} in order to stop an error that crops up. it fixes that error here but prevents the component from being used in test-quarto-static.qmd with the static HTML syntax.

Dynamic HTML import

In this second approach, we’ll import the module in JavaScript, create an instance, and finally update its props to pass new data in. The hope is that we can cirumvent OJS’s reactivity when creating the element, allowing it to transition its child elements (like bars or circles representing data) using its own reactivity. This notebook uses the same pattern to integrate DeckGL with Observable.

First, let’s import the JavaScript. Since OJS blocks are naturally Promises, we should be able to use either require() or await import().

CirclesOne = require("/web-component-tut/public/build/Circles.js");

How’d that go? No? What about await import()?

Circles = import("/web-component-tut/public/build/Circles.js");

Better, I think!

Circles

And the default constructor?

Circles.default

Demo: animated Svelte component

Let’s create a CirclesTwo. I’ve create a target div, #mycircles, on the margin for it to load into first:

myCircles = new Circles.default({
  target: document.querySelector("#mycircles"),
  props: {
    // data: "10|5,30|15,50|25,70|17,90|8"
    data: [
      {x: 10, r: 5},
      {x: 30, r: 15},
      {x: 50, r: 25},
      {x: 70, r: 17},
      {x: 90, r: 8}
    ]
  }
});

Great! A little bit convoluted, but it’s working. Now we want to push some new data to it - the equivalent of setProps() in DeckGL.

I can’t see anything that immediately stands out in myCircles, but inspecting the compiled code in Circles.js reveals that the class returned includes get data and set data.

Maybe I can just write to the prop directly? Let’s set up some options to switch between reactively:

viewof selectedDataset = Inputs.select(
  new Map([
    ["Dataset A", [
      {x: 10, r: 5},
      {x: 30, r: 15},
      {x: 50, r: 25},
      {x: 70, r: 17},
      {x: 90, r: 8}
    ]],
    ["Dataset B", [
      {x: 10, r: 25},
      {x: 30, r: 5},
      {x: 50, r: 5}
    ]],
    ["Dataset C", [
      {x: 5, r: 12},
      {x: 25, r: 5},
      {x: 32, r: 8},
      {x: 45, r: 21},
      {x: 70, r: 5}
    ]]
  ]), {
    label: "Selected dataset"
  });

And now we’ll update the Svelte component with it:

myCircles.data = selectedDataset;
Inputs.table(selectedDataset)

(These are keyed by the x value, not an independent index, so when you choose dataset C, most of the circles are treated as new elements that exit/enter, not existing ones that transition)

Note

I initially got an error:

“Error: : Props cannot be set directly on the component instance unless compiling with ‘accessors: true’ or ‘’”

Adding accessors to the svelte:options line at the start of Circles.svelte fixed this right up!