Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions packages/docs/components/Action/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Modifiers can be chained with a `.` as separator:

Use this option to define the components that should be used as targets to the [effect callback](#effect). Multiple components can be defined by using a single space as delimiter.

When an Action is inside a [`DataScope`](../DataScope/index.md), targets are limited to components in that same nearest scope. Actions outside a `DataScope` keep resolving targets globally.

::: info Name definition
The `Action` component will use the `name` property defined in the static `config` object of each class to resolve components on the page.

Expand Down
62 changes: 62 additions & 0 deletions packages/docs/components/DataBind/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,68 @@ registerComponent(DataBind);

:::

### 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 Tabs-like controls keep their labels while updating state and panels from the `tab` group:

```html
<input
id="current-tab"
type="hidden"
value="overview"
data-component="DataBind"
data-option-group="tab"
data-option-immediate />

<button
data-component="Action DataBind"
data-option-group="tab"
data-on:click="DataBind(#current-tab) -> target.value = 'overview'"
data-bind:class.is-active="value === 'overview'"
data-bind:attr.aria-selected="String(value === 'overview')">
Overview
</button>
<button
data-component="Action DataBind"
data-option-group="tab"
data-on:click="DataBind(#current-tab) -> target.value = 'details'"
data-bind:class.is-active="value === 'details'"
data-bind:attr.aria-selected="String(value === 'details')">
Details
</button>

<section
data-component="DataBind"
data-option-group="tab"
data-bind:attr.hidden="value !== 'overview'">
Overview panel
</section>
<section
data-component="DataBind"
data-option-group="tab"
data-bind:attr.hidden="value !== 'details'">
Details panel
</section>
```

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
40 changes: 36 additions & 4 deletions packages/docs/components/DataBind/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,21 @@ 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.

### `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 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.

Expand All @@ -61,15 +68,40 @@ Wether new values should be pushed to an array instead of a single value. This i

## 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
- `value` (`DataValue`): the value to set
- `dispatch` (`boolean`, default to `true`): wether to dispatch the value to other related instances or not

### `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
<button
data-component="Action"
data-option-target="DataBind(.state)"
data-option-effect="target.toggle('open', 'closed')">
Toggle
</button>
```

### `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.

### `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.

### `get()`

Get the value for the current instance.
2 changes: 1 addition & 1 deletion packages/docs/components/DataComputed/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The `DataComputed` component extends the [`DataBind` component](../DataBind/js-a
- Type: `string`
- Default: `''`

Use this option to define a piece of JavaScript code to transform the value before it is updated on the target. The `value` and `target` variables can be used to access both the current value of the binding and the DOM element targeted by the changes.
Use this option to define a piece of JavaScript code to transform the value before it is updated on the target. The `value` and `target` variables can be used to access both the current value of the binding and the DOM element targeted by the changes. Inside a `DataScope`, the third `$data` argument is a frozen snapshot of all keyed values in the resolved group.

**Example**

Expand Down
2 changes: 1 addition & 1 deletion packages/docs/components/DataEffect/js-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The `DataEffect` component extends the [`DataBind` component](../DataBind/js-api
- Type: `string`
- Default: `''`

Use this option to define a piece of JavaScript code to be executed when the value changes. The `value` and `target` variables can be used to access both the current value of the binding and the DOM element targeted by the changes.
Use this option to define a piece of JavaScript code to be executed when the value changes. The `value` and `target` variables can be used to access both the current value of the binding and the DOM element targeted by the changes. Inside a `DataScope`, the third `$data` argument is a frozen snapshot of all keyed values in the resolved group.

**Example**

Expand Down
50 changes: 50 additions & 0 deletions packages/docs/components/DataScope/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: DataScope examples
---

# Examples

## Isolated reusable widgets

Sibling scopes remain independent even when they use the same group name. In the following example, each profile owns its `person` group and its own `$data` snapshot. Updating one profile does not update the other.

<llm-exclude>
<PreviewPlayground
:html="() => import('./stories/isolated.twig')"
:script="() => import('./stories/isolated.js?raw')"
/>
</llm-exclude>
<llm-only>

:::code-group

<<< ./stories/isolated.twig
<<< ./stories/isolated.js

:::

</llm-only>

## Nested and explicit groups

A Data component without `data-option-group` inherits the group of its nearest `DataScope`. An explicit group creates another group inside the same scope instead of escaping the scope.

```html
<div data-component="DataScope" data-option-group="profile">
<!-- Uses the local "profile" group. -->
<input name="name" data-component="DataModel" />

<!-- Uses a separate local "settings" group. -->
<input
name="theme"
value="dark"
data-component="DataModel"
data-option-group="settings"
data-option-immediate />

<div data-component="DataScope" data-option-group="address">
<!-- Uses the nested scope's local "address" group. -->
<input name="city" data-component="DataModel" />
</div>
</div>
```
39 changes: 39 additions & 0 deletions packages/docs/components/DataScope/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
badges: [JS]
---

# DataScope <Badges :texts="$frontmatter.badges" />

Use the `DataScope` component to isolate [`DataBind`](../DataBind/index.md), [`DataModel`](../DataModel/index.md), [`DataComputed`](../DataComputed/index.md), and [`DataEffect`](../DataEffect/index.md) groups inside a reusable widget. [`Action`](../Action/index.md) targets inside the widget are isolated by the same scope.

Descendant Data components inherit the scope's group unless they define their own `data-option-group`. Nested scopes use the nearest `DataScope` boundary.

## Table of content

- [Examples](./examples.md)
- [JavaScript API](./js-api.md)

## Usage

Import the `DataScope` component with the Data components used by your application. Add `DataScope` to the root element of a widget, then omit `data-option-group` from descendants that should inherit its default group.

<llm-exclude>
<PreviewPlayground
:html="() => import('./stories/basic.twig')"
:script="() => import('./stories/basic.js?raw')"
/>
</llm-exclude>
<llm-only>

:::code-group

<<< ./stories/basic.twig
<<< ./stories/basic.js

:::

</llm-only>

The native `name` of a form control becomes its key inside the scope. Use `data-option-key` to define a key explicitly. Unkeyed computed values and effects receive the complete frozen `$data` snapshot, allowing expressions to combine several keyed values.

See the [JavaScript API](./js-api.md) for details about group inheritance, keys, and scoped data snapshots.
56 changes: 56 additions & 0 deletions packages/docs/components/DataScope/js-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: DataScope JavaScript API
outline: deep
---

# JS API

The `DataScope` component defines a local boundary for Data groups and stores keyed values in frozen `$data` snapshots.

## Options

### `group`

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

The `group` option defines the group inherited by descendant Data components that do not set their own `data-option-group`.

Each group belongs to its nearest `DataScope`. Two scopes can therefore use the same group name without sharing values. An explicit group on a descendant creates another isolated group within the same scope.

## Keys

Inside a scope, a Data component resolves its key from:

1. its `data-option-key` option;
2. the native `name` of an `<input>`, `<select>`, or `<textarea>`.

A keyed value updates bindings with the same key and notifies unkeyed [`DataComputed`](../DataComputed/index.md) and [`DataEffect`](../DataEffect/index.md) subscribers. Unscoped Data groups preserve their scalar behavior.

Use `data-option-immediate` on keyed sources to collect their initial values when the components mount. All immediate values mounted in the same tick are collected before subscribers are notified.

## Scoped data

Data callbacks receive the group's `$data` snapshot as their third argument:

```html
<div data-component="DataScope" data-option-group="person">
<input name="first" value="Ada" data-component="DataModel" data-option-immediate />
<input name="last" value="Lovelace" data-component="DataModel" data-option-immediate />
<span data-component="DataComputed" data-option-compute="`${$data.first} ${$data.last}`">
Ada Lovelace
</span>
</div>
```

The snapshot object and its array values are frozen. Array and `Date` values are cloned before they are exposed, so mutating the original value does not mutate an existing snapshot. A cloned `Date` can still be changed through its mutation methods, but later snapshots remain isolated from those changes.

## Action targets

An [`Action`](../Action/index.md) inside a `DataScope` resolves targets only among components in the same nearest scope. An Action outside a scope continues to resolve targets globally.

## Lifecycle

Scope membership is resolved when a Data component is initialized. Moving a mounted component between scopes or changing its group or key dynamically is not supported.

When the last component providing a key is destroyed, that key is removed from the scope's `$data` snapshot.
6 changes: 6 additions & 0 deletions packages/docs/components/DataScope/stories/basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { registerComponent } from '@studiometa/js-toolkit';
import { DataComputed, DataModel, DataScope } from '@studiometa/ui';

registerComponent(DataScope);
registerComponent(DataModel);
registerComponent(DataComputed);
29 changes: 29 additions & 0 deletions packages/docs/components/DataScope/stories/basic.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<div
data-component="DataScope"
data-option-group="person"
class="flex flex-col gap-4 rounded ring p-4">
<label class="flex flex-col gap-1">
<span>First name</span>
<input
name="first"
value="Ada"
data-component="DataModel"
data-option-immediate
class="bg-transparent rounded ring p-2" />
</label>
<label class="flex flex-col gap-1">
<span>Last name</span>
<input
name="last"
value="Lovelace"
data-component="DataModel"
data-option-immediate
class="bg-transparent rounded ring p-2" />
</label>
<p
data-component="DataComputed"
data-option-compute="`${$data.first} ${$data.last}`"
class="text-xl font-bold">
Ada Lovelace
</p>
</div>
6 changes: 6 additions & 0 deletions packages/docs/components/DataScope/stories/isolated.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { registerComponent } from '@studiometa/js-toolkit';
import { DataComputed, DataModel, DataScope } from '@studiometa/ui';

registerComponent(DataScope);
registerComponent(DataModel);
registerComponent(DataComputed);
37 changes: 37 additions & 0 deletions packages/docs/components/DataScope/stories/isolated.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div class="grid gap-6 md:grid-cols-2">
<div
data-component="DataScope"
data-option-group="person"
class="flex flex-col gap-4 rounded ring p-4">
<h2 class="text-xl font-bold">First profile</h2>
<input
name="first"
value="Ada"
data-component="DataModel"
data-option-immediate
class="bg-transparent rounded ring p-2" />
<output
data-component="DataComputed"
data-option-compute="`Hello ${$data.first}`">
Hello Ada
</output>
</div>

<div
data-component="DataScope"
data-option-group="person"
class="flex flex-col gap-4 rounded ring p-4">
<h2 class="text-xl font-bold">Second profile</h2>
<input
name="first"
value="Grace"
data-component="DataModel"
data-option-immediate
class="bg-transparent rounded ring p-2" />
<output
data-component="DataComputed"
data-option-compute="`Hello ${$data.first}`">
Hello Grace
</output>
</div>
</div>
Loading