Skip to content

Commit

Permalink
fix #1327; document resize
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed May 14, 2024
1 parent 58acf8b commit 45f09c1
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 25 deletions.
66 changes: 51 additions & 15 deletions docs/javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ A program block looks like this (note the semicolon):
const foo = 1 + 2;
```

A program block doesn’t display anything by default, but you can call [`display`](#explicit-display) to display something.
A program block doesn’t display anything by default, but you can call [`display`](#display(value)) to display something.

JavaScript blocks do not show their code by default. If you want to show the code, use the `echo` directive:

Expand Down Expand Up @@ -129,7 +129,7 @@ Expressions cannot declare top-level reactive variables. To declare a variable,

## Explicit display

The built-in `display` function displays the specified value.
The built-in [`display` function](#display(value)) displays the specified value.

```js echo
const x = Math.random();
Expand Down Expand Up @@ -202,7 +202,7 @@ Inputs.button("Click me", {value: 0, reduce: (i) => displayThere(++i)})

## Implicit display

JavaScript expression fenced code blocks are implicitly wrapped with a call to [`display`](#explicit-display). For example, this arithmetic expression displays implicitly:
JavaScript expression fenced code blocks are implicitly wrapped with a call to [`display`](#display(value)). For example, this arithmetic expression displays implicitly:

```js echo
1 + 2 // implicit display
Expand Down Expand Up @@ -238,23 +238,17 @@ Implicit display also implicitly awaits promises.

## Responsive display

In Markdown, the built-in `width` reactive variable represents the current width of the main element. This variable is implemented by [`Generators.width`](./lib/generators#width(element)) and backed by a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver). The reactive width can be a handy thing to pass, say, as the **width** option to [Observable Plot](./lib/plot).
In Markdown, the built-in [`width` reactive variable](#width) represents the current width of the main element. This variable is implemented by [`Generators.width`](./lib/generators#width(element)) and backed by a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver). The reactive width can be a handy thing to pass, say, as the **width** option to [Observable Plot](./lib/plot).

```html echo
The current width is ${width}.
```

```js
import {resize} from "npm:@observablehq/stdlib";
```js echo
Plot.barX([9, 4, 8, 1, 11, 3, 4, 2, 7, 5]).plot({width})
```

For more control, or in a [grid](./css/grid) where you want to respond to either width or height changing, use the built-in `resize` helper. This takes a render function which is called whenever the width or height [changes](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver), and the element returned by the render function is inserted into the DOM.
For non-top-level elements, as when rendering content within an inline expression, use the built-in [`resize` function](#resize(render)) instead. This takes a _render_ function which is called whenever the width or height [changes](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver), and the element returned by the render function is inserted into the DOM.

```html echo
<div class="grid grid-cols-4">
<div class="card">
${resize((width) => `This card is ${width}px wide.`)}
</div>
<div class="card">
${resize((width) => Plot.barX([9, 4, 8, 1, 11, 3, 4, 2, 7, 5]).plot({width}))}
</div>
```

Expand All @@ -272,3 +266,45 @@ If your container defines a height, such as `240px` in the example below, then y
```

<div class="tip">If you are using <code>resize</code> with both <code>width</code> and <code>height</code> and see nothing rendered, it may be because your parent container does not have its own height specified. When both arguments are used, the rendered element is implicitly <code>position: absolute</code> to avoid affecting the size of its parent and causing a feedback loop.</div>

## display(*value*)

Displays the specified *value* in the current context, returning *value*. If *value* is a DOM element or node, it is inserted directly into the page. Otherwise, if the current context is a fenced code block, inspects the specified *value*; or, if the current context is an inline expression, coerces the specified *value* to a string and displays it as text.

```js echo
display(1 + 2);
```

See [Explicit display](#explicit-display) for more.

## resize(*render*)

Creates and returns a DIV element to contain responsive content; then, using a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) to observe changes to the returned element’s size, calls the specified *render* function with the new width and height whenever the size changes. The element returned by the *render* function is inserted into the DIV element, replacing any previously-rendered content. This is useful for responsive charts.

```js echo
resize((width) => `I am ${width} pixels wide.`)
```

If the *render* function returns a promise, the promise is implicitly awaited. If the resulting value is null, the DIV element is cleared; otherwise, if the resulting value is not a DOM element, it is coerced to a string and displayed as text.

See [Responsive display](#responsive-display) for more.

## width

The current width of the main element in pixels as a reactive variable. A fenced code block or inline expression that references `width` will re-run whenever the width of the main element changes, such as when the window is resized; often used for responsive charts.

```js echo
width
```

See [`Generators.width`](./lib/generators#width(element)) for implementation.

## now

The current time in milliseconds since Unix epoch as a reactive variable. A fenced code block or inline expression that references `now` will run continuously; often used for simple animations.

```js echo
now
```

See [`Generators.now`](./lib/generators#now()) for implementation.
16 changes: 9 additions & 7 deletions docs/lib/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Generators} from "npm:@observablehq/stdlib";

## input(*element*)

Returns an async generator that yields whenever the given *element* emits an *input* event, with the given *element*’s current value. (It’s a bit fancier than that because we special-case a few element types.) The built-in [`view` function](<../reactivity#inputs>) uses this.
[Source](https://github.com/observablehq/framework/blob/main/src/client/stdlib/generators/input.js) · Returns an async generator that yields whenever the given *element* emits an *input* event, with the given *element*’s current value. (It’s a bit fancier than that because we special-case a few element types.) The built-in [`view` function](<../reactivity#inputs>) uses this.

```js echo
const nameInput = display(document.createElement("input"));
Expand All @@ -19,9 +19,9 @@ const name = Generators.input(nameInput);
name
```

## observe(*change*)
## observe(*initialize*)

Returns an async generator that yields whenever the callback function *change* is called, with the value passed.
[Source](https://github.com/observablehq/framework/blob/main/src/client/stdlib/generators/observe.js) · Returns an async generator that immediately invokes the specified *initialize* function, being passed a *change* callback function, and yields the passed value whenever *change* is called. The *initialize* function may optionally return a *dispose* function that will be called when the generator is terminated.

```js echo
const hash = Generators.observe((change) => {
Expand All @@ -37,7 +37,9 @@ hash

## queue(*change*)

Returns an async generator that yields whenever the callback function *change* is called, with the value passed. This is identical to Generators.observe, except that if *change* is called multiple times before the consumer has a chance to process the yielded result, values will not be dropped; use this if you require that the consumer not miss a yielded value.
[Source](https://github.com/observablehq/framework/blob/main/src/client/stdlib/generators/queue.js) · Returns an async generator that immediately invokes the specified *initialize* function, being passed a *change* callback function, and yields the passed value whenever *change* is called. The *initialize* function may optionally return a *dispose* function that will be called when the generator is terminated.

This is identical to `Generators.observe` except that if *change* is called multiple times before the consumer has a chance to process the yielded result, values will not be dropped; use this if you require that the consumer not miss a yielded value.

```js run=false
const hash = Generators.queue((change) => {
Expand All @@ -53,7 +55,7 @@ hash

## now()

Returns a generator that repeatedly yields `Date.now()`, forever. This generator is available by default as `now` in Markdown.
[Source](https://github.com/observablehq/framework/blob/main/src/client/stdlib/generators/now.js) · Returns a generator that repeatedly yields `Date.now()`, forever. This generator is available by default as `now` in Markdown.

```js run=false
const now = Generators.now();
Expand All @@ -65,7 +67,7 @@ now

## width(*element*)

Returns an async generator that yields the width of the given target *element*. Using a [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver), the generator will yield whenever the width of the *element* changes. This generator for the `main` element is available by default as `width` in Markdown.
[Source](https://github.com/observablehq/framework/blob/main/src/client/stdlib/generators/width.js) · Returns an async generator that yields the width of the given target *element*. Using a [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver), the generator will yield whenever the width of the *element* changes. This generator for the `main` element is available by default as `width` in Markdown.

```js run=false
const width = Generators.width(document.querySelector("main"));
Expand All @@ -77,7 +79,7 @@ width

## dark() <a href="https://github.com/observablehq/framework/releases/tag/v1.3.0" class="observablehq-version-badge" data-version="^1.3.0" title="Added in 1.3.0"></a>

Returns an async generator that yields a boolean indicating whether the page is currently displayed with a dark [color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme).
[Source](https://github.com/observablehq/framework/blob/main/src/client/stdlib/generators/dark.js) · Returns an async generator that yields a boolean indicating whether the page is currently displayed with a dark [color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme).

If the page supports both light and dark mode (as with the [default theme](../themes)), the value reflects the user’s [preferred color scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme). The generator will yield a new value if the preferred color changes — as when the user changes their system settings, or if the user’s system adapts automatically to the diurnal cycle — allowing you to update the display as needed without requiring a page reload.

Expand Down
6 changes: 3 additions & 3 deletions src/client/stdlib/resize.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// TODO Automatically disconnect the observer when the returned DIV is detached.
export function resize(
render: (width: number, height: number) => Node | null | Promise<Node | null>,
render: (width: number, height: number) => Node | string | null | Promise<Node | string | null>,
invalidation?: Promise<void>
): Node {
const div = document.createElement("div");
Expand All @@ -24,6 +24,6 @@ export function resize(
return div;
}

function isElement(node: Node): node is HTMLElement {
return node.nodeType === 1;
function isElement(node: Node | string): node is HTMLElement {
return typeof node === "object" && node.nodeType === 1;
}

0 comments on commit 45f09c1

Please sign in to comment.