github observablehq/framework v1.9.0

4 months ago

JSX + React ⚛

Framework now supports JSX rendered with React, 🎉 providing a powerful new mechanism for implementing reusable stateful components. You can both write fenced code blocks (```jsx) and import JSX modules (.jsx). For example, to define a Greeting component that accepts a subject prop:

function Greeting({subject}) {
  return <div>Hello, <b>{subject}</b>!</div>
}

You can then call the built-in display function to render JSX content:

display(<Greeting subject="JSX" />);

The display function in JSX code blocks uses React’s createRoot to render the specified contents into the DOM. When you call display again later, it applies React’s reconciliation algorithm to efficiently update the DOM.

Naturally, you can also use React’s built-in hooks such as useState, useEffect, and useRef. For example, here is a Counter component whose count you can increment by clicking a button:

function Counter() {
  const [count, setCount] = React.useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      You clicked {count} times
    </button>
  );
}

React and ReactDOM are now available by default in Markdown; if you prefer, you can also import them explicitly from npm:react or npm:react-dom.

Span-less inline expression rendering

Framework’s inline expressions allow you to interpolate dynamic content anywhere on the page using ${…}. Previously, interpolated content was wrapped in a SPAN element; now Framework uses a comment (e.g., <!--:85902a01:-->) to track where to insert displayed content. This enables a variety of new use cases. For example, you can now use inline expressions to populate the contents of a grid,

<div class="grid grid-cols-4">
  ${d3.range(4).map((i) => html`<div class="card">Hello ${i}</div>`)}
</div>

… or to generate table rows,

<table>
  <thead><th>Index</th></thead>
  <tbody>${d3.range(4).map((i) => html`<tr><td>${i}</td></tr>`)}</tbody>
</table>

… or even within SVG elements!

<svg width="640" height="120">
  <text x="20" y="20">My favorite number is ${Math.random()}</text>
</svg>

As part of this change, we also fixed several edge cases in incremental updates during preview. (Previously we only tracked changes to top-level elements, but now we also track changes to top-level text nodes and comments.)

More robust inline expression parsing

We’ve also improved how we parse inline expressions: Framework now uses the HTML5 tokenizer algorithm (adopted from Hypertext Literal) to determine context, allowing Framework to ignore inline expressions in unsupported contexts such as attributes, coments, and raw text. This means that if you comment out a chunk of Markdown that includes an inline expression, Framework no longer runs the code (and no longer generates an error)!

<!-- ${"this code doesn’t run"} -->

In the future, we’d like to support interpolation into attributes <a href=${link}> and possibly raw text <textarea>${1 + 2}</textarea>; please upvote #32 if you’re interested in this feature.

Framework now also uses Acorn’s tokenizer to determine when an inline expression ends. Previously, Framework counted quotes and curly braces; by adopting Acorn’s tokenizer, Framework can correctly parse (and skip) JavaScript comments within inline expressions. For example, the following inline expression evaluates to 3:

${1 + /* } */ 2}

Lastly, Framework now correctly handles backslash escaping of inline expressions within HTML blocks. To escape an inline expression, resulting in the literal text ${1 + 2}, place a backslash \ before either the dollar sign $ or left curly brace {:

<pre>\${1 + 2}</pre>

To instead show a literal backslash prior to the result of the inline expression \3, use two backslashes \\:

<pre>\\${1 + 2}</pre>

Or, to show a literal backslash followed by literal text \${1 + 2}, use three backslashes:

<pre>\\\${1 + 2}</pre>

Together, these rendering and parsing improvements make Framework’s inline expressions feel more robust — they just work.

Other improvements

The FileAttachment function now returns a canonical instance: calling FileAttachment with the same name will return the same object, allowing easier comparison.

FileAttachment("foo.csv") === FileAttachment("foo.csv") // true

The Mod-Enter keyboard shortcut now opens search results in a new tab, making it easier to open multiple search results for the same query.

Sample datasets, such as penguins and miserables, are now self-hosted from npm:@observablehq/sample-datasets, allowing you to work with sample data while offline.

The deploy command now supports the --deploy-config command-line argument to specify an alternative path to the deploy.json configuration file. (By default, this file lives in .observablehq/deploy.json within the source root.) The deploy command now prints a better error message when attempting to deploy without authentication from a non-interactive terminal.

Examples improvements

We’ve added a variety of new technique examples:

The examples are now searchable from the Framework documentation. The example config files have also been greatly simplified by removing shared boilerplate. Lastly, descenders in the hero text of the default template’s home page are no longer clipped.

Thanks @martinswan for contributing to the docs!

Full Changelog: v1.8.0...v1.9.0

Don't miss a new framework release

NewReleases is sending notifications on new releases.