Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
357a7f9
Add scoped data groups
titouanmathis Jul 15, 2026
029cb32
Remove destroyed scoped values
titouanmathis Jul 15, 2026
7d58c48
Protect scoped data snapshots
titouanmathis Jul 15, 2026
7f2fe4f
Add virtual data bindings
titouanmathis Jul 15, 2026
b798bb1
Resolve acronym property bindings
titouanmathis Jul 15, 2026
27a8fbb
Add data mutation helpers
titouanmathis Jul 15, 2026
ad143cd
Guard unsupported toggle targets
titouanmathis Jul 15, 2026
2931c57
Merge pull request #513 from studiometa/feature/data-bindings
titouanmathis Jul 16, 2026
338aa69
Merge virtual data bindings
titouanmathis Jul 16, 2026
16df6b0
Merge pull request #514 from studiometa/feature/data-mutations
titouanmathis Jul 16, 2026
20e06fc
Improve DataScope documentation
titouanmathis Jul 16, 2026
391253c
Fix scoped source cleanup
titouanmathis Jul 16, 2026
ae1626f
Prune disconnected scoped sources
titouanmathis Jul 16, 2026
b3c709b
Fix virtual binding mutations
titouanmathis Jul 16, 2026
15ded99
Fix scoped input source tracking
titouanmathis Jul 16, 2026
4327123
Notify scoped value removal
titouanmathis Jul 16, 2026
1701ae4
Preserve mutation value types
titouanmathis Jul 16, 2026
4e00214
Guard consumer mutation helpers
titouanmathis Jul 16, 2026
0708054
Add DataScope tabs and accordion demos
titouanmathis Jul 16, 2026
34a6622
Clear nullish binding values
titouanmathis Jul 16, 2026
03b0c50
Recompute multiple scoped values
titouanmathis Jul 16, 2026
5711d5a
Broadcast scoped value changes
titouanmathis Jul 16, 2026
062a09c
Track mirrored data models
titouanmathis Jul 16, 2026
0c3cc91
Refactor Data reactivity with signals
titouanmathis Jul 16, 2026
41f13ef
Share Data channels globally
titouanmathis Jul 16, 2026
8dc5c45
Merge pull request #515 from studiometa/feature/data-signals
titouanmathis Jul 16, 2026
4eb7a78
Update Data component examples
titouanmathis Jul 17, 2026
c4b501b
Remove DataScope target isolation from Action
titouanmathis Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/docs/components/DataBind/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ title: DataBind, DataModel, DataEffect and DataComputed examples

## Immediate propagation

