Datastar SDK Examples
Running the Examples
All code from this reference is available as runnable examples in the zio-http-example module.
1. Clone the repository and navigate to the project:
git clone https://github.com/zio/zio-http.git
cd zio-http
2. Run individual examples with sbt:
Simple Hello World Example
This example demonstrates the most basic use of Datastar with ZIO HTTP by streaming a "Hello, world!" message to the browser. It's an excellent starting point for understanding how Server-Sent Events work with Datastar:
package example.datastar
import zio._
import zio.http._
import zio.http.datastar._
import zio.http.endpoint.Endpoint
import zio.http.template2._
object SimpleHelloWorldExample extends ZIOAppDefault {
val message = "Hello, world!"
val routes: Routes[Any, Response] = Routes(
// Main page route
Method.GET / Root -> handler {
Response(
headers = Headers(
Header.ContentType(MediaType.text.html),
),
body = Body.fromCharSequence(indexPage.render),
)
},
Method.GET / "hello-world" -> events {
handler {
ZIO.foreachDiscard(message.indices) { i =>
for {
_ <- ServerSentEventGenerator.executeScript(js"console.log('Sending substring(0, ${i + 1})')")
_ <- ServerSentEventGenerator.patchElements(div(id("message"), message.substring(0, i + 1)))
_ <- ZIO.sleep(100.millis)
} yield ()
}
}
},
)
def indexPage = html(
head(
meta(charset("UTF-8")),
meta(name("viewport"), content("width=device-width, initial-scale=1.0")),
title("Datastar Hello World - ZIO HTTP Datastar"),
datastarScript,
style.inlineCss(css),
),
body(
dataInit := Endpoint(Method.GET / "hello-world").out[String].datastarRequest(()),
div(
className := "container",
h1("Hello World Example"),
div(id("message")),
),
),
)
override def run: ZIO[Any, Throwable, Unit] =
Server
.serve(routes)
.provide(Server.default)
val css = css"""
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
font-family: system-ui, -apple-system, sans-serif;
font-size: 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
}
.container {
text-align: center;
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
max-width: 600px;
width: 100%;
}
h1 {
font-size: 3rem;
color: #333;
margin-bottom: 30px;
margin-top: 0;
}
#message {
font-size: 2rem;
margin-top: 2rem;
padding: 20px;
background: #f0f4ff;
border-left: 4px solid #667eea;
border-radius: 6px;
color: #333;
min-height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
"""
}
How it works:
The page uses the dataOn.load attribute to trigger a GET request to /hello-world as soon as the page loads. The server responds with a stream of SSE events, where each event patches a div element with id="message" to display progressively more characters of the message.
On the server side, the handler iterates through each character index of "Hello, world!" and for each iteration:
- Executes a console.log script on the client using
ServerSentEventGenerator.executeScriptto log the current character index - Patches the
#messagediv with a substring containing all characters up to the current index usingServerSentEventGenerator.patchElements - Waits 100 milliseconds before processing the next character
This creates a typewriter effect where the message appears one character at a time. The entire interaction happens without any custom JavaScript code - just declarative HTML attributes and server-side streaming.
Hello World with Custom Delay
This example builds upon the Simple Hello World example by adding user control over the animation speed, demonstrating how to use Datastar signals for bidirectional communication between client and server:
package example.datastar
import zio._
import zio.json._
import zio.http._
import zio.http.datastar._
import zio.http.endpoint.Endpoint
import zio.http.template2._
case class Delay(delay: Int)
object Delay {
implicit val jsonCodec: JsonCodec[Delay] = DeriveJsonCodec.gen
}
object HelloWorldWithCustomDelayExample extends ZIOAppDefault {
val message = "Hello, world!"
val routes: Routes[Any, Response] = Routes(
// Main page route
Method.GET / Root -> handler {
Response(
headers = Headers(
Header.ContentType(MediaType.text.html),
),
body = Body.fromCharSequence(indexPage.render),
)
},
Method.GET / "hello-world" -> events {
handler { (request: Request) =>
val delay = request.url.queryParams
.getAll("datastar")
.headOption
.flatMap { s =>
Delay.jsonCodec.decodeJson(s).toOption
}
.getOrElse(Delay(100))
ZIO.foreachDiscard(message.indices) { i =>
for {
_ <- ServerSentEventGenerator.executeScript(js"console.log('Sending substring(0, $i)')")
_ <- ServerSentEventGenerator.patchElements(
div(id("message"), message.substring(0, i + 1)),
)
_ <- ZIO.sleep(delay.delay.millis)
} yield ()
}
}
},
)
def indexPage =
html(
head(
meta(charset("UTF-8")),
meta(name("viewport"), content("width=device-width, initial-scale=1.0")),
title("Datastar Hello World - ZIO HTTP Datastar"),
datastarScript,
style.inlineCss(css),
),
body(
div(
className := "container",
h1("Hello World Example"), {
val $delay = Signal[Int]("delay")
div(
dataSignals($delay) := js"100",
label("Delay (ms): ", `for` := "delay"),
input(dataBind($delay.name), name := "delay", `type` := "number", step := "100"),
)
},
button(dataOn.click := Endpoint(Method.GET / "hello-world").out[String].datastarRequest(()))(
"Start Animation",
),
div(id("message")),
),
),
)
override def run: ZIO[Any, Throwable, Unit] =
Server
.serve(routes)
.provide(Server.default)
val css = css"""
body {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-family: system-ui, -apple-system, sans-serif;
font-size: 1.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
}
.container {
text-align: center;
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
h1 {
font-size: 3rem;
color: #333;
margin-bottom: 30px;
}
label {
display: inline-block;
margin-right: 10px;
color: #555;
font-weight: 500;
}
input[type="number"] {
padding: 8px 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.3s;
}
input[type="number"]:focus {
outline: none;
border-color: #667eea;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
font-size: 1.5rem;
padding: 1rem 2rem;
margin-top: 2rem;
border: none;
border-radius: 6px;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
#message {
font-size: 2rem;
margin-top: 2rem;
padding: 20px;
background: #f0f4ff;
border-left: 4px solid #667eea;
border-radius: 6px;
color: #333;
min-height: 50px;
}
#message:empty {
display: none;
}
"""
}
How it works:
The page declares a signal called delay using dataSignals(Signal[Int]("delay")) with an initial value of 100 milliseconds. This signal is bound to a number input field via dataBind("delay"), which means any changes to the input automatically update the $delay signal value. A button with dataOn.click := Js("@get('/hello-world')") triggers the animation when clicked.
The key difference from the simple example is how the server extracts and uses the delay value. When the button is clicked, Datastar automatically includes all current signal values in a query parameter named datastar. The server extracts this parameter, decodes it as JSON to get the Delay case class, and uses that value to control the sleep duration between character updates.
Users can adjust the delay value and restart the animation to see it play at different speeds, all without writing any JavaScript code.
Server Time Example
This example showcases real-time, server-pushed updates by streaming the current server time to the browser every second. It demonstrates a common pattern for live dashboards and monitoring applications.
package example.datastar
import java.time.format.DateTimeFormatter
import zio._
import zio.http._
import zio.http.datastar._
import zio.http.endpoint.Endpoint
import zio.http.template2._
object ServerTimeExample extends ZIOAppDefault {
val timeHTML = html(
head(
title("Server Time - Datastar"),
datastarScript,
style.inlineCss(
css"""
body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: system-ui, -apple-system, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.container {
text-align: center;
background: rgba(255, 255, 255, 0.1);
padding: 3rem;
border-radius: 1rem;
backdrop-filter: blur(10px);
}
h1 {
font-size: 2.5rem;
margin-bottom: 2rem;
}
.time-display {
font-size: 4rem;
font-weight: bold;
margin: 2rem 0;
font-family: 'Courier New', monospace;
}
button {
font-size: 1.2rem;
padding: 1rem 2rem;
margin: 0.5rem;
cursor: pointer;
border: none;
border-radius: 0.5rem;
background: white;
color: #667eea;
font-weight: bold;
transition: transform 0.2s;
}
button:hover {
transform: scale(1.05);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status {
margin-top: 1rem;
font-size: 1.2rem;
opacity: 0.8;
}
""".stripMargin,
),
),
body(
div(className := "container")(
h1("Live Server Time"), {
val $currentTime = Signal[String]("currentTime")
span(
dataSignals($currentTime) := js"'--:--:--'",
dataText := $currentTime,
className := "time-display",
dataOn.load := Endpoint(Method.GET / "server-time").out[String].datastarRequest(()),
)
},
),
),
)
// Time formatter
val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss")
// Server time streaming endpoint
val serverTimeHandler =
events {
handler {
ZIO.clock
.flatMap(_.currentDateTime)
.map(_.toLocalTime.format(timeFormatter))
.flatMap { currentTime =>
ZIO.logInfo(s"Sending time: $currentTime") *>
ServerSentEventGenerator.patchSignals(
s"{ 'currentTime': '$currentTime' }",
PatchSignalOptions(retryDuration = 5.seconds),
)
}
.schedule(Schedule.spaced(1.second))
.unit
}
}
val routes = Routes(
Method.GET / Root ->
event(handler((_: Request) => DatastarEvent.patchElements(timeHTML))),
Method.GET / "server-time" ->
serverTimeHandler,
)
override def run =
ZIO.logInfo("Starting server on http://localhost:8080") *>
Server.serve(routes).provide(Server.default)
}
How it works:
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.
The server handler uses ZIO's scheduling capabilities to create a repeating effect that runs every second. Each second, the server:
- Gets the current time from the clock
- Formats it as "HH:mm:ss"
- Patches the
currentTimesignal with the new value usingpatchSignals
The PatchSignalOptions configures a 5-second retry duration, meaning if the connection drops, the client will attempt to reconnect after 5 seconds. The signal patching sends JSON that updates only the specified signals without requiring a full page refresh.
Greeting Form Example
This example demonstrates single-shot (non-streaming) responses using traditional form submissions, showing how Datastar handles HTTP transactions alongside SSE streaming.
package example.datastar
import zio._
import zio.http._
import zio.http.datastar._
import zio.http.template2._
object GreetingFormExample extends ZIOAppDefault {
val routes: Routes[Any, Response] = Routes(
Method.GET / "" ->
event(handler((_: Request) => DatastarEvent.patchElements(indexPage))),
Method.GET / "greet" -> event {
handler { (req: Request) =>
DatastarEvent.patchElements(
div(
id("greeting"),
p(s"Hello ${req.queryParam("name").getOrElse("Guest")}"),
),
)
}
} @@ Middleware.debug,
)
def indexPage = html(
head(
meta(charset("UTF-8")),
meta(name("viewport"), content("width=device-width, initial-scale=1.0")),
title("Greeting Form - ZIO HTTP Datastar"),
datastarScript,
style.inlineCss(css),
),
body(
div(
className := "container",
h1("👋 Greeting Form 👋"),
form(
id("greetingForm"),
dataOn.submit := js"@get('/greet', {contentType: 'form'})",
label(
`for`("name"),
"What's your name?",
),
input(
`type`("text"),
id("name"),
name("name"),
placeholder("Enter your name!"),
required,
autofocus,
),
button(
`type`("submit"),
"Greet me!",
),
),
div(id("greeting")),
),
),
)
val css =
"""
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
h1 {
color: #333;
margin-bottom: 30px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
input[type="text"] {
width: 100%;
padding: 12px;
margin-bottom: 20px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
box-sizing: border-box;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 30px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#greeting {
margin-top: 30px;
padding: 20px;
background: #f0f4ff;
border-left: 4px solid #667eea;
border-radius: 6px;
font-size: 18px;
color: #333;
}
#greeting:empty {
display: none;
}
"""
override def run: ZIO[Any, Throwable, Unit] =
Server
.serve(routes)
.provide(Server.default)
}
How it works:
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.
Unlike the streaming examples, the server responds with a single HTML fragment (not SSE):
event(handler((_: Request) => DatastarEvent.patchElements(indexPage)))
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.
The interaction is smooth and partial—only the greeting div updates, not the entire page.
Fruit Explorer Example
This example demonstrates real-time search with debouncing and view transitions, showcasing advanced Datastar features for building sophisticated interactive UIs.
package example.datastar
import zio._
import zio.http._
import zio.http.datastar._
import zio.http.template2._
object FruitExplorerExample extends ZIOAppDefault {
val routes: Routes[Any, Response] = Routes(
// Main page route
Method.GET / "" -> handler {
Response(
headers = Headers(
Header.ContentType(MediaType.text.html),
),
body = Body.fromCharSequence(indexPage.render),
)
},
Method.GET / "search" -> events {
handler { (req: Request) =>
for {
searchTerm <- ZIO
.succeed(
req.url.queryParameters
.getAll("q")
.headOption,
)
results <- search(searchTerm)
_ <- ZIO.when(results.isEmpty)(
ServerSentEventGenerator.patchElements(div(id("result"), p("No results found."))),
)
_ <- ZIO.when(results.nonEmpty) {
ServerSentEventGenerator.patchElements(div(id("result"), ol(id("list")))) *>
ZIO.foreachDiscard(results) { r =>
ServerSentEventGenerator
.patchElements(
li(r),
PatchElementOptions(
selector = Some(CssSelector.id("list")),
mode = ElementPatchMode.Append,
useViewTransition = true,
),
)
.delay(100.millis)
}
}
} yield ()
}.orDie
} @@ Middleware.debug,
)
def indexPage = {
html(
head(
meta(charset("UTF-8")),
meta(name("viewport"), content("width=device-width, initial-scale=1.0")),
title("Fruit Explorer Example - ZIO HTTP Datastar"),
datastarScript,
style.inlineCss(css"""
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.8s;
}
::view-transition-old(root) {
animation-timing-function: ease-in-out;
}
::view-transition-new(root) {
animation-timing-function: ease-in-out;
}
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
h1 {
color: #333;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
input[type="text"] {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 16px;
transition: border-color 0.3s;
box-sizing: border-box;
}
input[type="text"]:focus {
outline: none;
border-color: #667eea;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 30px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
#greeting {
margin-top: 30px;
padding: 20px;
background: #f0f4ff;
border-left: 4px solid #667eea;
border-radius: 6px;
font-size: 18px;
color: #333;
}
#greeting:empty {
display: none;
}
"""),
),
body(
div(
className := "container",
h1("\uD83D\uDD0E Fruit Explorer \uD83C\uDF47"), {
val $query = Signal[String]("query")
input(
`type` := "text",
placeholder := "Search ...",
name := "query",
dataSignals($query) := "",
dataBind($query.name),
dataOn.input.debounce(300.millis) := js"@get('/search?q=' + ${$query})",
autofocus,
)
},
div(id("result")),
),
),
)
}
def search(term: Option[String]): Task[List[String]] = ZIO.succeed {
val data: List[String] = List(
"Apple",
"Banana",
"Orange",
"Mango",
"Strawberry",
"Grape",
"Watermelon",
"Pineapple",
"Peach",
"Cherry",
"Pear",
"Plum",
"Kiwi",
"Blueberry",
"Raspberry",
"Blackberry",
"Lemon",
"Lime",
"Grapefruit",
"Avocado",
"Coconut",
"Pomegranate",
"Apricot",
"Nectarine",
"Cantaloupe",
"Honeydew",
"Fig",
"Date",
"Persimmon",
"Mulberry",
"Quince",
"Melon",
"Greengage",
"Barberry",
"Bitter Orange",
"Sour Cherry",
)
if (term.isEmpty) Nil
else data.filter(_.toLowerCase.contains(term.get.toLowerCase))
}
override def run: ZIO[Any, Throwable, Unit] =
Server
.serve(routes)
.provide(Server.default)
}
How it works:
The page contains a single input field with two key attributes:
dataBind("query")- Binds the input value to a$querysignaldataOn.input.debounce(300.millis) := js"@get('/search?q=' + ${$query})"- Triggers a search request 300ms after the user stops typing
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.
The server handler extracts the search term from the query parameter and filters a fruit list. When results are found, it:
-
First patches an empty results container with
ServerSentEventGenerator.patchElements(div(id("result"), ol(id("list")))) -
Then streams each result as a separate list item with a 100ms delay between items:
ServerSentEventGenerator.patchElements(li(r),PatchElementOptions(selector = Some(CssSelector.id("list")),mode = ElementPatchMode.Append,useViewTransition = true)).delay(100.millis)
The PatchElementOptions are particularly interesting here:
selector = Some(CssSelector.id("list"))- Targets the specific list element by IDmode = ElementPatchMode.Append- Adds each item to the end of the list rather than replacing ituseViewTransition = true- Enables smooth CSS View Transitions API animations
The CSS includes view transition rules that create smooth fade-in effects:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.8s;
}
The result is a highly responsive search experience with beautiful animations, all controlled from the server without complex client-side state management.
Real-time Chat Example
For a more comprehensive example demonstrating multi-client real-time chat with ZIO Hub broadcasting, see the Real-time Chat with Datastar guide. This example showcases:
- Broadcasting messages to multiple connected clients using ZIO Hub
- Persistent SSE connections for real-time updates
- Two-way signal binding with form inputs
- Type-safe request handling with
readSignals[T]
Dispatch Event Example
This example demonstrates a complete multi-step data processing workflow with real-time progress updates via Server-Sent Events and custom event dispatching to coordinate client-side state changes. It shows how to combine SSE streaming for live feedback with event dispatch for final state coordination.
package example.datastar
import zio._
import zio.http._
import zio.http.datastar._
import zio.http.template2._
/**
* Datastar Dispatch Event — Complete Example with Server-Sent Events
*
* This complete example demonstrates a data processing workflow where:
* 1. Client initiates a long-running task via POST
* 2. Server streams SSE updates during processing
* 3. Server dispatches a custom event when complete
* 4. Client responds to the event with UI updates
*
* Run with: sbt "zioHttpExample/runMain
* example.datastar.DispatchEventCompleteExample"
*/
object DispatchEventCompleteExample extends ZIOAppDefault {
val routes: Routes[Any, Response] = Routes(
Method.GET / Root -> event {
handler { (_: Request) =>
DatastarEvent.patchElements(indexPage)
}
},
Method.POST / "api" / "data-processor" -> events {
handler {
for {
// Send initial log entry
_ <- ServerSentEventGenerator.patchElements(
div(className := "log-entry", "✓ Processing started"),
PatchElementOptions(
selector = Some(CssSelector.id("logContainer")),
mode = ElementPatchMode.Append,
),
)
_ <- ZIO.sleep(500.millis)
// Simulate processing step 1
_ <- ServerSentEventGenerator.patchElements(
div(className := "log-entry", "✓ Step 1: Validating data"),
PatchElementOptions(
selector = Some(CssSelector.id("logContainer")),
mode = ElementPatchMode.Append,
),
)
_ <- ZIO.sleep(800.millis)
// Simulate processing step 2
_ <- ServerSentEventGenerator.patchElements(
div(className := "log-entry", "✓ Step 2: Processing records"),
PatchElementOptions(
selector = Some(CssSelector.id("logContainer")),
mode = ElementPatchMode.Append,
),
)
_ <- ZIO.sleep(800.millis)
// Simulate processing step 3
_ <- ServerSentEventGenerator.patchElements(
div(className := "log-entry", "✓ Step 3: Generating report"),
PatchElementOptions(
selector = Some(CssSelector.id("logContainer")),
mode = ElementPatchMode.Append,
),
)
_ <- ZIO.sleep(800.millis)
// Update status box
_ <- ServerSentEventGenerator.patchElements(
div(
id("statusBox"),
className := "status-box success",
"✓ Processing complete!",
),
PatchElementOptions(selector = Some(CssSelector.id("statusBox"))),
)
// Dispatch custom event to trigger client-side handler
_ <- ServerSentEventGenerator.dispatchEvent(
"processingComplete",
js"{}",
DispatchEventOptions(source = Some(CssSelector.id("startBtn"))),
)
// Final log entry
_ <- ServerSentEventGenerator.patchElements(
div(className := "log-entry success", "✓ All steps completed successfully"),
PatchElementOptions(
selector = Some(CssSelector.id("logContainer")),
mode = ElementPatchMode.Append,
),
)
} yield ()
}
},
)
def indexPage = html(
head(
title("Data Processor with Dispatch Events"),
datastarScript,
style.inlineCss(css),
),
body(
div(
className := "container",
h1("📊 Data Processing with Server Events"),
p("Demonstrates server-side event dispatching for coordinating multi-step workflows."),
button(
"Start Processing",
id("startBtn"),
dataOn.click := js"this.disabled = true; @post('/api/data-processor'); document.getElementById('statusBox').textContent = 'Processing...';",
),
div(
id("statusBox"),
className := "status-box",
"Ready to process",
),
div(
id("logContainer"),
className := "log-container",
div(className := "log-entry", "Waiting for processing to start..."),
),
dataOn("processingComplete") := js"document.getElementById('startBtn').disabled = false;",
),
),
)
val css = css"""
body {
font-family: system-ui;
margin: 40px;
background: #f5f5f5;
}
.container {
max-width: 700px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #1976d2;
margin-bottom: 10px;
}
p {
color: #666;
margin-bottom: 20px;
}
button {
padding: 12px 24px;
font-size: 16px;
background: #1976d2;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.status-box {
margin-top: 20px;
padding: 15px;
border-left: 4px solid #1976d2;
background: #f5f5f5;
border-radius: 4px;
font-weight: 500;
}
.status-box.success {
border-left-color: #4caf50;
background: #e8f5e9;
color: #2e7d32;
}
.log-container {
margin-top: 30px;
max-height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
background: #fafafa;
}
.log-entry {
padding: 10px 15px;
border-bottom: 1px solid #eee;
font-family: 'Courier New', monospace;
font-size: 13px;
color: #333;
}
.log-entry.success {
color: #4caf50;
}
"""
override def run: ZIO[Any, Throwable, Unit] =
Server
.serve(routes)
.provide(Server.default)
}
(source)
sbt "zioHttpExample/runMain example.datastar.DispatchEventCompleteExample"