Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make modifiers spec compliant #346

Merged
merged 4 commits into from
Dec 10, 2024
Merged
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ This should be the final round of API changes before v1.0.0 🚀
- Renamed the `data-store` attribute to `data-signals`.
- Renamed the `data-bind` attribute to `data-attributes`.
- Renamed the `data-model` attribute to `data-bind`.
- Changed the `data-*` attribute modifier delimiter from `.` to `:` (`data-on-keydown:debounce_100ms:throttle_lead="value"`).
- Changed the `data-*` attribute modifier delimiter from `.` to `__` for modifiers and from `_` to `.` for arguments. This is to be spec compliant while still parseable with new nested signal syntax #345 (`data-on-keydown__debounce.100ms__throttle.noLead="value"`).
- The the `get()`, `post()`, `put()`, and `delete()` plugins have been replaced by a single `sse()` plugin that accepts the method as an option (`sse(url, {method: 'post'})`), defaulting to `get`.
- The `setAll()` and `toggleAll` plugins now accept a path prefix, instead of a regular expression.
- Nested signals no longer allow for `__` in the key. It causes a conflict with modifiers.

### Fixed

Expand Down
4 changes: 2 additions & 2 deletions bundles/datastar-core.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bundles/datastar-core.js.map

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions bundles/datastar.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bundles/datastar.js.map

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions library/src/engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class Engine {

// Extract the key and value from the dataset
const keyRaw = rawKey.slice(p.name.length);
let [key, ...rawModifiers] = keyRaw.split(":");
let [key, ...rawModifiers] = keyRaw.split(/\_\_+/);

const hasKey = key.length > 0;
if (hasKey) {
Expand Down Expand Up @@ -148,7 +148,7 @@ export class Engine {
appliedMacros.clear();
const mods: Modifiers = new Map<string, Set<string>>();
rawModifiers.forEach((m) => {
const [label, ...args] = m.split("_");
const [label, ...args] = m.split(".");
mods.set(camelize(label), new Set(args));
});
const macros = [
Expand Down
4 changes: 4 additions & 0 deletions library/src/engine/nestedSignals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ function mergeNested(
): void {
for (const key in values) {
if (values.hasOwnProperty(key)) {
if (key.match(/\_\_+/)) {
throw dsErr("InvalidSignalKey", { key });
}

const value = values[key];
if (value instanceof Object && !Array.isArray(value)) {
if (!target[key]) {
Expand Down
57 changes: 12 additions & 45 deletions library/src/plugins/official/dom/attributes/on.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,17 @@
// Slug: Add an event listener to an element
// Description: This action adds an event listener to an element. The event listener can be triggered by a variety of events, such as clicks, keypresses, and more. The event listener can also be set to trigger only once, or to be passive or capture. The event listener can also be debounced or throttled. The event listener can also be set to trigger only when the event target is outside the element.

import { dsErr } from "../../../../engine/errors";
import {
AttributePlugin,
PluginType,
Requirement,
} from "../../../../engine/types";
import { argsHas, argsMs } from "../../../../utils/arguments";
import { onElementRemoved } from "../../../../utils/dom";
import { kebabize } from "../../../../utils/text";
import { debounce, throttle } from "../../../../utils/timing";

const knownOnModifiers = new Set([
"window",
"once",
"passive",
"capture",
"debounce",
"throttle",
"remote",
"outside",
]);
let lastSignalsMarshalled = new Map<string, any>();

const EVT = "evt";
export const On: AttributePlugin = {
Expand Down Expand Up @@ -65,6 +56,10 @@ export const On: AttributePlugin = {
if (mods.has("window")) target = window;

let callback = (evt?: Event) => {
if (evt) {
if (!mods.has("noPrevent")) evt.preventDefault();
if (!mods.has("noPropagation")) evt.stopPropagation();
}
rx(evt);
};

Expand Down Expand Up @@ -93,38 +88,6 @@ export const On: AttributePlugin = {
if (mods.has("passive")) evtListOpts.passive = true;
if (mods.has("once")) evtListOpts.once = true;

const unknownModifierKeys = [...mods.keys()].filter(
(key) => !knownOnModifiers.has(key),
);

unknownModifierKeys.forEach((attrName) => {
const eventValues = mods.get(attrName) || [];
const cb = callback;
const revisedCallback = () => {
const evt = event as any;
const attr = evt[attrName];
let valid: boolean;

if (typeof attr === "function") {
valid = attr(...eventValues);
} else if (typeof attr === "boolean") {
valid = attr;
} else if (typeof attr === "string") {
const lowerAttr = attr.toLowerCase().trim();
const expr = [...eventValues].join("").toLowerCase().trim();
valid = lowerAttr === expr;
} else {
throw dsErr("InvalidValue", { attrName, key, el });
}

if (valid) {
cb(evt);
}
};
callback = revisedCallback;
});

let lastSignalsMarshalled = "";
const eventName = kebabize(key).toLowerCase();
switch (eventName) {
case "load":
Expand All @@ -145,11 +108,15 @@ export const On: AttributePlugin = {
};

case "signals-change":
onElementRemoved(el, () => {
lastSignalsMarshalled.delete(el.id);
});
return effect(() => {
const onlyRemoteSignals = mods.has("remote");
const current = signals.JSON(false, onlyRemoteSignals);
if (lastSignalsMarshalled !== current) {
lastSignalsMarshalled = current;
const last = lastSignalsMarshalled.get(el.id) || "";
if (last !== current) {
lastSignalsMarshalled.set(el.id, current);
callback();
}
});
Expand Down
17 changes: 16 additions & 1 deletion library/src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,19 @@ export function elUniqId(el: Element) {
el = el.parentNode as Element;
}
return DATASTAR + hash;
}
}

export function onElementRemoved(element: Element, callback: () => void) {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const removedNode of mutation.removedNodes) {
if (removedNode === element) {
observer.disconnect();
callback();
return;
}
}
}
});
observer.observe(element.parentNode as Node, { childList: true });
}
4 changes: 2 additions & 2 deletions sdk/dotnet/src/Consts.fs

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

4 changes: 2 additions & 2 deletions sdk/go/consts.go

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

4 changes: 2 additions & 2 deletions sdk/java/src/main/java/StarFederation/Datastar/Consts.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
public final class Consts {
public static final String DATASTAR_KEY = "datastar";
public static final String VERSION = "0.21.0-beta2";
public static final int VERSION_CLIENT_BYTE_SIZE = 33647;
public static final int VERSION_CLIENT_BYTE_SIZE_GZIP = 12356;
public static final int VERSION_CLIENT_BYTE_SIZE = 33602;
public static final int VERSION_CLIENT_BYTE_SIZE_GZIP = 12354;

// The default duration for settling during fragment merges. Allows for CSS transitions to complete.
public static final int DEFAULT_FRAGMENTS_SETTLE_DURATION = 300;
Expand Down
4 changes: 2 additions & 2 deletions sdk/php/src/Consts.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class Consts
{
public const DATASTAR_KEY = 'datastar';
public const VERSION = '0.21.0-beta2';
public const VERSION_CLIENT_BYTE_SIZE = 33647;
public const VERSION_CLIENT_BYTE_SIZE_GZIP = 12356;
public const VERSION_CLIENT_BYTE_SIZE = 33602;
public const VERSION_CLIENT_BYTE_SIZE_GZIP = 12354;

// The default duration for settling during fragment merges. Allows for CSS transitions to complete.
public const DEFAULT_FRAGMENTS_SETTLE_DURATION = 300;
Expand Down
2 changes: 1 addition & 1 deletion site/routes_examples_active_search.templ
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ templ ActiveSearchComponent(filteredUsers []*ActiveSearchUser, scores map[string
type="text"
placeholder="Search..."
data-bind="search"
data-on-input:debounce_500ms={ datastar.GetSSE("/examples/active_search/updates") }
data-on-input__debounce.500ms={ datastar.GetSSE("/examples/active_search/updates") }
data-indicator="fetching"
/>
@sseIndicator("fetching")
Expand Down
8 changes: 6 additions & 2 deletions site/routes_examples_inline_validation.templ
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ templ inlineValidationFieldComponent(label, field string, isValid bool, isNotVal
<input
class={ "input input-bordered", templ.KV("input-error",!isValid) }
data-bind={ field }
data-on-keydown:debounce_500ms={ datastar.GetSSE("/examples/inline_validation/data") }
data-on-keydown__debounce.500ms={ datastar.GetSSE("/examples/inline_validation/data") }
data-testid={ "input_" + field }
/>
if !isValid {
Expand All @@ -29,7 +29,11 @@ templ inlineValidationFieldComponent(label, field string, isValid bool, isNotVal
}

templ inlineValidationUserComponent(u *inlineValidationUser, isEmailValid, isFirstNameValid, isLastNameValid, isValid bool) {
<div id="inline_validation" class="flex flex-col gap-4" data-signals:ifmissing={ templ.JSONString(u) }>
<div
id="inline_validation"
class="flex flex-col gap-4"
data-signals__ifmissing={ templ.JSONString(u) }
>
<div class="text-2xl font-bold">Sign Up</div>
<div>
@inlineValidationFieldComponent("Email Address", "email", isEmailValid, "Email '%s' is already taken or is invalid. Please enter another email.", u.Email)
Expand Down
3 changes: 3 additions & 0 deletions site/routes_examples_model_bindings.templ
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,8 @@ templ ModelBindingView(optionCount int, signals *ModelBindingSignals) {
</div>
}
</div>
<code>
<pre data-text="ctx.signals.JSON()"></pre>
</code>
</div>
}
9 changes: 8 additions & 1 deletion site/routes_examples_signals_ifmissing.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package site
import (
"fmt"
"net/http"
"strings"
"time"

"github.com/go-chi/chi/v5"
Expand All @@ -27,7 +28,13 @@ func setupExamplesSignalsIfMissing(examplesRouter chi.Router) error {

switch i % 2 {
case 0:
fragment := fmt.Sprintf(`<div id="placeholder" data-signals:ifmissing=%q data-text="id.value"></div>`, signals)
fragment := strings.TrimSpace(fmt.Sprintf(`
<div
id="placeholder"
data-signals__ifmissing=%q
data-text="id.value"
></div>
`, signals))
sse.MergeFragments(fragment, datastar.WithMergeUpsertAttributes())
case 1:
sse.MarshalAndMergeSignalsIfMissing(signals)
Expand Down
4 changes: 2 additions & 2 deletions site/shared.templ
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ templ Page(title, description string, uri string) {
}
</style>
</head>
// data-on-pageshow:window is to combat Safari's aggressive caching
// data-on-pageshow__window is to combat Safari's aggressive caching
// https://stackoverflow.com/questions/8788802/prevent-safari-loading-from-cache-when-back-button-is-clicked
<body
data-on-pageshow:window="evt?.persisted && window.location.reload()"
data-on-pageshow__window="evt?.persisted && window.location.reload()"
class="flex flex-col min-h-screen overflow-y-scroll min-w-screen scrollbar scrollbar-thumb-primary scrollbar-track-accent"
>
{ children... }
Expand Down
34 changes: 18 additions & 16 deletions site/smoketests/custom_events_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package smoketests

import (
"strconv"
"testing"
"time"

"github.com/stretchr/testify/assert"
)
Expand All @@ -13,22 +15,22 @@ func TestExampleCustomEvents(t *testing.T) {
assert.NotNil(t, page)

t.Run("observe custom event", func(t *testing.T) {
evt := "myevent"

// setup listener
page.MustEval(`() => {
addEventListener('` + evt + `', function(event) {
window.__CUSTOM_EVENT = event;
});
}
`)

// wait until an event is captured in global scope
page.MustWait(`() => (window.__CUSTOM_EVENT !== undefined && window.__CUSTOM_EVENT !== undefined)`)

// capture event details
result := page.MustEval(`() => window.__CUSTOM_EVENT.detail`).Str()

assert.Contains(t, result, "eventTime")
evtCountEl := page.MustElement("#eventCount")

count := func() int {
evtCountRaw := evtCountEl.MustText()
evtCount, err := strconv.Atoi(evtCountRaw)
assert.NoError(t, err)
return evtCount
}

prev := count()
for i := 0; i < 2; i++ {
time.Sleep(1 * time.Second)
evtCountAgain := count()
assert.Greater(t, evtCountAgain, prev)
prev = evtCountAgain
}
})
}
2 changes: 1 addition & 1 deletion site/static/md/examples/active_search.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The interesting part is the input field:
```html
<input
data-bind="search"
data-on-input:debounce_1000ms="sse('/examples/active_search/data')"
data-on-input__debounce.1000ms="sse('/examples/active_search/data')"
placeholder="Search..."
type="text"
/>
Expand Down
12 changes: 4 additions & 8 deletions site/static/md/examples/bind_keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

## Demo

<h1 data-on-keydown:window:ctrl-key:key_k="alert('you hit the cheat code!')">Press Ctrl+K</h1>
<h1 data-on-keydown:window:key_enter="alert('you hit the other code!')">Press Enter</h1>
<h1 data-on-keydown__window="evt.ctrlKey && evt.key =='k' && alert('you hit the cheat code!')">Press Ctrl+K</h1>
<h1 data-on-keydown__window="evt.key == 'Enter' && alert('you hit the other code!')">Press Enter</h1>

## Explanation

```html
<h1 data-on-keydown:window:ctrl-key:key_k="alert('you hit the cheat code!')">
Press Ctrl+K
</h1>
<h1 data-on-keydown:window:key_enter="alert('you hit the other code!')">
Press Enter
</h1>
<h1 data-on-keydown__window="evt.ctrlKey && evt.key =='k' && alert('you hit the cheat code!')">Press Ctrl+K</h1>
<h1 data-on-keydown__window="evt.key == 'Enter' && alert('you hit the other code!')">Press Enter</h1>
```

Able to bind to any value on the `event`. Because of how `data-*` attributes are interpreted you'll need to use `kebab-case` for `camelCase` attributes.
4 changes: 2 additions & 2 deletions site/static/md/examples/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<div
data-signals="{count:0}"
data-on-raf:throttle_500ms="count.value++"
data-on-raf__throttle.500ms="count.value++"
data-computed-blinker="count.value % 2 === 0"
>
<div data-text="count.value">Count</div>
Expand All @@ -18,7 +18,7 @@
```html
<div
data-signals="{count:0}"
data-on-raf:throttle_500ms="count.value++"
data-on-raf__throttle.500ms="count.value++"
data-computed-blinker="count.value % 2 === 0"
>
<div data-text="count.value">Count</div>
Expand Down
2 changes: 1 addition & 1 deletion site/static/md/examples/custom_events.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<div data-signals="{eventCount:0,eventDetails:''}">
<div id="foo" data-on-myevent="eventDetails.value=evt.detail;eventCount.value++">
<div>Event count: <span data-text="eventCount.value">EventCount</span></div>
<div>Event count: <span id="eventCount" data-text="eventCount.value">EventCount</span></div>
<div>Last Event Details: <span data-text="eventDetails.value">EventTime</span></div>
</div>
<script>
Expand Down
Loading
Loading