diff --git a/package-lock.json b/package-lock.json index 0afd09a0..2557e4fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6369,10 +6369,9 @@ } }, "node_modules/alien-signals": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz", - "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", - "dev": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", "license": "MIT" }, "node_modules/ansi-html-community": { @@ -21163,6 +21162,7 @@ "version": "1.9.0-beta.0", "license": "MIT", "dependencies": { + "alien-signals": "^3.2.1", "deepmerge": "^4.3.1", "morphdom": "^2.7.8" }, diff --git a/packages/docs/components/DataBind/examples.md b/packages/docs/components/DataBind/examples.md index a53f2fcb..9da3674a 100644 --- a/packages/docs/components/DataBind/examples.md +++ b/packages/docs/components/DataBind/examples.md @@ -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 ``, in order to set the value of the second `` 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. -
+
+ + - - - - - + + Hello world +
``` ::: +### Multiple virtual bindings + +Use virtual `data-bind:*` attributes to update several parts of an element from the same value: + +| Syntax | Behavior | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `data-bind:prop.` | Assigns the DOM property. | +| `data-bind:attr.` | Removes the attribute for `false`, `null`, or `undefined`; writes an empty attribute for `true`; otherwise writes the stringified value. | +| `data-bind:class.` | Toggles the class according to the result's boolean value. | +| `data-bind:style.` | 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 +
+ + + +
+``` + +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. diff --git a/packages/docs/components/DataBind/js-api.md b/packages/docs/components/DataBind/js-api.md index 0e802160..edff5d0f 100644 --- a/packages/docs/components/DataBind/js-api.md +++ b/packages/docs/components/DataBind/js-api.md @@ -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 `` -- `valueAsNumber` from `` +- `valueAsNumber` for `` - `value` for all other inputs - for ` +
+

Hello world

-

- Lengh: 11 +

+ Length: 11

diff --git a/packages/docs/components/DataBind/stories/immediate.js b/packages/docs/components/DataBind/stories/immediate.js index dcdcd3f3..4e7ed6c8 100644 --- a/packages/docs/components/DataBind/stories/immediate.js +++ b/packages/docs/components/DataBind/stories/immediate.js @@ -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); diff --git a/packages/docs/components/DataBind/stories/immediate.twig b/packages/docs/components/DataBind/stories/immediate.twig index 986b25fc..0b0772a0 100644 --- a/packages/docs/components/DataBind/stories/immediate.twig +++ b/packages/docs/components/DataBind/stories/immediate.twig @@ -1,16 +1,22 @@ -
- - +
+ + + Hello world
diff --git a/packages/docs/components/DataComputed/index.md b/packages/docs/components/DataComputed/index.md index 348e7deb..458a82df 100644 --- a/packages/docs/components/DataComputed/index.md +++ b/packages/docs/components/DataComputed/index.md @@ -20,7 +20,7 @@ Use the `DataComputed` component alongside the [`DataModel` component](../DataMo ::: code-group ```html [index.html] - + @@ -62,7 +62,9 @@ registerComponent(DataComputed); -### 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. - -

- Count: - 10 -

-

- Double: - - 20 - +

+ + +

+ Total: + 20

diff --git a/packages/docs/components/DataComputed/stories/uppercase.twig b/packages/docs/components/DataComputed/stories/uppercase.twig index 3bc0d9fb..822b6e94 100644 --- a/packages/docs/components/DataComputed/stories/uppercase.twig +++ b/packages/docs/components/DataComputed/stories/uppercase.twig @@ -1,5 +1,5 @@
- + - - +```html [index.html] +
+ + +
``` ```js [app.js] twoslash import { registerComponent } from '@studiometa/js-toolkit'; -import { DataModel, DataEffect } from '@studiometa/ui'; +import { DataEffect, DataModel, DataScope } from '@studiometa/ui'; +registerComponent(DataScope); registerComponent(DataModel); registerComponent(DataEffect); ``` diff --git a/packages/docs/components/DataEffect/js-api.md b/packages/docs/components/DataEffect/js-api.md index 36765726..d65a1f9d 100644 --- a/packages/docs/components/DataEffect/js-api.md +++ b/packages/docs/components/DataEffect/js-api.md @@ -5,7 +5,7 @@ outline: deep # DataEffect JS API -The `DataEffect` component extends the [`DataBind` component](../DataBind/js-api.md), it inherits from its API. +The `DataEffect` component extends the [`DataBind` component](../DataBind/js-api.md) and inherits its binding API. Mutation helpers such as `toggle()`, `increment()`, and `cycle()` are not supported on effects. ## Options @@ -14,11 +14,11 @@ 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** -In the following example, the counter text color is changed based on the input's length: +In the following example, the counter is animated whenever the range value changes: + data-option-effect="target.animate([{ opacity: 0.5 }, { opacity: 1 }], { duration: 200 })"> Count: 50

diff --git a/packages/docs/components/DataEffect/stories/too-long.js b/packages/docs/components/DataEffect/stories/too-long.js index 1ab46c54..0de22f7b 100644 --- a/packages/docs/components/DataEffect/stories/too-long.js +++ b/packages/docs/components/DataEffect/stories/too-long.js @@ -1,5 +1,6 @@ import { registerComponent } from '@studiometa/js-toolkit'; -import { DataEffect, DataModel } from '@studiometa/ui'; +import { DataEffect, DataModel, DataScope } from '@studiometa/ui'; -registerComponent(DataEffect); +registerComponent(DataScope); registerComponent(DataModel); +registerComponent(DataEffect); diff --git a/packages/docs/components/DataEffect/stories/too-long.twig b/packages/docs/components/DataEffect/stories/too-long.twig index fb6ec234..cf68fc1f 100644 --- a/packages/docs/components/DataEffect/stories/too-long.twig +++ b/packages/docs/components/DataEffect/stories/too-long.twig @@ -1,10 +1,15 @@ -
- +
+ + data-option-effect="target.setCustomValidity(value.length > 10 ? 'Use 10 characters or fewer.' : '')" + class="p-2 border border-current/20 invalid:ring-red-500 bg-transparent ring rounded" /> + The effect updates the input's native validation state.
diff --git a/packages/docs/components/DataModel/index.md b/packages/docs/components/DataModel/index.md index 52d2f6b8..92a9952e 100644 --- a/packages/docs/components/DataModel/index.md +++ b/packages/docs/components/DataModel/index.md @@ -15,6 +15,8 @@ Use the `DataModel` component to create a two-way binding of a property of HTML Import the components in your main app and use the `DataModel` component on HTML form elements and the `DataBind` and `DataComputed` components on other elements that need to be updated accordingly. The `DataEffect` component can be used to execute side effects when the value changes. +Inside a [`DataScope`](../DataScope/index.md), non-radio controls with the same key are mirrored models. In this example, both inputs use the native name `text`: editing either input updates the other one and every subscriber for that key. The first model uses `data-option-immediate` to hydrate the initial value. + + + + Ada +
+``` + +Destroying one mirrored model does not remove the value from `$data`. The key is removed only after its last `DataModel` source is destroyed. ## Methods diff --git a/packages/docs/components/DataModel/stories/app.js b/packages/docs/components/DataModel/stories/app.js index 378f2be6..b74192e8 100644 --- a/packages/docs/components/DataModel/stories/app.js +++ b/packages/docs/components/DataModel/stories/app.js @@ -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); diff --git a/packages/docs/components/DataModel/stories/basic.twig b/packages/docs/components/DataModel/stories/basic.twig index 8f9914d4..26406591 100644 --- a/packages/docs/components/DataModel/stories/basic.twig +++ b/packages/docs/components/DataModel/stories/basic.twig @@ -1,25 +1,31 @@ -
- -

- Hello world -

-

- Lengh: 11 +

+ + +

+ Hello world +

+

+ Length: + 11

diff --git a/packages/docs/components/DataScope/examples.md b/packages/docs/components/DataScope/examples.md new file mode 100644 index 00000000..688f4e7f --- /dev/null +++ b/packages/docs/components/DataScope/examples.md @@ -0,0 +1,92 @@ +--- +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. + + + + + + +:::code-group + +<<< ./stories/isolated.twig +<<< ./stories/isolated.js + +::: + + + +## Tabs + +The following example implements two independent tab interfaces with `DataScope`, [`Action`](../Action/index.md), [`DataModel`](../DataModel/index.md), and [`DataBind`](../DataBind/index.md). The tab buttons are mirrored models for the same `active` key: the first button hydrates the initial value, every button can publish a new value, and virtual bindings synchronize the selected state and panels. The `Tabs` component is not registered. + + + + + + +:::code-group + +<<< ./stories/tabs.twig +<<< ./stories/tabs.js + +::: + + + +## Accordion + +This example uses the same mirrored-model pattern for two independent accordion interfaces. Each button toggles the shared `active` key with [`toggle()`](../DataBind/js-api.md); opening one panel closes the other panels in that scope, and clicking the open item closes it. The `Accordion` component is not registered. + + + + + + +:::code-group + +<<< ./stories/accordion.twig +<<< ./stories/accordion.js + +::: + + + +## 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 +
+ + + + + + +
+ + +
+
+``` diff --git a/packages/docs/components/DataScope/index.md b/packages/docs/components/DataScope/index.md new file mode 100644 index 00000000..196b7899 --- /dev/null +++ b/packages/docs/components/DataScope/index.md @@ -0,0 +1,39 @@ +--- +badges: [JS] +--- + +# DataScope + +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. + +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. + + + + + + +:::code-group + +<<< ./stories/basic.twig +<<< ./stories/basic.js + +::: + + + +The native `name` of a form control becomes its key inside the scope. Use `data-option-key` to define a key explicitly. Non-radio `DataModel` controls with the same key mirror each other and jointly retain the scoped value. 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. diff --git a/packages/docs/components/DataScope/js-api.md b/packages/docs/components/DataScope/js-api.md new file mode 100644 index 00000000..f4495dd4 --- /dev/null +++ b/packages/docs/components/DataScope/js-api.md @@ -0,0 +1,54 @@ +--- +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 ``, `