In the following example, we use the `data-option-immediate` attribute to enable the [`immediate` option](./js-api.md#immediate) on the first `<input>`, in order to set the value of the second `<input>` on mount.
In the following example, the first [`DataModel`](../DataModel/index.md) uses the [`immediate` option](./js-api.md#immediate) to hydrate the scoped `text` key on mount. The second model mirrors the same native `name`, and the keyed `DataBind` renders their shared value.

<llm-exclude>
<PreviewPlayground
Expand Down
70 changes: 61 additions & 9 deletions packages/docs/components/DataBind/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ badges: [JS]

Use the `DataBind` to create a one-way binding of a property of the targeted DOM element. This component should be used with the [`DataModel` component](../DataModel/index.md), which handles two-way bindings.

The related [`DateComputed`](../DataComputed/index.md) and [`DataEffect`](../DataEffect/index.md) components can also be used for computed values and side effects respectively.
The related [`DataComputed`](../DataComputed/index.md) and [`DataEffect`](../DataEffect/index.md) components can also be used for computed values and side effects respectively.

## Table of content

Expand All @@ -23,24 +23,76 @@ Import the components in your main app and use the [`DataModel` component](../Da

```js [app.js] twoslash
import { registerComponent } from '@studiometa/js-toolkit';
import { DataBind } from '@studiometa/ui';
import { Action, DataBind, DataModel, DataScope } from '@studiometa/ui';

registerComponent(DataScope);
registerComponent(DataBind);
registerComponent(DataModel);
registerComponent(Action);
```

```html [index.html]
<!-- Bind "textContent" to the "text" group -->
<div data-component="DataBind" data-option-group="text"></div>
<div data-component="DataScope" data-option-group="message">
<!-- Hydrate the "text" key from the input's native name. -->
<input name="text" value="Hello world" data-component="DataModel" data-option-immediate />

<!-- Bind "value" to the "text" group -->
<input type="text" data-component="DataModel" data-option-group="text" />

<!-- Bind "value" to the "text" group and sync it on mount -->
<input type="text" data-component="DataModel" data-option-group="text" data-option-immediate />
<!-- Render only updates published for the "text" key. -->
<output data-component="DataBind" data-option-key="text">Hello world</output>
</div>
```

:::

### Multiple virtual bindings

Use virtual `data-bind:*` attributes to update several parts of an element from the same value:

| Syntax | Behavior |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `data-bind:prop.<name>` | Assigns the DOM property. |
| `data-bind:attr.<name>` | Removes the attribute for `false`, `null`, or `undefined`; writes an empty attribute for `true`; otherwise writes the stringified value. |
| `data-bind:class.<name>` | Toggles the class according to the result's boolean value. |
| `data-bind:style.<name>` | Clears the style for `false`, `null`, or `undefined`; otherwise writes the stringified value. |
| `data-bind:text` | Assigns `textContent`. |

A non-empty attribute value is a JavaScript expression with access to `value`, `target`, and `$data`. An empty attribute passes through the current value. When an element has one or more virtual bindings, they replace the default single `textContent` or property update. Bindings are read when first used; changing their attributes afterward is not supported.

Use kebab-case for camel-cased DOM properties because HTML attribute names are case-insensitive, for example `data-bind:prop.tab-index` targets `tabIndex`.

For ARIA attributes, explicitly stringify booleans when `"false"` must remain present, for example `data-bind:attr.aria-selected="String(value === 'overview')"`.

The following disclosure keeps its button label while updating its class, ARIA state, and panel visibility from one scoped value. The `DataModel` uses its `value` property to hydrate the initial state; virtual bindings then retain the reactive value without replacing the label.

```html
<div data-component="DataScope" data-option-group="disclosure">
<button
type="button"
value="closed"
aria-controls="details"
aria-expanded="false"
data-component="Action DataModel"
data-option-key="state"
data-option-prop="value"
data-option-immediate
data-on:click="DataModel.toggle('open', 'closed')"
data-bind:class.is-active="value === 'open'"
data-bind:attr.aria-expanded="String(value === 'open')">
Details
</button>

<section
id="details"
hidden
data-component="DataBind"
data-option-key="state"
data-bind:attr.hidden="value !== 'open'">
Disclosure content
</section>
</div>
```

Expression errors are reported without interrupting updates to the other bindings, matching `DataComputed` and `DataEffect` behavior.

### Advanced usage with computed and effects

The whole family of `Data...` components can be used to create reactivity in your HTML with only a few `data-...` attributes.
Expand Down
93 changes: 83 additions & 10 deletions packages/docs/components/DataBind/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ The `DataBind` component can be used to keep a value in sync between multiple DO
- Type: `string`
- Default: `'textContent'`

The default value for the `prop` options depends on the type of the targeted element. If the element is an input, a textearea or a select, the default prop will be one of the following:
The default value for the `prop` option depends on the type of the targeted element. If the element is an input, a textarea, or a select, the default prop will be one of the following:

- `valueAsDate` for `<input type="date">`
- `valueAsNumber` from `<input type="number">`
- `valueAsNumber` for `<input type="number">`
- `value` for all other inputs
- for `<select>` elements, the prop option is not used and the value will always be the selected option(s)

Expand All @@ -28,22 +28,29 @@ If the option is explicitly set with the `data-option-prop` attribute, it will o
- Type: `boolean`
- Default: `false`

Use the `data-option-immediate` attribute on a `DataBind` component to propage its value on mount to other components in the same group.
Use the `data-option-immediate` attribute on a `DataBind` component to propagate its value on mount to other components in the same group. Immediate keyed values inside a [`DataScope`](../DataScope/index.md) are collected before subscribers are notified.

### `group`

- Type: `string`
- Default: `''`

The `group` option is used to group instances together. All related instances will be updated when the value changes.
The `group` option is used to group instances together. All related instances will be updated when the value changes. Inside a [`DataScope`](../DataScope/index.md), an omitted group inherits the scope's group and remains isolated from other scopes.

When using it with multiple checkboxes or select multiple, use the `[]` suffix to push each selected value in an array. See the [checkboxes example](/components/DataBind/examples.md#checkboxes) for more details on how this works.
### `key`

- Type: `string`
- Default: the native form control `name`, when scoped

A keyed value updates only bindings with the same key while notifying unkeyed subscribers. Keys are local to a `DataScope`; unscoped bindings preserve scalar group behavior.

When using it with multiple checkboxes or a multiple select, use the `[]` suffix to push each selected value into an array. See the [checkboxes example](../DataModel/examples.md#checkboxes) for more details.

## Properties

### `value`

Get and set the value on the current instance. This is a getter and setter alias for the [`set(value)`](#set-value-string-boolean-string) and [get()](#get) methods.
Get and set the value on the current instance. This is a getter and setter alias for the `set(value)` and [`get()`](#get) methods.

### `target`

Expand All @@ -57,18 +64,84 @@ The targeted DOM element.
- Type: `boolean`
- Readonly

Wether new values should be pushed to an array instead of a single value. This is enabled by adding the `[]` suffix to the [`group` option](#group).
Whether new values should be pushed to an array instead of a single value. This is enabled by adding the `[]` suffix to the [`group` option](#group).

## Methods

### `set(value: string | boolean | string[], dispatch = true)`
### `set(value: DataValue, dispatch = true)`

Set the value for the current instance and dispatch it to others if the second parameter `dispatch` is set to `true` (default).

`DataValue` accepts `boolean`, `string`, `string[]`, `number`, `Date`, `null`, or `undefined`.

**Params**

- `value` (`string | boolean | string[]`): the value to set
- `dispatch` (`boolean`, default to `true`): wether to dispatch the value to other related instances or not
- `value` (`DataValue`): the value to set
- `dispatch` (`boolean`, defaults to `true`): whether to dispatch the value to other related instances

The mutation helpers below are available on `DataBind` and `DataModel`. They are not supported on computed values or effects.

### `toggle(onValue = true, offValue = false)`

Toggle between two values and dispatch the result to the group. Single checkboxes support the default boolean values; custom values require a target that can represent them without coercing them to `checked`. Radio inputs are not supported.

Custom values can describe disclosure state without repeating comparison logic in an Action:

```html
<div data-component="DataScope" data-option-group="disclosure">
<button
type="button"
value="closed"
data-component="Action DataModel"
data-option-key="state"
data-option-prop="value"
data-option-immediate
data-on:click="DataModel.toggle('open', 'closed')"
data-bind:attr.aria-expanded="String(value === 'open')">
Toggle
</button>
</div>
```

### `increment(step = 1)`

Convert the current value to a number, increment it by `step`, and dispatch the result. A non-numeric current value starts at `0`. Pass a negative step to decrement. Date inputs are not supported.

```html
<div data-component="DataScope" data-option-group="counter">
<button
type="button"
value="0"
data-component="Action DataModel"
data-option-key="count"
data-option-prop="value"
data-option-immediate
data-on:click="DataModel.increment()"
data-bind:text="`Count: ${value}`">
Count: 0
</button>
</div>
```

### `cycle(values)`

Select and dispatch the value following the current value in the given array. The method wraps to the first value; an unknown current value also selects the first value. An empty array does nothing.

```html
<div data-component="DataScope" data-option-group="workflow">
<button
type="button"
value="draft"
data-component="Action DataModel"
data-option-key="status"
data-option-prop="value"
data-option-immediate
data-on:click="DataModel.cycle(['draft', 'review', 'published'])"
data-bind:text="`Status: ${value}`">
Status: draft
</button>
</div>
```

### `get()`

Expand Down
4 changes: 2 additions & 2 deletions packages/docs/components/DataBind/stories/basic.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { registerComponent } from '@studiometa/js-toolkit';
import { DataBind, DataComputed, DataEffect, DataModel } from '@studiometa/ui';
import { DataBind, DataComputed, DataModel, DataScope } from '@studiometa/ui';

registerComponent(DataScope);
registerComponent(DataBind);
registerComponent(DataComputed);
registerComponent(DataEffect);
registerComponent(DataModel);
41 changes: 22 additions & 19 deletions packages/docs/components/DataBind/stories/basic.twig
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
<div data-component="DataEffect"
data-option-group="msg"
data-option-effect="target.classList.toggle('bg-red-400/50', value.length > 15)"
class="flex flex-col gap-4">
<input
data-component="DataModel"
data-option-group="msg"
data-option-main
value="Hello world"
class="p-4 bg-transparent ring rounded" />
<div
data-component="DataScope"
data-option-group="message"
class="grid gap-4 max-w-lg rounded ring p-4">
<label class="grid gap-1">
<span>Message</span>
<input
name="text"
value="Hello world"
data-component="DataModel"
data-option-immediate
class="p-4 bg-transparent ring rounded" />
</label>
<h1
data-component="DataBind DataEffect"
data-option-group="msg"
data-option-effect="target.classList.toggle('text-red-600', value.length > 15)"
data-component="DataBind"
data-option-key="text"
data-bind:text
data-bind:class.text-red-600="value.length > 15"
class="text-4xl font-bold">
Hello world
</h1>
<p
data-component="DataComputed"
data-option-compute="`Length: ${value.length}`"
data-option-group="msg"
class="text-xl">
Lengh: 11
<p class="text-xl">
<span
data-component="DataComputed"
data-option-key="text"
data-option-compute="`Length: ${value.length}`">Length: 11</span>
</p>
</div>
4 changes: 3 additions & 1 deletion packages/docs/components/DataBind/stories/immediate.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { registerComponent } from '@studiometa/js-toolkit';
import { DataBind } from '@studiometa/ui';
import { DataBind, DataModel, DataScope } from '@studiometa/ui';

registerComponent(DataScope);
registerComponent(DataBind);
registerComponent(DataModel);
36 changes: 21 additions & 15 deletions packages/docs/components/DataBind/stories/immediate.twig
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
<div class="grid gap-4 max-w-sm">
<input
data-component="DataBind"
data-option-group="foo"
data-option-immediate
type="text"
value="foo"
class="p-2 border border-current/20 rounded" />
<input
data-component="DataBind"
data-option-group="foo"
type="text"
value=""
readonly
class="p-2 border border-current/20 rounded" />
<div
data-component="DataScope"
data-option-group="message"
class="grid gap-4 max-w-sm rounded ring p-4">
<label class="grid gap-1">
<span>Primary model</span>
<input
name="text"
value="Hello world"
data-component="DataModel"
data-option-immediate
class="p-2 bg-transparent ring rounded" />
</label>
<label class="grid gap-1">
<span>Mirrored model</span>
<input
name="text"
data-component="DataModel"
class="p-2 bg-transparent ring rounded" />
</label>
<output data-component="DataBind" data-option-key="text">Hello world</output>
</div>
6 changes: 4 additions & 2 deletions packages/docs/components/DataComputed/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Use the `DataComputed` component alongside the [`DataModel` component](../DataMo
::: code-group

```html [index.html]
<!-- Two ways binding on the "text" group for the input's value -->
<!-- Two-way binding on the "text" group for the input's value -->
<input type="text" data-component="DataModel" data-option-group="text" />

<!-- Update the text content with the input's value in UPPERCASE -->
Expand Down Expand Up @@ -62,7 +62,9 @@ registerComponent(DataComputed);

</llm-only>

### Double computed value
### Combine scoped values

Inside a [`DataScope`](../DataScope/index.md), an unkeyed computed expression can derive a value from several named models through the frozen `$data` snapshot.

<llm-exclude>
<PreviewPlayground
Expand Down
Loading
Loading