Skip to content

Commit bb04f46

Browse files
authored
Fix datastar examples (#3814)
1 parent 704ee92 commit bb04f46

File tree

9 files changed

+41
-65
lines changed

9 files changed

+41
-65
lines changed

docs/reference/datastar-sdk/index.md

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ import zio.http.template2._
107107
import zio.http.endpoint.Endpoint
108108

109109
body(
110-
dataOnLoad := Js("@get('/hello-world')"),
110+
dataOnLoad := js"@get('/hello-world')",
111111
dataOn.load := Endpoint(Method.GET / "hello-world").out[String].datastarRequest(()),
112112
div(
113113
className := "container",
@@ -237,7 +237,7 @@ div(
237237
h1("👋 Greeting Form 👋"),
238238
form(
239239
id("greetingForm"),
240-
dataOn.submit := Js("@get('/greet', {contentType: 'form'})"),
240+
dataOn.submit := js"@get('/greet', {contentType: 'form'})",
241241
label(`for`("name"), "What's your name?"),
242242
input(`type`("text"), id("name"), name("name"), placeholder("Enter your name!"), required, autofocus),
243243
button(`type`("submit"), "Greet me!"),
@@ -281,7 +281,7 @@ Assume you call the `/hello-world` endpoint that streams a "Hello, World!" messa
281281

282282
```scala mdoc:compile-only
283283
body(
284-
dataOn.load := Js("@get('/hello-world')"),
284+
dataOn.load := js"@get('/hello-world')",
285285
div(
286286
className := "container",
287287
h1("Hello World Example"),
@@ -563,7 +563,7 @@ utils.printSource("zio-http-example/src/main/scala/example/datastar/ServerTimeEx
563563

564564
**How it works:**
565565

566-
The page displays a time value using `dataText := Js("$currentTime")`, which binds the text content of a span element to the `currentTime` signal. The signal is declared with `dataSignals(Signal[String]("currentTime"))` and initialized to an empty string. When the page loads (`dataOn.load := Js("@get('/server-time')")`), it establishes an SSE connection to the `/server-time` endpoint.
566+
The page displays a time value using `dataText := $currentTime)`, which binds the text content of a span element to the `currentTime` signal. The signal is declared with `dataSignals(Signal[String]("currentTime"))` and initialized to an empty string. When the page loads (`dataOn.load := Js("@get('/server-time')")`), it establishes an SSE connection to the `/server-time` endpoint.
567567

568568
The server handler uses ZIO's scheduling capabilities to create a repeating effect that runs every second. Each second, the server:
569569
1. Gets the current time from the clock
@@ -583,22 +583,17 @@ utils.printSource("zio-http-example/src/main/scala/example/datastar/GreetingForm
583583

584584
**How it works:**
585585

586-
The page contains a form with an input field for the user's name. The form uses `dataOn.submit := Js("@get('/greet', {contentType: 'form'})")` to intercept the submit event and send a GET request with the form data. The `{contentType: 'form'}` option tells Datastar to serialize the form fields as query parameters.
586+
The page contains a form with an input field for the user's name. The form uses `dataOn.submit := js"@get('/greet', {contentType: 'form'})"` to intercept the submit event and send a GET request with the form data. The `{contentType: 'form'}` option tells Datastar to serialize the form fields as query parameters.
587587

588588
Unlike the streaming examples, the server responds with a single HTML fragment (not SSE):
589589

590590
```scala
591-
Response(
592-
headers = Headers(Header.ContentType(MediaType.text.`html`)),
593-
body = Body.fromCharSequence(
594-
div(id("greeting"), p(s"Hello ${req.queryParam("name").getOrElse("Guest")}")).render
595-
)
596-
)
591+
event(handler((_: Request) => DatastarEvent.patchElements(indexPage)))
597592
```
598593

599594
The response is a `text/html` fragment containing a div with `id="greeting"`. Datastar automatically finds the existing `<div id="greeting">` in the DOM and morphs it with the new content, displaying the personalized greeting.
600595

601-
The interaction is smooth and partial—only the greeting div updates, not the entire page. This pattern is useful for traditional CRUD operations where you don't need continuous streaming but want the benefits of hypermedia-driven updates.
596+
The interaction is smooth and partial—only the greeting div updates, not the entire page.
602597

603598
### Fruit Explorer Example
604599

@@ -612,7 +607,7 @@ utils.printSource("zio-http-example/src/main/scala/example/datastar/FruitExplore
612607

613608
The page contains a single input field with two key attributes:
614609
1. `dataBind("query")` - Binds the input value to a `$query` signal
615-
2. `dataOn.input.debounce(300.millis) := Js("@get('/search?q=' + $query)")` - Triggers a search request 300ms after the user stops typing
610+
2. `dataOn.input.debounce(300.millis) := js"@get('/search?q=' + ${$query})"` - Triggers a search request 300ms after the user stops typing
616611

617612
The debouncing prevents excessive server requests while typing. Each keystroke updates the `$query` signal, but the search only fires after a 300ms pause, reducing server load and providing a smoother UX.
618613

zio-http-example/src/main/scala/example/datastar/FruitExplorerExample.scala

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,8 @@ object FruitExplorerExample extends ZIOAppDefault {
5757
meta(charset("UTF-8")),
5858
meta(name("viewport"), content("width=device-width, initial-scale=1.0")),
5959
title("Fruit Explorer Example - ZIO HTTP Datastar"),
60-
script(
61-
`type` := "module",
62-
src := "https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js",
63-
),
64-
style.inlineCss("""
60+
datastarScript,
61+
style.inlineCss(css"""
6562
::view-transition-old(root),
6663
::view-transition-new(root) {
6764
animation-duration: 0.8s;
@@ -162,7 +159,7 @@ object FruitExplorerExample extends ZIOAppDefault {
162159
name := "query",
163160
dataSignals($query) := "",
164161
dataBind($query.name),
165-
dataOn.input.debounce(300.millis) := Js("@get('/search?q=' + $query)"),
162+
dataOn.input.debounce(300.millis) := js"@get('/search?q=' + ${$query})",
166163
autofocus,
167164
)
168165
},

zio-http-example/src/main/scala/example/datastar/GreetingFormExample.scala

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,17 @@ import zio.http.template2._
99
object GreetingFormExample extends ZIOAppDefault {
1010

1111
val routes: Routes[Any, Response] = Routes(
12-
Method.GET / "" -> handler {
13-
Response(
14-
headers = Headers(
15-
Header.ContentType(MediaType.text.html),
16-
),
17-
body = Body.fromCharSequence(indexPage.render),
18-
)
19-
},
20-
Method.GET / "greet" -> handler { (req: Request) =>
21-
Response(
22-
headers = Headers(
23-
Header.ContentType(MediaType.text.`html`),
24-
),
25-
body = Body.fromCharSequence(
12+
Method.GET / "" ->
13+
event(handler((_: Request) => DatastarEvent.patchElements(indexPage))),
14+
Method.GET / "greet" -> event {
15+
handler { (req: Request) =>
16+
DatastarEvent.patchElements(
2617
div(
2718
id("greeting"),
2819
p(s"Hello ${req.queryParam("name").getOrElse("Guest")}"),
29-
).render,
30-
),
31-
)
20+
),
21+
)
22+
}
3223
} @@ Middleware.debug,
3324
)
3425

@@ -37,10 +28,7 @@ object GreetingFormExample extends ZIOAppDefault {
3728
meta(charset("UTF-8")),
3829
meta(name("viewport"), content("width=device-width, initial-scale=1.0")),
3930
title("Greeting Form - ZIO HTTP Datastar"),
40-
script(
41-
`type` := "module",
42-
src := "https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js",
43-
),
31+
datastarScript,
4432
style.inlineCss(css),
4533
),
4634
body(
@@ -49,7 +37,7 @@ object GreetingFormExample extends ZIOAppDefault {
4937
h1("👋 Greeting Form 👋"),
5038
form(
5139
id("greetingForm"),
52-
dataOn.submit := Js("@get('/greet', {contentType: 'form'})"),
40+
dataOn.submit := js"@get('/greet', {contentType: 'form'})",
5341
label(
5442
`for`("name"),
5543
"What's your name?",

zio-http-example/src/main/scala/example/datastar/HelloWorldWithCustomDelayExample.scala

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,7 @@ object HelloWorldWithCustomDelayExample extends ZIOAppDefault {
5757
meta(charset("UTF-8")),
5858
meta(name("viewport"), content("width=device-width, initial-scale=1.0")),
5959
title("Datastar Hello World - ZIO HTTP Datastar"),
60-
script(
61-
`type` := "module",
62-
src := "https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js",
63-
),
60+
datastarScript,
6461
style.inlineCss(css),
6562
),
6663
body(
@@ -87,7 +84,7 @@ object HelloWorldWithCustomDelayExample extends ZIOAppDefault {
8784
.serve(routes)
8885
.provide(Server.default)
8986

90-
val css = """
87+
val css = css"""
9188
body {
9289
display: flex;
9390
flex-direction: row;

zio-http-example/src/main/scala/example/datastar/ServerTimeExample.scala

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,9 @@ object ServerTimeExample extends ZIOAppDefault {
1414
val timeHTML = html(
1515
head(
1616
title("Server Time - Datastar"),
17-
script(
18-
`type` := "module",
19-
src := "https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js",
20-
),
17+
datastarScript,
2118
style.inlineCss(
22-
"""
19+
css"""
2320
body {
2421
display: flex;
2522
justify-content: center;
@@ -111,14 +108,10 @@ object ServerTimeExample extends ZIOAppDefault {
111108
}
112109

113110
val routes = Routes(
114-
Method.GET / Root -> handler(
115-
Response(
116-
status = Status.Ok,
117-
headers = Headers(Header.ContentType(MediaType.text.html)),
118-
body = Body.fromString(timeHTML.render),
119-
),
120-
),
121-
Method.GET / "server-time" -> serverTimeHandler,
111+
Method.GET / Root ->
112+
event(handler((_: Request) => DatastarEvent.patchElements(timeHTML))),
113+
Method.GET / "server-time" ->
114+
serverTimeHandler,
122115
)
123116

124117
override def run =

zio-http-example/src/main/scala/example/datastar/SimpleHelloWorldExample.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ object SimpleHelloWorldExample extends ZIOAppDefault {
5656
.serve(routes)
5757
.provide(Server.default)
5858

59-
val css = """
59+
val css = css"""
6060
body {
6161
display: flex;
6262
flex-direction: column;

zio-http/shared/src/main/scala-2/zio/http/template2/JSInterpolatorMacros.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ object JSInterpolatorMacros {
5858
val trimmed = js.trim
5959
if (trimmed.isEmpty) return true
6060

61+
// Check for Datastar expressions (e.g., @get('url'), @post('url'), etc.)
62+
val datastarPattern = """@[a-zA-Z_][a-zA-Z0-9_]*\s*\([^)]*\)""".r
63+
if (datastarPattern.findFirstMatchIn(trimmed).isDefined) return true
64+
6165
// Basic JavaScript validation patterns
6266
val jsKeywords = Set(
6367
"var",
@@ -104,6 +108,6 @@ object JSInterpolatorMacros {
104108

105109
validJsPatterns.exists(_.findFirstMatchIn(trimmed).isDefined) ||
106110
jsKeywords.exists(keyword => trimmed.contains(keyword)) ||
107-
"""^[a-zA-Z0-9_$\s.(){}\[\];:,'"+-=<>!&|*/%]+$""".r.findFirstMatchIn(trimmed).isDefined
111+
"""^[a-zA-Z0-9_$\s.(){}\[\];:,'"+-=<>!&|*/%@]+$""".r.findFirstMatchIn(trimmed).isDefined
108112
}
109113
}

zio-http/shared/src/main/scala-3/zio/http/template2/JsInterpolatorMacros.scala

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ object JsInterpolatorMacros {
5151
val trimmed = js.trim
5252
if (trimmed.isEmpty) return true
5353

54+
// Check for Datastar expressions (e.g., @get('url'), @post('url'), etc.)
55+
val datastarPattern = """@[a-zA-Z_][a-zA-Z0-9_]*\s*\([^)]*\)""".r
56+
if (datastarPattern.matches(trimmed) || datastarPattern.findFirstMatchIn(trimmed).isDefined) return true
57+
5458
// Basic JavaScript validation patterns
5559
val jsKeywords = Set(
5660
"var", "let", "const", "function", "if", "else", "for", "while", "do", "switch", "case",
@@ -70,6 +74,6 @@ object JsInterpolatorMacros {
7074

7175
validJsPatterns.exists(_.matches(trimmed)) ||
7276
jsKeywords.exists(keyword => trimmed.contains(keyword)) ||
73-
trimmed.matches("""^[a-zA-Z0-9_$\s.(){}\[\];:,'"+-=<>!&|*/%]+$""")
77+
trimmed.matches("""^[a-zA-Z0-9_$\s.(){}\[\];:,'"+-=<>!&|*/%@]+$""")
7478
}
7579
}

zio-http/shared/src/main/scala/zio/http/template2/Css.scala

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,4 @@ sealed abstract case class Css(value: String) {
55
def stripMargin: Css = Css(value.stripMargin)
66
}
77

8-
object Css {
9-
private[template2] def apply(value: String): Css = new Css(value) {}
10-
}
8+
object Css { def apply(value: String): Css = new Css(value) {} }

0 commit comments

Comments
 (0)