Default to kebab-case and dispatch `datastar-sse` on element (#761)

* Rename event and dispatch on element

* Add release note

* Upate docs

* Fix test

* Make events bubble up

* Default to kebab-case

* Kebab

* Fix docs

* Default to kebab-case for `data-class-*`

* Fix missing argument

* Finalise docs
This commit is contained in:
Ben Croker 2025-03-15 19:32:29 -06:00 committed by GitHub
parent c35b85ce4e
commit 1bd119caf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 187 additions and 46 deletions

View File

@ -13,6 +13,9 @@ Each tagged version of Datastar is accompanied by a release note. Read the [rele
### Changed
- Updated Idiomorph to version [0.7.3](https://github.com/bigskysoftware/idiomorph/releases/tag/v0.7.3).
- Classes used in `data-class-*` attributes now default to kebab-case ([#761](https://github.com/starfederation/datastar/issues/761)).
- Events used in `data-on-*` attributes now default to kebab-case ([#761](https://github.com/starfederation/datastar/issues/761)).
- The `datastar-sse` event is now dispatched on the element itself ([#761](https://github.com/starfederation/datastar/issues/761)).
- The NPM package now also exports all official plugins and bundles ([#742](https://github.com/starfederation/datastar/issues/742)).
### Fixed

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

@ -4,7 +4,7 @@
import { DATASTAR, DATASTAR_REQUEST, DefaultSseRetryDurationMs } from '../../../../engine/consts'
import { runtimeErr } from '../../../../engine/errors'
import type { RuntimeContext } from '../../../../engine/types'
import type { HTMLorSVGElement, RuntimeContext } from '../../../../engine/types'
import {
type FetchEventSourceInit,
fetchEventSource,
@ -18,10 +18,11 @@ import {
STARTED,
} from '../shared'
function dispatchSSE(type: string, argsRaw: Record<string, string>) {
document.dispatchEvent(
function dispatchSSE(el: HTMLorSVGElement, type: string, argsRaw: Record<string, string>) {
el.dispatchEvent(
new CustomEvent<DatastarSSEEvent>(DATASTAR_SSE_EVENT, {
detail: { type, argsRaw },
bubbles: true,
}),
)
}
@ -87,7 +88,7 @@ export const sse = async (
const action = method.toLowerCase()
let cleanupFn = (): void => {}
try {
dispatchSSE(STARTED, { elId })
dispatchSSE(el, STARTED, { elId })
if (!url?.length) {
throw runtimeErr('SseNoUrlProvided', ctx, { action })
}
@ -113,7 +114,7 @@ export const sse = async (
onopen: async (response: Response) => {
if (response.status >= 400) {
const status = response.status.toString()
dispatchSSE(ERROR, { status })
dispatchSSE(el, ERROR, { status })
}
},
onmessage: (evt) => {
@ -142,7 +143,7 @@ export const sse = async (
}
// if you aren't seeing your event you can debug by using this line in the console
dispatchSSE(type, argsRaw)
dispatchSSE(el, type, argsRaw)
},
onerror: (error) => {
if (isWrongContent(error)) {
@ -152,7 +153,7 @@ export const sse = async (
// do nothing and it will retry
if (error) {
console.error(error.message)
dispatchSSE(RETRYING, { message: error.message })
dispatchSSE(el, RETRYING, { message: error.message })
}
},
}
@ -215,7 +216,7 @@ export const sse = async (
// set the content-type to text/event-stream
}
} finally {
dispatchSSE(FINISHED, { elId })
dispatchSSE(el, FINISHED, { elId })
cleanupFn()
}
}

View File

@ -8,7 +8,7 @@ import {
PluginType,
Requirement,
} from '../../../../engine/types'
import { modifyCasing } from '../../../../utils/text'
import { kebab, modifyCasing } from '../../../../utils/text'
export const Class: AttributePlugin = {
type: PluginType.Attribute,
@ -29,12 +29,15 @@ export const Class: AttributePlugin = {
}
}
} else {
key = modifyCasing(key, mods)
// Default to kebab-case and allow modifying
let className = kebab(key)
className = modifyCasing(className, mods)
const shouldInclude = rx<boolean>()
if (shouldInclude) {
cl.add(key)
cl.add(className)
} else {
cl.remove(key)
cl.remove(className)
}
}
})

View File

@ -11,7 +11,7 @@ import {
Requirement,
} from '../../../../engine/types'
import { tagHas, tagToMs } from '../../../../utils/tags'
import { camel, modifyCasing } from '../../../../utils/text'
import { camel, kebab, modifyCasing } from '../../../../utils/text'
import { debounce, delay, throttle } from '../../../../utils/timing'
import { supportsViewTransitions } from '../../../../utils/view-transtions'
import type { Signal } from '../../../../vendored/preact-core'
@ -172,7 +172,10 @@ export const On: AttributePlugin = {
callback = targetOutsideCallback
}
const eventName = modifyCasing(key, mods)
// Default to kebab-case and allow modifying
let eventName = kebab(key)
eventName = modifyCasing(eventName, mods)
target.addEventListener(eventName, callback, evtListOpts)
return () => {
target.removeEventListener(eventName, callback)

4
sdk/go/consts.go generated
View File

@ -7,8 +7,8 @@ import "time"
const (
DatastarKey = "datastar"
Version = "1.0.0-beta.9"
VersionClientByteSize = 39670
VersionClientByteSizeGzip = 14828
VersionClientByteSize = 39706
VersionClientByteSizeGzip = 14847
//region Default durations

View File

@ -59,6 +59,7 @@ func setupTests(ctx context.Context, router chi.Router) (err error) {
{ID: "signals_change"},
{ID: "signals_change_path"},
{ID: "signals_change_path_once"},
{ID: "sse_events"},
},
},
}
@ -119,6 +120,7 @@ func setupTests(ctx context.Context, router chi.Router) (err error) {
setupTestsMergeFragmentWhitespace(testsRouter),
setupTestsOnLoad(testsRouter),
setupTestsRemoveFragment(testsRouter),
setupTestsSseEvents(testsRouter),
); err != nil {
panic(fmt.Sprintf("error setting up tests routes: %s", err))
}

View File

@ -0,0 +1,17 @@
package site
import (
"net/http"
"github.com/go-chi/chi/v5"
datastar "github.com/starfederation/datastar/sdk/go"
)
func setupTestsSseEvents(testsRouter chi.Router) error {
testsRouter.Get("/sse_events/data", func(w http.ResponseWriter, r *http.Request) {
datastar.NewSSE(w, r)
})
return nil
}

View File

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

View File

@ -34,6 +34,14 @@ Note that `data-*` attributes are evaluated in the order they appear in the DOM.
<div data-signals-foo="1"></div>
```
### Attribute Casing
Note that `data-*` attributes are [case-insensitive](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*). The keys used in attribute plugins that define signals, such as `data-signals-*`, are converted to [camelCase](https://developer.mozilla.org/en-US/docs/Glossary/Camel_case) (`data-signals-my-signal` defines a signal named `mySignal`).
The keys used by all other attribute plugins are are converted to [kebab-case](https://developer.mozilla.org/en-US/docs/Glossary/Kebab_case) (`data-class-text-blue-700` adds or removes the class `text-blue-700`).
You can use the `__case` modifier to convert between camelCase, kebab-case, snake_case, and PascalCase, or alternatively use object syntax when available.
## Core Plugins
[Source Code](https://github.com/starfederation/datastar/blob/main/library/src/plugins/official/core/attributes)
@ -64,22 +72,27 @@ The `data-signals` attribute can also be used to merge multiple signals using a
The value above is written in JavaScript object notation, but JSON, which is a subset and which most templating languages have built-in support for, is also allowed.
Note that `data-*` attributes are case-insensitive. If you want to use uppercase characters in signal names, you'll need to kebabize them or use object syntax. So the signal name `mySignal` must be written as `data-signals-my-signal` or `data-signals="{mySignal: 1}"`.
You can further modify the casing of keys in `data-*` attributes using the `__case` modifier, followed by `.kebab`, `.snake`, or `.pascal`.
Keys used in `data-signals-*` are converted to camel case, so the signal name `mySignal` must be written as `data-signals-my-signal` or `data-signals="{mySignal: 1}"`.
Signals beginning with an underscore are considered _local signals_ and are not included in requests to the backend by default. You can include them by setting the [`includeLocal`](/reference/action_plugins#options) option to `true`.
Signal names cannot begin or contain double underscores (`__`), due to the use of `__` as a modifer delimiter.
Signal names cannot begin or contain double underscores (`__`), due to its use as a modifer delimiter.
#### Modifiers
Modifiers allow you to modify behavior when merging signals.
- `__case` - Converts the casing of the signal name.
- `.camel` - Camel case: `mySignal` (default)
- `.kebab` - Kebab case: `my-signal`
- `.snake` - Snake case: `my_signal`
- `.pascal` - Pascal case: `MySignal`
- `__ifmissing` - Only merges signals if their keys do not already exist. This is useful for setting defaults without overwriting existing values.
```html
<div data-signals-foo__ifmissing="1"></div>
<div data-signals-my-signal__case.kebab="1"
data-signals-foo__ifmissing="1"
></div>
```
### `data-computed`
@ -97,6 +110,20 @@ Computed signals are useful for memoizing expressions containing other signals.
<div data-text="$foo"></div>
```
#### Modifiers
Modifiers allow you to modify behavior when defining computed signals.
- `__case` - Converts the casing of the signal name.
- `.camel` - Camel case: `mySignal` (default)
- `.kebab` - Kebab case: `my-signal`
- `.snake` - Snake case: `my_signal`
- `.pascal` - Pascal case: `MySignal`
```html
<div data-computed-my-signal__case.kebab="$bar + $baz"></div>
```
### `data-ref`
Creates a new signal that is a reference to the element on which the data attribute is placed.
@ -117,6 +144,20 @@ The signal value can then be used to reference the element.
`foo` holds a <span data-text="$foo.tagName"></span> element.
```
#### Modifiers
Modifiers allow you to modify behavior when defining references.
- `__case` - Converts the casing of the signal name.
- `.camel` - Camel case: `mySignal` (default)
- `.kebab` - Kebab case: `my-signal`
- `.snake` - Snake case: `my_signal`
- `.pascal` - Pascal case: `MySignal`
```html
<div data-ref-my-signal__case.kebab></div>
```
## DOM Plugins
[Source Code](https://github.com/starfederation/datastar/blob/main/library/src/plugins/official/dom/attributes)
@ -176,6 +217,20 @@ Multiple input values can be assigned to a single signal by predefining the sign
</div>
```
#### Modifiers
Modifiers allow you to modify behavior when binding signals.
- `__case` - Converts the casing of the signal name.
- `.camel` - Camel case: `mySignal` (default)
- `.kebab` - Kebab case: `my-signal`
- `.snake` - Snake case: `my_signal`
- `.pascal` - Pascal case: `MySignal`
```html
<input data-bind-my-signal__case.kebab />
```
### `data-class`
Adds or removes a class to or from an element based on an expression.
@ -192,6 +247,20 @@ The `data-class` attribute can also be used to add or remove multiple classes fr
<div data-class="{hidden: $foo, 'font-bold': $bar}"></div>
```
#### Modifiers
Modifiers allow you to modify behavior defining a class name.
- `__case` - Converts the casing of the class.
- `.camel` - Camel case: `myClass`
- `.kebab` - Kebab case: `my-class` (default)
- `.snake` - Snake case: `my_class`
- `.pascal` - Pascal case: `MyClass`
```html
<div data-class-my-class__case.camel="$foo"></div>
```
### `data-on`
Attaches an event listener to an element, executing an expression whenever the event is triggered.
@ -227,6 +296,11 @@ Modifiers allow you to modify behavior when events are triggered. Some modifiers
- `__once` \* - Only trigger the event listener once.
- `__passive` \* - Do not call `preventDefault` on the event listener.
- `__capture` \* - Use a capture event listener.
- `__case` - Converts the casing of the event.
- `.camel` - Camel case: `myEvent`
- `.kebab` - Kebab case: `my-event` (default)
- `.snake` - Snake case: `my_event`
- `.pascal` - Pascal case: `MyEvent`
- `__delay` - Delay the event listener.
- `.500ms` - Delay for 500 milliseconds.
- `.1s` - Delay for 1 second.
@ -253,7 +327,9 @@ Modifiers allow you to modify behavior when events are triggered. Some modifiers
\* Only works on built-in events.
```html
<div data-on-click__window__debounce.500ms.leading="$foo = ''"></div>
<div data-on-click__window__debounce.500ms.leading="$foo = ''"
data-on-my-event__case.camel="$foo = ''"
></div>
```
### `data-persist`
@ -278,12 +354,17 @@ If a key is provided, it will be used as the key when saving in storage, otherwi
#### Modifiers
Modifiers allow you to modify the storage target.
Modifiers allow you to modify the key and storage target.
- `__case` - Converts the casing of the key.
- `.camel` - Camel case: `myKey` (default)
- `.kebab` - Kebab case: `my-key`
- `.snake` - Snake case: `my_key`
- `.pascal` - Pascal case: `MyKey`
- `__session` - Persists signals in Session Storage.
```html
<div data-persist__session></div>
<div data-persist-my-key__case.kebab__session></div>
```
### `data-replace-url`
@ -426,6 +507,16 @@ The signal name can be specified in the key (as above), or in the value (as belo
<button data-indicator="fetching"></button>
```
#### Modifiers
Modifiers allow you to modify behavior when defining indicator signals.
- `__case` - Converts the casing of the signal name.
- `.camel` - Camel case: `mySignal` (default)
- `.kebab` - Kebab case: `my-signal`
- `.snake` - Snake case: `my_signal`
- `.pascal` - Pascal case: `MySignal`
## Ignoring Elements
### `data-star-ignore`

View File

@ -0,0 +1,12 @@
# SSE Events
Tests that SSE events are dispatched on the element itself.
<div data-signals-result="0" data-on-load="@get('/tests/sse_events/data')" data-on-datastar-sse="$result++; events.innerHTML += evt.detail.type + '\n'">
<pre id="events"></pre>
<hr />
Result:
<code id="result" data-text="$result == 2 ? 1 : 0">0</code>
<hr />
Expected result on load: <code>1</code>
</div>