Add wildcard support (#805)

* Dispatch `datastar-sse` event on `document`

* Release note

* Fix

* Modify view transition

* Fixes

* Fixes

* Add wildcard support

* Add wildcard to OnSignalChange
This commit is contained in:
Ben Croker 2025-03-29 11:11:18 -06:00 committed by GitHub
parent 05c751770b
commit 5ba9759fba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 296 additions and 115 deletions

View File

@ -17,3 +17,6 @@ Each tagged version of Datastar is accompanied by a release note. Read the [rele
### Changed
- The `datastar-sse` event is now dispatched on the `document` element, and using `data-on-datastar-sse` automatically listens for the event on the `document` ([#802](https://github.com/starfederation/datastar/issues/802)).
- The `data-on-signals-change-*` attribute key now accepts a path in which `*` can be used as a wildcard (`data-on-signals-change-foo.*`).
- The `@setAll` action now accepts one or more space-separated paths in which `*` can be used as a wildcard (`@setAll('foo.* bar.*', true)`).
- The `@toggleAll` action now accepts one or more space-separated paths in which `*` can be used as a wildcard (`@toggleAll('foo.* bar.*', true)`).

View File

@ -10,7 +10,7 @@
Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework.
Getting started is as easy as adding a single 14.5 KiB script tag to your HTML.
Getting started is as easy as adding a single 14.6 KiB script tag to your HTML.
```html
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.10/bundles/datastar.js"></script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -10,7 +10,7 @@
Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework.
Getting started is as easy as adding a single 14.5 KiB script tag to your HTML.
Getting started is as easy as adding a single 14.6 KiB script tag to your HTML.
```html
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.10/bundles/datastar.js"></script>

View File

@ -10,6 +10,7 @@ import {
PluginType,
Requirement,
} from '../../../../engine/types'
import { pathMatchesPattern } from '../../../../utils/paths'
import { modifyCasing } from '../../../../utils/text'
import { modifyTiming } from '../../../../utils/timing'
import { modifyViewTransition } from '../../../../utils/view-transtions'
@ -33,10 +34,10 @@ export const OnSignalChange: AttributePlugin = {
}
}
const signalPath = modifyCasing(key, mods)
const pattern = modifyCasing(key, mods)
const signalValues = new Map<Signal, any>()
signals.walk((path, signal) => {
if (path.startsWith(signalPath)) {
if (pathMatchesPattern(path, pattern)) {
signalValues.set(signal, signal.value)
}
})

View File

@ -1,16 +1,25 @@
// Authors: Delaney Gillilan
// Icon: ion:checkmark-round
// Slug: Set all signals that match a regular expression
// Slug: Set all signals that match the signal path
// Description: Set all signals that match one or more space-separated paths in which `*` can be used as a wildcard
import { type ActionPlugin, PluginType } from '../../../../engine/types'
import { pathMatchesPattern } from '../../../../utils/paths'
import { trimDollarSignPrefix } from '../../../../utils/text'
export const SetAll: ActionPlugin = {
type: PluginType.Action,
name: 'setAll',
fn: ({ signals }, prefix: string, newValue) => {
fn: ({ signals }, paths: string, newValue) => {
let patterns = paths.split(/\s+/).filter((p) => p !== '')
patterns = patterns.map((p) => trimDollarSignPrefix(p))
for (const pattern of patterns) {
signals.walk((path, signal) => {
if (!path.startsWith(prefix)) return
if (pathMatchesPattern(path, pattern)) {
signal.value = newValue
}
})
}
},
}

View File

@ -1,16 +1,25 @@
// Authors: Delaney Gillilan
// Icon: material-symbols:toggle-off
// Slug: Toggle all signals that match a regular expression
// Slug: Toggle all signals that match the signal path
// Description: Toggle all signals that match one or more space-separated paths in which `*` can be used as a wildcard
import { type ActionPlugin, PluginType } from '../../../../engine/types'
import { pathMatchesPattern } from '../../../../utils/paths'
import { trimDollarSignPrefix } from '../../../../utils/text'
export const ToggleAll: ActionPlugin = {
type: PluginType.Action,
name: 'toggleAll',
fn: ({ signals }, prefix: string) => {
fn: ({ signals }, paths: string) => {
let patterns = paths.split(/\s+/).filter((p) => p !== '')
patterns = patterns.map((p) => trimDollarSignPrefix(p))
for (const pattern of patterns) {
signals.walk((path, signal) => {
if (!path.startsWith(prefix)) return
if (pathMatchesPattern(path, pattern)) {
signal.value = !signal.value
}
})
}
},
}

View File

@ -0,0 +1,7 @@
export function pathMatchesPattern(path: string, pattern: string) {
const regex = new RegExp(
`^${pattern.replaceAll('.', '\\.').replaceAll('*', '.*')}$`,
)
return regex.test(path)
}

4
sdk/go/consts.go generated
View File

@ -7,8 +7,8 @@ import "time"
const (
DatastarKey = "datastar"
Version = "1.0.0-beta.10"
VersionClientByteSize = 40008
VersionClientByteSizeGzip = 14887
VersionClientByteSize = 40231
VersionClientByteSizeGzip = 14988
//region Default durations

View File

@ -55,6 +55,8 @@ func setupTests(ctx context.Context, router chi.Router) (err error) {
{ID: "on_signal_change"},
{ID: "on_signal_change_path"},
{ID: "on_signal_change_path_once"},
{ID: "on_signal_change_path_wildcard"},
{ID: "persist_signals"},
{ID: "plugin_name_prefix"},
{ID: "radio_value"},
{ID: "ref"},
@ -62,6 +64,9 @@ func setupTests(ctx context.Context, router chi.Router) (err error) {
{ID: "remove_initiating_fragment"},
{ID: "select_multiple"},
{ID: "select_single"},
{ID: "set_all_path"},
{ID: "set_all_path_wildcard"},
{ID: "set_all_paths"},
{ID: "sse_error_event"},
{ID: "sse_events"},
},

View File

@ -5,5 +5,5 @@ import (
)
func TestUnitOnSignalChangePathOnce(t *testing.T) {
setupPageTestOnClick(t, "tests/on_signal_change_path_once")
setupPageTestOnLoad(t, "tests/on_signal_change_path_once")
}

View File

@ -5,5 +5,5 @@ import (
)
func TestUnitOnSignalChangePath(t *testing.T) {
setupPageTestOnClick(t, "tests/on_signal_change_path")
setupPageTestOnLoad(t, "tests/on_signal_change_path")
}

View File

@ -0,0 +1,9 @@
package smoketests
import (
"testing"
)
func TestUnitOnSignalChangePathWildcard(t *testing.T) {
setupPageTestOnLoad(t, "tests/on_signal_change_path_wildcard")
}

View File

@ -5,5 +5,5 @@ import (
)
func TestUnitOnSignalChange(t *testing.T) {
setupPageTestOnClick(t, "tests/on_signal_change")
setupPageTestOnLoad(t, "tests/on_signal_change")
}

View File

@ -0,0 +1,39 @@
package smoketests
import (
"testing"
"github.com/Jeffail/gabs/v2"
"github.com/go-rod/rod"
"github.com/stretchr/testify/assert"
)
func TestPersistSignals(t *testing.T) {
setupPageTest(t, "tests/persist_signals", func(runner runnerFn) {
runner("tests/persist_signals", func(t *testing.T, page *rod.Page) {
checkLocalStorage := func(path string) string {
fromLocalStorage := page.MustEval(`k => localStorage[k]`, "datastar")
marshalled := fromLocalStorage.String()
c, err := gabs.ParseJSON([]byte(marshalled))
assert.NoError(t, err)
actual, ok := c.Path(path).Data().(string)
assert.True(t, ok)
return actual
}
page.MustWaitIdle()
assert.Equal(t, "", checkLocalStorage("foo"))
assert.Equal(t, "", checkLocalStorage("bar"))
assert.Equal(t, "", checkLocalStorage("baz"))
page.MustWaitIdle()
foo := page.MustElement("#foo")
bar := page.MustElement("#bar")
foo.MustInput("1")
bar.MustInput("1")
page.MustWaitIdle()
assert.Equal(t, "1", checkLocalStorage("foo"))
assert.Equal(t, "1", checkLocalStorage("bar"))
assert.Equal(t, "", checkLocalStorage("baz"))
})
})
}

View File

@ -10,6 +10,7 @@ import (
func TestExamplePersist(t *testing.T) {
setupPageTest(t, "examples/persist", func(runner runnerFn) {
t.Skip("skipping test, handled by unit tests")
runner("persist", func(t *testing.T, page *rod.Page) {
page.MustWaitIdle()

View File

@ -0,0 +1,9 @@
package smoketests
import (
"testing"
)
func TestUnitSetAllPath(t *testing.T) {
setupPageTestOnLoad(t, "tests/set_all_path")
}

View File

@ -0,0 +1,9 @@
package smoketests
import (
"testing"
)
func TestUnitSetAllPathWildcard(t *testing.T) {
setupPageTestOnLoad(t, "tests/set_all_path_wildcard")
}

View File

@ -0,0 +1,9 @@
package smoketests
import (
"testing"
)
func TestUnitSetAllPaths(t *testing.T) {
setupPageTestOnLoad(t, "tests/set_all_paths")
}

View File

@ -390,7 +390,7 @@ Now when the `Fetch a question` button is clicked, the server will respond with
### `data-indicator`
The [`data-indicator`](/reference/attribute_plugins#data-data-indicator) attribute sets the value of a signal to `true` while the request is in flight, otherwise `false`. We can use this signal to show a loading indicator, which may be desirable for slower responses.
The [`data-indicator`](/reference/attribute_plugins#data-indicator) attribute sets the value of a signal to `true` while the request is in flight, otherwise `false`. We can use this signal to show a loading indicator, which may be desirable for slower responses.
```html
<div id="question"></div>
@ -454,19 +454,19 @@ Actions in Datastar are helper functions that are available in `data-*` attribut
### `@setAll()`
The `@setAll()` action sets the values of multiple signals at once. It takes a path prefix that is used to match against signals, and a value to set them to, as arguments.
The `@setAll()` action sets the value of all matching signals to the expression provided in the second argument. The first argument can be one or more space-separated paths in which `*` can be used as a wildcard.
```html
<button data-on-click="@setAll('form.', true)"></button>
<button data-on-click="@setAll('foo.*', $bar)"></button>
```
This sets the values of all signals namespaced under the `form` signal to `true`, which could be useful for enabling input fields in a form.
This sets the values of all signals namespaced under the `foo` signal to the value of `$bar`. This can be useful for checking multiple checkbox fields in a form, for example:
```html
<input type="checkbox" data-bind-checkboxes.checkbox1 /> Checkbox 1
<input type="checkbox" data-bind-checkboxes.checkbox2 /> Checkbox 2
<input type="checkbox" data-bind-checkboxes.checkbox3 /> Checkbox 3
<button data-on-click="@setAll('checkboxes.', true)">Check All</button>
<button data-on-click="@setAll('checkboxes.*', true)">Check All</button>
```
<div class="flex flex-col items-start gap-2 p-8 alert">
@ -488,26 +488,26 @@ This sets the values of all signals namespaced under the `form` signal to `true`
<input type="checkbox" data-bind-checkboxes1.checkbox3 class="toggle" />
</label>
</div>
<button data-on-click="@setAll('checkboxes1.', true)" class="mt-4 btn btn-secondary">
<button data-on-click="@setAll('checkboxes1.*', true)" class="mt-4 btn btn-secondary">
Check All
</button>
</div>
### `@toggleAll()`
The `@toggleAll()` action toggles the values of multiple signals at once. It takes a path prefix that is used to match against signals, as an argument.
The `@toggleAll()` action toggles the value of all matching signals. The first argument can be one or more space-separated paths in which `*` can be used as a wildcard.
```html
<button data-on-click="@toggleAll('form.')"></button>
<button data-on-click="@toggleAll('foo.*')"></button>
```
This toggles the values of all signals containing `form.` (to either `true` or `false`), which could be useful for toggling input fields in a form.
This toggles the values of all signals namespaced under the `foo` signal (to either `true` or `false`). This can be useful for toggling multiple checkbox fields in a form, for example:
```html
<input type="checkbox" data-bind-checkboxes.checkbox1 /> Checkbox 1
<input type="checkbox" data-bind-checkboxes.checkbox2 /> Checkbox 2
<input type="checkbox" data-bind-checkboxes.checkbox3 /> Checkbox 3
<button data-on-click="@toggleAll('checkboxes.')">Toggle All</button>
<button data-on-click="@toggleAll('checkboxes.*')">Toggle All</button>
```
<div class="flex flex-col items-start gap-2 p-8 alert">
@ -529,7 +529,7 @@ This toggles the values of all signals containing `form.` (to either `true` or `
<input type="checkbox" data-bind-checkboxes2.checkbox_3 class="toggle" />
</label>
</div>
<button data-on-click="@toggleAll('checkboxes2.')" class="mt-4 btn btn-secondary">
<button data-on-click="@toggleAll('checkboxes2.*')" class="mt-4 btn btn-secondary">
Toggle All
</button>
</div>

View File

@ -137,22 +137,38 @@ Copies the provided evaluated expression to the clipboard.
### `@setAll()`
Arguments: `@setAll(pathPrefix: string, value: any)`
Arguments: `@setAll(paths: string, value: any)`
Sets all the signals that start with the prefix to the expression provided in the second argument. This is useful for setting all the values of a signal namespace at once.
Sets the value of all matching signals to the expression provided in the second argument. The first argument can be one or more space-separated signal paths in which `*` can be used as a wildcard.
```html
<div data-on-change="@setAll('foo.', true)"></div>
<!-- Sets the value of `$foo` to `true` -->
<div data-signals-foo="false">
<button data-on-click="@setAll('foo', $bar)"></button>
</div>
<!-- Sets the values of `$foo` and `$bar.baz` to `true` -->
<div data-signals-foo="false" data-signals-bar.baz="false">
<button data-on-click="@setAll('foo bar.*', true)"></button>
</div>
```
### `@toggleAll()`
Arguments: `@toggleAll(pathPrefix: string)`
Arguments: `@toggleAll(paths: string)`
Toggles all the signals that start with the prefix. This is useful for toggling all the values of a signal namespace at once.
Toggles the value of all matching signals. The first argument can be one or more space-separated signal paths or namespaced signal paths in which `*` can be used as a wildcard.
```html
<div data-on-click="@toggleAll('foo.')"></div>
<!-- Toggles the value of `$foo` -->
<div data-signals-foo="false">
<button data-on-change="@toggleAll('foo')"></button>
</div>
<!-- Toggles the values of `$foo` and `$bar.baz` -->
<div data-signals-foo="false" data-signals-bar.baz="false">
<button data-on-click="@toggleAll('foo bar.*')"></button>
</div>
```
### `@fit()`

View File

@ -41,7 +41,7 @@ Datastar provides the following
<div>
The Datastar <a href="https://marketplace.visualstudio.com/items?itemName=starfederation.datastar-vscode">VSCode
extension</a> and <a href="https://plugins.jetbrains.com/plugin/26072-datastar-support">IntelliJ plugin</a>
provided autocompletion for all <code>data-*</code> attributes.
provide autocompletion for all <code>data-*</code> attributes.
</div>
</div>
@ -542,6 +542,7 @@ Modifiers allow you to modify the element intersection behavior and the timing o
- `.1s` - Throttle for 1 second.
- `.noleading` - Throttle without leading edge.
- `.trail` - Throttle with trailing edge.
- `__viewtransition` - Wraps the expression in `document.startViewTransition()` when the View Transition API is available.
```html
<div data-on-intersect__once__full="$fullyIntersected = true"></div>
@ -563,6 +564,7 @@ Modifiers allow you to modify the interval duration.
- `.500ms` - Interval duration of 500 milliseconds.
- `.1s` - Interval duration of 1 second (default).
- `.leading` - Execute the first interval immediately.
- `__viewtransition` - Wraps the expression in `document.startViewTransition()` when the View Transition API is available.
```html
<div data-on-interval__duration.500ms="$count++"></div>
@ -583,6 +585,7 @@ Modifiers allow you to add a delay to the event listener.
- `__delay` - Delay the event listener.
- `.500ms` - Delay for 500 milliseconds.
- `.1s` - Delay for 1 second.
- `__viewtransition` - Wraps the expression in `document.startViewTransition()` when the View Transition API is available.
```html
<div data-on-load__delay.500ms="$count = 1"></div>
@ -610,6 +613,7 @@ Modifiers allow you to modify the timing of the event listener.
- `.1s` - Throttle for 1 second.
- `.noleading` - Throttle without leading edge.
- `.trail` - Throttle with trailing edge.
- `__viewtransition` - Wraps the expression in `document.startViewTransition()` when the View Transition API is available.
```html
<div data-on-raf__debounce.10ms="$count++"></div>
@ -629,6 +633,14 @@ A key can be provided to only trigger the event when a specific signal changes.
<div data-on-signal-change-foo="$fooCount++"></div>
```
The signal path can contain `*` as a wildcard.
```html
<div data-signals-foo.bar="1"
data-on-signal-change-foo.*="$fooCount++"
></div>
```
#### Modifiers
Modifiers allow you to modify the timing of the event listener.
@ -643,6 +655,7 @@ Modifiers allow you to modify the timing of the event listener.
- `.1s` - Throttle for 1 second.
- `.noleading` - Throttle without leading edge.
- `.trail` - Throttle with trailing edge.
- `__viewtransition` - Wraps the expression in `document.startViewTransition()` when the View Transition API is available.
```html
<div data-on-signal-change__debounce.100ms="$count++"></div>

View File

@ -76,9 +76,9 @@ Action plugins are used in Datastar expressions to perform specific actions.
| Action | Description |
|--------|-------------|
| [`@setAll()`](/reference/action_plugins#setall) | Sets all signals with a specific prefix to a provided value. |
| [`@toggleAll()`](/reference/action_plugins#toggleall) | Toggles all signals that start with a given prefix. |
| [`@fit()`](/reference/action_plugins#fit) | Makes a value linearly interpolate from an original range to a new one. |
| [`@setAll()`](/reference/action_plugins#setall) | Sets all signal to a provided value. |
| [`@toggleAll()`](/reference/action_plugins#toggleall) | Toggles all signal values. |
| [`@fit()`](/reference/action_plugins#fit) | Makes a value linearly interpolate. |
View the [action plugins reference](/reference/action_plugins)

View File

@ -1,10 +1,8 @@
# On Signal Change
Tests that a signal change is detected.
Tests detecting a signal change.
<div data-signals="{foo: {bar: 0}, result: 0}" data-on-signal-change="$result = $foo.bar">
<button id="clickable" data-on-click="$foo.bar = 1" class="btn">Change</button>
<hr />
<div data-signals="{foo: {bar: 0}, result: 0}" data-on-signal-change="$result = $foo.bar" data-on-load="$foo.bar = 1">
Result:
<code id="result" data-text="$result"></code>
<hr />

View File

@ -1,10 +1,8 @@
# On Signal Change Path
Tests that a signal change with a path is detected.
Tests detecting a signal change with a path.
<div data-signals="{foo: {bar: 0}, result: 0}" data-on-signal-change-foo="$result = $foo.bar">
<button id="clickable" data-on-click="$foo.bar = 1" class="btn">Change</button>
<hr />
<div data-signals="{foo: 0, result: 0}" data-on-signal-change-foo="$result = $foo" data-on-load="$foo = 1">
Result:
<code id="result" data-text="$result"></code>
<hr />

View File

@ -1,10 +1,8 @@
# On Signal Change Path Once
Tests that a signal change with a path is detected and the expression is called once.
Tests detecting a signal change with a path, and that the expression is called once.
<div data-signals="{foo: {bar: 0}, result: 0}" data-on-signal-change-foo="$result++">
<button id="clickable" data-on-click="$foo.bar = 1" class="btn">Change</button>
<hr />
<div data-signals="{foo: {bar: 0}, result: 0}" data-on-signal-change-foo.bar="$result++" data-on-load="$foo.bar = 1">
Result:
<code id="result" data-text="$result"></code>
<hr />

View File

@ -0,0 +1,10 @@
# On Signal Change Path Wildcard
Tests detecting a signal change with a path using a wildcard.
<div data-signals="{foo: {bar: 0}, result: 0}" data-on-signal-change-foo.*="$result = $foo.bar" data-on-load="$foo.bar = 1">
Result:
<code id="result" data-text="$result"></code>
<hr />
Expected result on click: <code>1</code>
</div>

View File

@ -0,0 +1,8 @@
# Persist Signals
Tests persisting signals.
<div data-signals="{foo: 0, bar: 0, baz: 0}" data-persist="foo bar" data-on-load="$foo = 1; $bar = 1; $baz = 1">
Expected value in local storage (in alphabetical order):
<pre><code>datastar: {"bar":1,"foo":1}</code></pre>
</div>

View File

@ -0,0 +1,10 @@
# Set All Path
Tests the set all action on a single path.
<div data-signals="{foo: false, result: 0}" data-on-load="@setAll('foo', true)">
Result:
<code id="result" data-text="$result = $foo ? 1 : 0"></code>
<hr />
Expected result on load: <code>1</code>
</div>

View File

@ -0,0 +1,10 @@
# Set All Path Wildcard
Tests the set all action on a path using a wildcard.
<div data-signals="{foo: {bar: false}, result: 0}" data-on-load="@setAll('foo.*', true)">
Result:
<code id="result" data-text="$result = $foo.bar ? 1 : 0"></code>
<hr />
Expected result on load: <code>1</code>
</div>

View File

@ -0,0 +1,10 @@
# Set All Path
Tests the set all action on multiple paths.
<div data-signals="{foo: false, bar: false, result: 0}" data-on-load="@setAll('foo bar', true)">
Result:
<code id="result" data-text="$result = $foo && $bar ? 1 : 0"></code>
<hr />
Expected result on load: <code>1</code>
</div>