@zio.dev/zio-blocks 0.0.33 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/adr/2026-07-18-data-migration.md +123 -0
- package/guides/async-getting-started.md +687 -0
- package/guides/compile-time-resource-safety-with-scope.md +21 -16
- package/guides/getting-started-with-mux.md +1395 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +640 -165
- package/guides/sql-checked-interpolation.md +173 -0
- package/guides/sql-transactions.md +286 -0
- package/guides/telemetry-guide.md +1130 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +248 -389
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +1499 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config/config-decoder.md +460 -0
- package/reference/config/config-source.md +489 -0
- package/reference/config/errors.md +278 -0
- package/reference/config/flags.md +369 -0
- package/reference/config/formats.md +314 -0
- package/reference/config/index.md +304 -0
- package/reference/config/rollout.md +336 -0
- package/reference/context.md +9 -52
- package/reference/data-migration.md +269 -0
- package/reference/datastar/attributes.md +302 -0
- package/reference/datastar/events.md +234 -0
- package/reference/datastar/index.md +256 -0
- package/reference/datastar/signals.md +230 -0
- package/reference/datastar/sse.md +295 -0
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/bulk-creation.md +96 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +745 -0
- package/reference/endpoint/path-codec.md +225 -0
- package/reference/endpoint/route-pattern.md +194 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +199 -0
- package/reference/html.md +1424 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +807 -0
- package/reference/htmx/response-headers.md +240 -0
- package/reference/http-model/headers.md +735 -0
- package/reference/http-model/index.md +49 -0
- package/reference/http-model/model.md +1517 -0
- package/reference/http-model/schema-codecs.md +522 -0
- package/reference/http-model/schema.md +750 -0
- package/reference/http-model/server-sent-event.md +341 -0
- package/reference/jwt.md +195 -0
- package/reference/maybe.md +943 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.md +254 -0
- package/reference/mux.mdx +828 -0
- package/reference/openapi.md +1351 -0
- package/reference/projection.md +654 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -98
- package/reference/resource-management/scope.md +28 -220
- package/reference/resource-management/wire.md +5 -55
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +185 -0
- package/reference/ringbuffer/mpsc.mdx +164 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +416 -0
- package/reference/{allows.md → schema/allows.md} +4 -100
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +3 -4
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +510 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +11 -11
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +196 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +52 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +167 -72
- package/reference/schema/reflect-transformer.md +140 -0
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/schema/schema-search.md +263 -0
- package/reference/{schema.md → schema/schema.md} +22 -2
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +1032 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +148 -0
- package/reference/sql/db-tx.md +114 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +288 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +363 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/core/index.md +32 -0
- package/reference/streams/core/pipeline.md +854 -0
- package/reference/streams/core/sink.md +1404 -0
- package/reference/streams/core/stream.md +3236 -0
- package/reference/streams/execution-and-compatibility/async-execution.md +822 -0
- package/reference/streams/execution-and-compatibility/index.md +35 -0
- package/reference/streams/execution-and-compatibility/platform-differences.md +297 -0
- package/reference/streams/execution-and-compatibility/scala-2-compatibility.md +88 -0
- package/reference/streams/execution-and-compatibility/zero-boxing.md +393 -0
- package/reference/streams/index.md +726 -0
- package/reference/streams/primitives/index.md +30 -0
- package/reference/streams/primitives/reader.md +1992 -0
- package/reference/streams/primitives/writer.md +1201 -0
- package/reference/telemetry/common/any-value.md +90 -0
- package/reference/telemetry/common/attribute-key.md +87 -0
- package/reference/telemetry/common/attributes.md +118 -0
- package/reference/telemetry/common/index.md +39 -0
- package/reference/telemetry/common/instrumentation-scope.md +24 -0
- package/reference/telemetry/common/resource.md +34 -0
- package/reference/telemetry/index.md +311 -0
- package/reference/telemetry/logging/index.md +197 -0
- package/reference/telemetry/logging/log-enrichment.md +72 -0
- package/reference/telemetry/logging/log-formatter.md +100 -0
- package/reference/telemetry/logging/log-record-processor.md +56 -0
- package/reference/telemetry/logging/log-record.md +44 -0
- package/reference/telemetry/logging/log-writer.md +64 -0
- package/reference/telemetry/logging/logger-provider.md +142 -0
- package/reference/telemetry/logging/logger.md +83 -0
- package/reference/telemetry/logging/severity.md +62 -0
- package/reference/telemetry/metrics/index.md +150 -0
- package/reference/telemetry/metrics/instruments.md +183 -0
- package/reference/telemetry/metrics/labeled-instruments.md +74 -0
- package/reference/telemetry/metrics/meter-provider.md +76 -0
- package/reference/telemetry/metrics/meter.md +98 -0
- package/reference/telemetry/metrics/metric-data.md +57 -0
- package/reference/telemetry/otel/custom-exporter.md +216 -0
- package/reference/telemetry/otel/index.md +212 -0
- package/reference/telemetry/tracing/index.md +155 -0
- package/reference/telemetry/tracing/sampler.md +89 -0
- package/reference/telemetry/tracing/span-builder.md +57 -0
- package/reference/telemetry/tracing/span-context.md +39 -0
- package/reference/telemetry/tracing/span-data.md +32 -0
- package/reference/telemetry/tracing/span-kind.md +55 -0
- package/reference/telemetry/tracing/span-processor.md +53 -0
- package/reference/telemetry/tracing/span-status.md +47 -0
- package/reference/telemetry/tracing/span.md +117 -0
- package/reference/telemetry/tracing/tracer-provider.md +91 -0
- package/reference/telemetry/tracing/tracer.md +52 -0
- package/reference/typeid.md +5 -83
- package/sidebars.js +376 -43
- package/undocumented-report.md +528 -270
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: events
|
|
3
|
+
title: "Event Handlers"
|
|
4
|
+
sidebar_label: "Event Handlers"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`dataOn` opens the event side of the attribute DSL: sixteen predefined DOM events, fourteen chainable modifiers, and four case modifiers, all rendering into a single `data-on:<event>__<modifiers>` attribute. Four sibling builders cover triggers that are not DOM events — intersection, interval, signal patches, and initialization. Core types: `DataOn`, `PartialDataOn`, `EventModifier`, `CaseModifier`, `DataOnIntersect`, `DataOnInterval`, `DataOnSignalPatch`, `DataInit`. The shape of the builder:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
final class DataOn private (name: String, modifiers: Maybe[EventModifier], caseModifier: CaseModifier) {
|
|
11
|
+
def debounce(millis: Long): DataOn
|
|
12
|
+
def once: DataOn
|
|
13
|
+
def :=[T](value: T)(implicit toDatastarExpr: ToDatastarExpr[T]): Dom.Attribute
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Motivation
|
|
18
|
+
|
|
19
|
+
An event handler in Datastar is an attribute whose name encodes both the event and its options. `data-on:input__debounce.300ms` debounces; `data-on:click__once__prevent` fires once and calls `preventDefault`. The name is structured, order-insensitive between modifiers, and entirely stringly-typed on the wire.
|
|
20
|
+
|
|
21
|
+
Written by hand that is a lot of punctuation to get right, and a modifier that Datastar does not recognize is silently ignored — the handler still fires, just without the debouncing you thought you had. A search box that issues a request per keystroke looks like it works, right up until it doesn't.
|
|
22
|
+
|
|
23
|
+
`dataOn` turns the whole name into a method chain. The event is a method, each modifier is a method, and the attribute name is assembled for you. A modifier that does not exist is a compile error, and the rendered name is correct by construction.
|
|
24
|
+
|
|
25
|
+
## Quick Showcase
|
|
26
|
+
|
|
27
|
+
Chain modifiers before assigning the handler:
|
|
28
|
+
|
|
29
|
+
```scala
|
|
30
|
+
import zio.blocks.html._
|
|
31
|
+
import zio.http.datastar._
|
|
32
|
+
|
|
33
|
+
val term = Signal[String]("term")
|
|
34
|
+
|
|
35
|
+
val box = input(
|
|
36
|
+
dataBind(term),
|
|
37
|
+
dataOn.input.debounce(300) := js"@get('/search')"
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The modifier becomes part of the attribute name:
|
|
42
|
+
|
|
43
|
+
```scala
|
|
44
|
+
box.renderMinified
|
|
45
|
+
// res0: String = "<input data-bind:term data-on:input__debounce.300ms=\"@get('/search')\"/>"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Predefined Events
|
|
49
|
+
|
|
50
|
+
Sixteen events have dedicated methods, each returning a `DataOn` ready for modifiers or assignment:
|
|
51
|
+
|
|
52
|
+
| Category | Methods |
|
|
53
|
+
| -------- | ------- |
|
|
54
|
+
| Pointer | `dataOn.click`, `dataOn.mouseover`, `dataOn.mouseout`, `dataOn.mouseenter`, `dataOn.mouseleave` |
|
|
55
|
+
| Keyboard | `dataOn.keydown`, `dataOn.keyup`, `dataOn.keypress` |
|
|
56
|
+
| Form | `dataOn.submit`, `dataOn.input`, `dataOn.change`, `dataOn.focus`, `dataOn.blur` |
|
|
57
|
+
| Window | `dataOn.scroll`, `dataOn.resize`, `dataOn.load` |
|
|
58
|
+
|
|
59
|
+
Each renders as `data-on:<event>`:
|
|
60
|
+
|
|
61
|
+
```scala
|
|
62
|
+
import zio.blocks.html._
|
|
63
|
+
import zio.http.datastar._
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The method name is the event name — `dataOn.click`, `dataOn.submit`, `dataOn.input` — so nothing needs looking up:
|
|
67
|
+
|
|
68
|
+
```scala
|
|
69
|
+
button(dataOn.click := js"@post('/save')").renderMinified
|
|
70
|
+
// res2: String = "<button data-on:click=\"@post('/save')\"></button>"
|
|
71
|
+
form(dataOn.submit := js"@post('/submit')").renderMinified
|
|
72
|
+
// res3: String = "<form data-on:submit=\"@post('/submit')\"></form>"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `dataOn.apply` — any other event
|
|
76
|
+
|
|
77
|
+
For an event without a dedicated method, `dataOn(name)` takes it as a string. The name is validated, so a structurally invalid event name fails rather than rendering a broken attribute:
|
|
78
|
+
|
|
79
|
+
```scala
|
|
80
|
+
div(dataOn("animationend") := js"@get('/done')").renderMinified
|
|
81
|
+
// res4: String = "<div data-on:animationend=\"@get('/done')\"></div>"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`dataOn` on its own is a `PartialDataOn` — the builder that the event methods live on — so `dataOn` alone is not a complete attribute.
|
|
85
|
+
|
|
86
|
+
## Event Modifiers
|
|
87
|
+
|
|
88
|
+
Fourteen modifiers chain before `:=`, each contributing a `__` suffix.
|
|
89
|
+
|
|
90
|
+
| Method | Renders | Effect |
|
|
91
|
+
| ------ | ------- | ------ |
|
|
92
|
+
| `debounce(ms)` | `__debounce.<ms>ms` | Fire after quiet period |
|
|
93
|
+
| `debounceLeading(ms)` | `__debounce.<ms>ms.leading` | Fire immediately, then suppress |
|
|
94
|
+
| `throttle(ms)` | `__throttle.<ms>ms` | At most once per interval |
|
|
95
|
+
| `throttleLeading(ms)` | `__throttle.<ms>ms.leading` | Leading-edge throttle |
|
|
96
|
+
| `delay(ms)` | `__delay.<ms>ms` | Wait before firing |
|
|
97
|
+
| `DataOn#once` | `__once` | Fire at most once |
|
|
98
|
+
| `DataOn#passive` | `__passive` | Passive listener |
|
|
99
|
+
| `DataOn#capture` | `__capture` | Capture phase |
|
|
100
|
+
| `DataOn#stop` | `__stop` | `stopPropagation` |
|
|
101
|
+
| `DataOn#prevent` | `__prevent` | `preventDefault` |
|
|
102
|
+
| `DataOn#outside` | `__outside` | Fire on events outside the element |
|
|
103
|
+
| `DataOn#window` | `__window` | Listen on the window object |
|
|
104
|
+
| `DataOn#document` | `__document` | Listen on the document object |
|
|
105
|
+
| `DataOn#viewTransition` | `__viewTransition` | Wrap the resulting patch in a view transition |
|
|
106
|
+
|
|
107
|
+
Modifiers combine, and the rendered name carries each one:
|
|
108
|
+
|
|
109
|
+
```scala
|
|
110
|
+
import zio.blocks.html._
|
|
111
|
+
import zio.http.datastar._
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A click that fires once and suppresses the default action needs two, `DataOn#once` and `DataOn#prevent`:
|
|
115
|
+
|
|
116
|
+
```scala
|
|
117
|
+
button(dataOn.click.once.prevent := js"@post('/subscribe')").renderMinified
|
|
118
|
+
// res6: String = "<button data-on:click__once__prevent=\"@post('/subscribe')\"></button>"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`DataOn#outside` is the one worth knowing about: it inverts the target, firing when the event happens anywhere *but* this element — which is how a dropdown closes when you click away:
|
|
122
|
+
|
|
123
|
+
```scala
|
|
124
|
+
div(dataOn.click.outside := js"$$open = false").renderMinified
|
|
125
|
+
// res7: String = "<div data-on:click__outside=\"$open = false\"></div>"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Repeating a timing or target modifier does not stack: the chain is normalized to the last effective value before the name is rendered, so chaining `DataOn#debounce` twice keeps only the second value.
|
|
129
|
+
|
|
130
|
+
Modifiers are represented by the `EventModifier` ADT — `Debounce`, `Throttle`, `Delay`, `Once`, `Passive`, `Capture`, `Stop`, `Prevent`, `Outside`, `Window`, `Document`, `ViewTransition`, and `And` for combination — but the builder methods are the intended interface.
|
|
131
|
+
|
|
132
|
+
Each trigger has its own modifier ADT, and they are not interchangeable: `EventModifier` for `dataOn`, `IntersectModifier` for `dataOnIntersect` (which adds `Half`, `Full`, `Exit`, and `Threshold`), `OnIntervalModifier` for `dataOnInterval` (`Duration`, `ViewTransition`), `OnSignalPatchModifier` for `dataOnSignalPatch` (`Delay`, `Debounce`, `Throttle`), and `InitModifier` for `dataInit` (`Delay`, `ViewTransition`). Every one of them has an `And` variant, which is how a chain of builder calls accumulates. You never need to construct these directly — the builder methods do it — but they are what a modifier chain is made of.
|
|
133
|
+
|
|
134
|
+
## Case Modifiers
|
|
135
|
+
|
|
136
|
+
`CaseModifier` controls how the event name is cased in the rendered attribute. Its four variants — `Camel`, `Kebab`, `Snake`, and `Pascal` — are selected by the same-named builder methods `DataOn#camel`, `DataOn#kebab`, `DataOn#snake`, and `DataOn#pascal`. The suffix appears only when the requested case differs from the builder's default, which for `dataOn` is kebab:
|
|
137
|
+
|
|
138
|
+
```scala
|
|
139
|
+
import zio.blocks.html._
|
|
140
|
+
import zio.http.datastar._
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
A custom event whose real name is camelCase needs the modifier. Note what it actually does: the attribute key stays kebab-cased either way, and the modifier is what tells Datastar to convert it back:
|
|
144
|
+
|
|
145
|
+
```scala
|
|
146
|
+
div(dataOn("myCustomEvent").camel := js"@get('/handle')").renderMinified
|
|
147
|
+
// res9: String = "<div data-on:my-custom-event__case.camel=\"@get('/handle')\"></div>"
|
|
148
|
+
div(dataOn("myCustomEvent") := js"@get('/handle')").renderMinified
|
|
149
|
+
// res10: String = "<div data-on:my-custom-event=\"@get('/handle')\"></div>"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
So `DataOn#camel` does not change the rendered key — it appends `__case.camel` alongside it. The default differs per builder: `dataOn` defaults to kebab, while `dataSignals(signal)` defaults to camel, and a modifier matching the default is omitted. That is why the same case call is a no-op on one and meaningful on the other.
|
|
153
|
+
|
|
154
|
+
Note also that the non-DOM triggers render as their own hyphenated attribute names — `data-on-intersect`, not `data-on:intersect` — since the trigger is the attribute rather than a key on it.
|
|
155
|
+
|
|
156
|
+
## Non-DOM Triggers
|
|
157
|
+
|
|
158
|
+
Four builders fire on something other than a DOM event. Each has its own modifier set and its own `:=`.
|
|
159
|
+
|
|
160
|
+
### `dataOnIntersect` — element enters the viewport
|
|
161
|
+
|
|
162
|
+
Fires when the element becomes visible, which is the basis for infinite scroll and lazy loading:
|
|
163
|
+
|
|
164
|
+
| Modifier | Effect |
|
|
165
|
+
| -------- | ------ |
|
|
166
|
+
| `DataOnIntersect#once` | Fire only the first time |
|
|
167
|
+
| `half` | Require 50% visibility |
|
|
168
|
+
| `full` | Require 100% visibility |
|
|
169
|
+
| `exit` | Fire on leaving rather than entering |
|
|
170
|
+
| `threshold(pct)` | Require an explicit visibility fraction |
|
|
171
|
+
| `delay(ms)`, `debounce(ms)`, `throttle(ms)` | Timing control |
|
|
172
|
+
| `DataOnIntersect#viewTransition` | Wrap the patch in a view transition |
|
|
173
|
+
|
|
174
|
+
A sentinel element at the end of a list is the canonical use:
|
|
175
|
+
|
|
176
|
+
```scala
|
|
177
|
+
import zio.blocks.html._
|
|
178
|
+
import zio.http.datastar._
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Loading the next page once, when the sentinel is fully visible:
|
|
182
|
+
|
|
183
|
+
```scala
|
|
184
|
+
div(dataOnIntersect.once.full := js"@get('/page/2')").renderMinified
|
|
185
|
+
// res12: String = "<div data-on-intersect__once__full=\"@get('/page/2')\"></div>"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `dataOnInterval` — fire on a timer
|
|
189
|
+
|
|
190
|
+
Polls on an interval, with `DataOnInterval#duration` setting the period, `DataOnInterval#durationLeading` firing immediately as well, and `DataOnInterval#viewTransition` wrapping the resulting patch:
|
|
191
|
+
|
|
192
|
+
```scala
|
|
193
|
+
div(dataOnInterval.duration(5000) := js"@get('/status')").renderMinified
|
|
194
|
+
// res13: String = "<div data-on-interval__duration.5000ms=\"@get('/status')\"></div>"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Without a `DataOnInterval#duration`, the attribute renders bare and Datastar applies its default interval.
|
|
198
|
+
|
|
199
|
+
### `dataOnSignalPatch` — react to signal changes
|
|
200
|
+
|
|
201
|
+
Fires when signals are patched, with `DataOnSignalPatch#delay`, `DataOnSignalPatch#debounce`, and `DataOnSignalPatch#throttle` for timing. Pair it with `dataOnSignalPatchFilter` to narrow which signals count:
|
|
202
|
+
|
|
203
|
+
```scala
|
|
204
|
+
div(dataOnSignalPatch.debounce(200) := js"@get('/recalculate')").renderMinified
|
|
205
|
+
// res14: String = "<div data-on-signal-patch__debounce.200ms=\"@get('/recalculate')\"></div>"
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `dataInit` — fire once on load
|
|
209
|
+
|
|
210
|
+
Runs when Datastar first processes the element, with `DataInit#delay` and `DataInit#viewTransition`:
|
|
211
|
+
|
|
212
|
+
```scala
|
|
213
|
+
div(dataInit := js"@get('/bootstrap')").renderMinified
|
|
214
|
+
// res15: String = "<div data-init=\"@get('/bootstrap')\"></div>"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
This is the hook for fetching initial content that is too expensive to render server-side on first paint.
|
|
218
|
+
|
|
219
|
+
## Choosing a Trigger
|
|
220
|
+
|
|
221
|
+
| You want to react to | Use |
|
|
222
|
+
| -------------------- | --- |
|
|
223
|
+
| A user interaction | `dataOn.<event>` |
|
|
224
|
+
| An event with no dedicated method | `dataOn("name")` |
|
|
225
|
+
| The element scrolling into view | `dataOnIntersect` |
|
|
226
|
+
| The passage of time | `dataOnInterval` |
|
|
227
|
+
| Signal state changing | `dataOnSignalPatch` |
|
|
228
|
+
| The page loading | `dataInit` |
|
|
229
|
+
|
|
230
|
+
## Integration Points
|
|
231
|
+
|
|
232
|
+
Every builder here ends in `:=`, which goes through `ToDatastarExpr` and returns a `Dom.Attribute` — see [Attributes](./attributes.md) for that type class and why raw `String` values are rejected. Handler expressions typically read and write [Signals](./signals.md) and call Datastar's `@get`/`@post` actions, which the server answers with [Server-Sent Events](./sse.md).
|
|
233
|
+
|
|
234
|
+
The `js"..."` interpolator producing those expressions, and the `Dom` types the attributes attach to, come from [HTML](../html.md).
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: index
|
|
3
|
+
title: "Datastar"
|
|
4
|
+
sidebar_label: "Datastar"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`zio.http.datastar` builds [Datastar](https://data-star.dev) hypermedia applications: it renders the `data-*` attributes that make a page reactive, and it produces the SSE events that patch that page from the server. Core types: `Signal`, `SignalUpdate`, `DatastarEvent`, `DataOn`, `DatastarAttrKey`, `ToDatastarExpr`, `ElementPatchMode`. The two halves of the module:
|
|
8
|
+
|
|
9
|
+
```scala
|
|
10
|
+
final class Signal[A] private (val name: String) {
|
|
11
|
+
def :=(value: A)(implicit schema: Schema[A]): SignalUpdate[A]
|
|
12
|
+
def ref: DatastarRef
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
sealed trait DatastarEvent {
|
|
16
|
+
def renderSSE: String
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Introduction
|
|
21
|
+
|
|
22
|
+
Datastar puts application state in the browser as named *signals*, and drives changes to it declaratively. A page says what it reacts to — "when this button is clicked, POST to `/increment`" — using `data-*` attributes, and the server replies with events that patch elements or signals in place. No client-side framework code, no JSON API to design, no separate view model.
|
|
23
|
+
|
|
24
|
+
This module is the Scala side of that contract. It gives you a typed way to write the attributes, a typed way to name and update signals, and a builder for each kind of SSE event, so both directions of the exchange are checked by the compiler rather than assembled from strings.
|
|
25
|
+
|
|
26
|
+
Two things it deliberately does not do: it does not ship a server, and it does not ship the Datastar JavaScript. It produces `Dom` attributes and SSE payload strings, which you serve with whatever HTTP layer you already have.
|
|
27
|
+
|
|
28
|
+
## Motivation
|
|
29
|
+
|
|
30
|
+
The natural way to write Datastar from a server language is string interpolation — `s"data-on-click=\"@post('/increment')\""` — and it goes wrong in the usual ways. A misspelled signal name fails silently in the browser. An attribute name typo produces an attribute Datastar ignores. A value that should be a Datastar expression gets a Scala `String`, which renders as a literal instead of an expression, and the page just does nothing.
|
|
31
|
+
|
|
32
|
+
This module closes each of those:
|
|
33
|
+
|
|
34
|
+
- **Signal names are validated, literals at compile time.** `Signal[Int]("count")` checks the name during compilation; `Signal.dynamic` defers the same check to runtime for names you compute.
|
|
35
|
+
- **Raw strings are rejected in expression positions.** `ToDatastarExpr` is deliberately ambiguous for `String`, so passing one is a compile error that names the fix. Expressions come from the `js"..."` interpolator or from typed signals.
|
|
36
|
+
- **Attributes are `Dom.Attribute` values**, so they compose with the rest of the HTML DSL and cannot be misplaced into text content. The helpers also settle the naming convention for you: plain attributes render with a hyphen (`data-text`), and keyed ones with a colon (`data-on:click`, `data-class:active`, `data-computed:total`).
|
|
37
|
+
- **SSE events are builders**, so each event kind offers the options it accepts and rendering emits the exact protocol field names. The one exception is `executeScript`, which returns an element-patch builder and so exposes `selector` and `mode` that do not apply to it.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Add the module to your build:
|
|
42
|
+
|
|
43
|
+
```scala
|
|
44
|
+
libraryDependencies += "dev.zio" %% "zio-blocks-datastar" % "0.0.55"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For Scala.js, use `%%%` instead of `%%`:
|
|
48
|
+
|
|
49
|
+
```scala
|
|
50
|
+
libraryDependencies += "dev.zio" %%% "zio-blocks-datastar" % "0.0.55"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Supported Scala versions: 2.13.x and 3.x.
|
|
54
|
+
|
|
55
|
+
:::note[The package is `zio.http.datastar`]
|
|
56
|
+
Despite living under `zio/blocks/datastar` in the source tree, the package is `zio.http.datastar`. The package object extends `DatastarAttributes`, so a single wildcard import brings every `data*` helper into scope.
|
|
57
|
+
:::
|
|
58
|
+
|
|
59
|
+
## Overview
|
|
60
|
+
|
|
61
|
+
The module divides along the direction data flows.
|
|
62
|
+
|
|
63
|
+
### Signals — state that lives in the browser
|
|
64
|
+
|
|
65
|
+
`Signal[A]` is a named, typed handle on a client-side signal. `Signal#:=` pairs it with a value to produce a `SignalUpdate[A]`, serialized through the type's JSON codec. `Signal#ref` produces a `DatastarRef`, the `$name` form used inside expressions. See [Signals](./signals.md).
|
|
66
|
+
|
|
67
|
+
### Attributes — declaring reactivity on the page
|
|
68
|
+
|
|
69
|
+
`DatastarAttributes` supplies 27 `data*` helpers — `dataText`, `dataShow`, `dataBind`, `dataClass`, `dataComputed`, and the rest — each returning either a `Dom.Attribute` or a `DatastarAttrKey` awaiting a value. `ToDatastarExpr` governs what may be assigned. See [Attributes](./attributes.md).
|
|
70
|
+
|
|
71
|
+
### Events — reacting to the user
|
|
72
|
+
|
|
73
|
+
`dataOn` opens the event side: sixteen predefined events, fourteen chainable modifiers for debouncing, throttling, and propagation control, and four specialized triggers for intersection, interval, signal-patch, and init. See [Event Handlers](./events.md).
|
|
74
|
+
|
|
75
|
+
### SSE — patching the page from the server
|
|
76
|
+
|
|
77
|
+
`DatastarEvent` builds the four server-to-browser events: patch elements, patch signals, execute a script, and remove elements. `ElementPatchMode` chooses where content lands, and `EventType` names the protocol event. See [Server-Sent Events](./sse.md).
|
|
78
|
+
|
|
79
|
+
## How They Work Together
|
|
80
|
+
|
|
81
|
+
A Datastar interaction is a loop, and the module sits on both ends of it:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
1. Server renders HTML html DSL + data* attributes
|
|
85
|
+
2. Browser becomes reactive Datastar reads data-* and wires up signals
|
|
86
|
+
3. User acts an event matching a data-on:* fires
|
|
87
|
+
4. Browser issues a request @post('/increment') from the expression
|
|
88
|
+
5. Server responds with SSE DatastarEvent.patchSignals / patchElements
|
|
89
|
+
6. Browser applies the patch signals update, elements morph in place
|
|
90
|
+
└─> back to 3
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The types involved at each end:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
BROWSER SIDE (rendered into HTML)
|
|
97
|
+
|
|
98
|
+
Signal[A] ──ref──> DatastarRef ──> "$count" used inside js"..." expressions
|
|
99
|
+
│
|
|
100
|
+
└─:=─> SignalUpdate[A] ──> {"count": 42} JSON via Schema[A].jsonCodec
|
|
101
|
+
|
|
102
|
+
dataOn.click ──> DataOn ──modifiers──> DataOn ──:=──> Dom.Attribute
|
|
103
|
+
│ data-on:click__debounce.300ms
|
|
104
|
+
├─ EventModifier (14: debounce, throttle,
|
|
105
|
+
│ once, passive, stop, …)
|
|
106
|
+
└─ CaseModifier (__case.camel | kebab | snake | pascal)
|
|
107
|
+
|
|
108
|
+
dataText / dataShow / dataClass(…) ──> DatastarAttrKey ──:=──> Dom.Attribute
|
|
109
|
+
▲
|
|
110
|
+
ToDatastarExpr guards the value:
|
|
111
|
+
js"…" and Signal ok, raw String rejected
|
|
112
|
+
|
|
113
|
+
SERVER SIDE (rendered into an SSE stream)
|
|
114
|
+
|
|
115
|
+
DatastarEvent.patchElements(dom) ──> PatchElementsBuilder ──renderSSE──> String
|
|
116
|
+
│ ├─ selector(CssSelector)
|
|
117
|
+
│ ├─ mode(ElementPatchMode)
|
|
118
|
+
│ ├─ viewTransition / namespace
|
|
119
|
+
│ └─ eventId / retry
|
|
120
|
+
├─ patchSignals(updates*) ──> PatchSignalsBuilder ├─ onlyIfMissing
|
|
121
|
+
├─ executeScript(js) ──────> PatchElementsBuilder
|
|
122
|
+
└─ removeElements(sel) ────> RemoveElementsBuilder
|
|
123
|
+
|
|
124
|
+
renderSSE delegates to zio.http.ServerSentEvent, so the result is
|
|
125
|
+
standard SSE with a Datastar-specific event name and data body.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Common Patterns
|
|
129
|
+
|
|
130
|
+
Four shapes cover most Datastar work.
|
|
131
|
+
|
|
132
|
+
### A Counter, End to End
|
|
133
|
+
|
|
134
|
+
The smallest complete loop: a signal, an attribute that displays it, a button that asks the server to change it, and an SSE event that does:
|
|
135
|
+
|
|
136
|
+
```scala
|
|
137
|
+
import zio.blocks.html._
|
|
138
|
+
import zio.http.datastar._
|
|
139
|
+
|
|
140
|
+
val count = Signal[Int]("count")
|
|
141
|
+
|
|
142
|
+
val page = div(
|
|
143
|
+
dataSignals(count := 0),
|
|
144
|
+
span(dataText := count),
|
|
145
|
+
button(dataOn.click := js"@post('/increment')", "increment")
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Rendering gives ordinary HTML that Datastar can read:
|
|
150
|
+
|
|
151
|
+
```scala
|
|
152
|
+
page.renderMinified
|
|
153
|
+
// res0: String = "<div data-signals=\"{"count": 0}\"><span data-text=\"$count\"></span><button data-on:click=\"@post('/increment')\">increment</button></div>"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The handler for `/increment` replies with a signal patch rather than a JSON body:
|
|
157
|
+
|
|
158
|
+
```scala
|
|
159
|
+
DatastarEvent.patchSignals(count := 1).renderSSE
|
|
160
|
+
// res1: String = """event: datastar-patch-signals
|
|
161
|
+
// data: signals {"count":1}
|
|
162
|
+
//
|
|
163
|
+
// """
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Deriving State Instead of Storing It
|
|
167
|
+
|
|
168
|
+
`dataComputed` defines a signal whose value is an expression over other signals, so the derived value never has to be kept in sync:
|
|
169
|
+
|
|
170
|
+
```scala
|
|
171
|
+
import zio.blocks.html._
|
|
172
|
+
import zio.http.datastar._
|
|
173
|
+
|
|
174
|
+
val price = Signal[Double]("price")
|
|
175
|
+
val quantity = Signal[Int]("quantity")
|
|
176
|
+
val total = Signal[Double]("total")
|
|
177
|
+
|
|
178
|
+
val form = div(
|
|
179
|
+
dataSignals(price := 9.99, quantity := 1),
|
|
180
|
+
dataComputed(total) := js"$$price * $$quantity",
|
|
181
|
+
span(dataText := total)
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
The computed attribute carries the expression, and the browser recomputes it whenever either input changes:
|
|
186
|
+
|
|
187
|
+
```scala
|
|
188
|
+
form.renderMinified
|
|
189
|
+
// res3: String = "<div data-signals=\"{"price": 9.99, "quantity": 1}\" data-computed:total=\"$price * $quantity\"><span data-text=\"$total\"></span></div>"
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Rate-Limiting a Chatty Event
|
|
193
|
+
|
|
194
|
+
Modifiers chain on `dataOn` before the handler is assigned, which is how a keystroke-driven search avoids one request per character:
|
|
195
|
+
|
|
196
|
+
```scala
|
|
197
|
+
import zio.blocks.html._
|
|
198
|
+
import zio.http.datastar._
|
|
199
|
+
|
|
200
|
+
val term = Signal[String]("term")
|
|
201
|
+
|
|
202
|
+
val search = input(
|
|
203
|
+
dataBind(term),
|
|
204
|
+
dataOn.input.debounce(300) := js"@get('/search')"
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The modifier becomes part of the attribute name, so the browser applies it without any JavaScript of yours:
|
|
209
|
+
|
|
210
|
+
```scala
|
|
211
|
+
search.renderMinified
|
|
212
|
+
// res5: String = "<input data-bind:term data-on:input__debounce.300ms=\"@get('/search')\"/>"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Patching a Fragment Rather Than the Page
|
|
216
|
+
|
|
217
|
+
`DatastarEvent.patchElements` with a selector and a mode replaces part of the page, leaving the rest — and its signal state — untouched:
|
|
218
|
+
|
|
219
|
+
```scala
|
|
220
|
+
import zio.blocks.html._
|
|
221
|
+
import zio.http.datastar._
|
|
222
|
+
|
|
223
|
+
val row = tr(td("Widget"), td("in stock"))
|
|
224
|
+
|
|
225
|
+
val event = DatastarEvent
|
|
226
|
+
.patchElements(row)
|
|
227
|
+
.selector(CssSelector.id("inventory"))
|
|
228
|
+
.mode(ElementPatchMode.Append)
|
|
229
|
+
.renderSSE
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The rendered event names the selector and mode as protocol fields:
|
|
233
|
+
|
|
234
|
+
```scala
|
|
235
|
+
event
|
|
236
|
+
// res7: String = """event: datastar-patch-elements
|
|
237
|
+
// data: selector #inventory
|
|
238
|
+
// data: mode append
|
|
239
|
+
// data: elements <tr><td>Widget</td><td>in stock</td></tr>
|
|
240
|
+
//
|
|
241
|
+
// """
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Integration Points
|
|
245
|
+
|
|
246
|
+
The module is a thin typed layer over three other blocks, and adds no dependency of its own:
|
|
247
|
+
|
|
248
|
+
- **`zio-blocks-html`** supplies `Dom`, `Dom.Attribute`, `Js`, `CssSelector`, and the `ToJs` type class. Every attribute helper returns a `Dom.Attribute`, so Datastar attributes compose with the HTML DSL exactly like `id` or `class` — see [HTML](../html.md).
|
|
249
|
+
- **`zio-blocks-schema`** supplies the JSON codec behind `Signal#:=`. A signal of type `A` needs a `Schema[A]`, and the serialized form is whatever that schema's JSON codec produces — see [Schema](../schema/index.md).
|
|
250
|
+
- **`zio-http-model`** supplies `ServerSentEvent`, which `DatastarEvent#renderSSE` delegates to for the wire format. The Datastar-specific part is the event name and the structured `data:` body — see [ServerSentEvent](../http-model/server-sent-event.md).
|
|
251
|
+
|
|
252
|
+
Within the module, the dependency direction is one-way: attributes and events both consume `Signal`, and neither knows about the other. Nothing in the SSE layer reads the attribute DSL.
|
|
253
|
+
|
|
254
|
+
## Next Steps
|
|
255
|
+
|
|
256
|
+
Start with [Signals](./signals.md), which every other page builds on, then [Attributes](./attributes.md) for the declarative surface and [Event Handlers](./events.md) for triggers. [Server-Sent Events](./sse.md) covers the server half.
|