Skip to content

Commit

Permalink
docs: update datastar example (#1039)
Browse files Browse the repository at this point in the history
  • Loading branch information
YuryKL authored Jan 17, 2025
1 parent 24be99e commit 34660b3
Showing 1 changed file with 33 additions and 32 deletions.
65 changes: 33 additions & 32 deletions docs/docs/05-server-side-rendering/04-datastar.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[Datastar](https://data-star.dev) is a hypermedia framework that is similar to [HTMX](htmx).

Datastar can be used to selectively replace content within a web page by combining fine grained reactive signals with SSE. It's geared primarily to real time applications where you'd normally reach for a SPA framework such as React/Vue/Svelte.
Datastar can selectively replace content within a web page by combining fine-grained reactive signals with SSE. It's geared primarily to real-time applications where you'd normally reach for a SPA framework such as React/Vue/Svelte.

## Usage

Expand All @@ -13,7 +13,7 @@ Using Datastar requires:

## Installation

Datastar comes out of the box with templ components to speed up development. You can use `@datastar.ScriptCDNLatest()` or `ScriptCDNVersion(version string)` to include the latest version of the Datastar library in your HTML.
Datastar is included with Templ components out of the box to speed up development. You can use `@datastar.ScriptCDNLatest()` or `ScriptCDNVersion(version string)` to include the latest version of the Datastar library in your HTML.

:::info
Advanced Datastar installation and usage help is covered in the user guide at https://data-star.dev.
Expand All @@ -35,25 +35,28 @@ We are going to modify the [templ counter example](example-counter-application)

### Frontend

First, define a HTML some with two buttons. One to update a global state, and one to update a per-user state.
First, define some HTML with two buttons. One to update a global state, and one to update a per-user state.

```templ title="components.templ"
package site
type TemplCounterStore struct {
import datastar "github.com/starfederation/datastar/sdk/go"
type TemplCounterSignals struct {
Global uint32 `json:"global"`
User uint32 `json:"user"`
}
templ templCounterExampleButtons() {
<div>
<button
data-on-click="$$post('/examples/templ_counter/increment/global')"
data-on-click="@post('/examples/templ_counter/increment/global')"
>
Increment Global
</button>
<button
data-on-click="$$post('/examples/templ_counter/increment/user')"
data-on-click={ datastar.PostSSE('/examples/templ_counter/increment/user') }
<!-- Alternative: Using Datastar SDK sugar-->
>
Increment User
</button>
Expand All @@ -73,10 +76,10 @@ templ templCounterExampleCounts() {
</div>
}
templ templCounterExampleInitialContents(store TemplCounterStore) {
templ templCounterExampleInitialContents(signals TemplCounterSignals) {
<div
id="container"
data-store={ templ.JSONString(store) }
data-signals={ templ.JSONString(signals) }
>
@templCounterExampleButtons()
@templCounterExampleCounts()
Expand All @@ -85,13 +88,13 @@ templ templCounterExampleInitialContents(store TemplCounterStore) {
```

:::tip
Note that Datastar doesn't promote the use of forms, because they are ill-suited to nested reactive contents. Instead it sends all[^1] reactive state (as JSON) to the server on each request. This means far less bookkeeping and more predictable state management.
Note that Datastar doesn't promote the use of forms because they are ill-suited to nested reactive content. Instead, it sends all[^1] reactive state (as JSON) to the server on each request. This means far less bookkeeping and more predictable state management.
:::

:::note
`data-store` is a special attribute that Datastar uses to store the initial state of the page. The contents will turn into signals that can be used to update the page.
`data-signals` is a special attribute that Datastar uses to merge one or more signals into the existing signals. In the example, we store $global and $user when we initially render the container.

`data-on-click="$$post('/examples/templ_counter/increment/global')"` is an attribute expression that says "when this element is clicked, send a POST request to the server to the specified URL". The `$$post` is an action that is a sandboxed function that knows about things like signals.
`data-on-click="@post('/examples/templ_counter/increment/global')"` is an attribute expression that says "When this element is clicked, send a POST request to the server to the specified URL". The `@post` is an action that is a sandboxed function that knows about things like signals.

`data-text="$global"` is an attribute expression that says "replace the contents of this element with the value of the `global` signal in the store". This is a reactive signal that will update the page when the value changes, which we'll see in a moment.
:::
Expand All @@ -108,20 +111,21 @@ import (
"sync/atomic"

"github.com/Jeffail/gabs/v2"
"github.com/delaneyj/datastar"
"github.com/go-chi/chi/v5"
"github.com/gorilla/sessions"
datastar "github.com/starfederation/datastar/sdk/go"
)

func setupExamplesTemplCounter(examplesRouter chi.Router, sessionStore sessions.Store) error {
func setupExamplesTemplCounter(examplesRouter chi.Router, sessionSignals sessions.Store) error {

var globalCounter atomic.Uint32
const (
sessionKey = "templ_counter"
countKey = "count"
)

userVal := func(r *http.Request) (uint32, *sessions.Session, error) {
sess, err := sessionStore.Get(r, sessionKey)
sess, err := sessionSignals.Get(r, sessionKey)
if err != nil {
return 0, nil, err
}
Expand All @@ -139,26 +143,25 @@ func setupExamplesTemplCounter(examplesRouter chi.Router, sessionStore sessions.
http.Error(w, err.Error(), http.StatusInternalServerError)
}

store := TemplCounterStore{
signals := TemplCounterSignals{
Global: globalCounter.Load(),
User: userVal,
}

sse := datastar.NewSSE(w, r)
datastar.RenderFragmentTempl(sse, templCounterExampleInitialContents(store))
c := templCounterExampleInitialContents(signals)
datastar.NewSSE(w, r).MergeFragmentTempl(c)
})

updateGlobal := func(store *gabs.Container) {
store.Set(globalCounter.Add(1), "global")
updateGlobal := func(signals *gabs.Container) {
signals.Set(globalCounter.Add(1), "global")
}

examplesRouter.Route("/templ_counter/increment", func(incrementRouter chi.Router) {
incrementRouter.Post("/global", func(w http.ResponseWriter, r *http.Request) {
update := gabs.New()
updateGlobal(update)

sse := datastar.NewSSE(w, r)
datastar.PatchStore(sse, update)
datastar.NewSSE(w, r).MarshalAndMergeSignals(update)
})

incrementRouter.Post("/user", func(w http.ResponseWriter, r *http.Request) {
Expand All @@ -177,30 +180,28 @@ func setupExamplesTemplCounter(examplesRouter chi.Router, sessionStore sessions.
updateGlobal(update)
update.Set(val, "user")

sse := datastar.NewSSE(w, r)
datastar.PatchStore(sse, update)
datastar.NewSSE(w, r).MarshalAndMergeSignals(update)
})
})

return nil
}
```
}```
The `atomic.Uint32` type is used to store the global state. The `userVal` function is a helper that retrieves the user's session state. The `updateGlobal` function increments the global state.
The `atomic.Uint32` type stores the global state. The `userVal` function is a helper that retrieves the user's session state. The `updateGlobal` function increments the global state.
:::note
In this example, the global state is stored in RAM, and will be lost when the web server reboots. To support load-balanced web servers, and stateless function deployments, consider storing the state in a data store such as [NATS KV](https://docs.nats.io/using-nats/developer/develop_jetstream/kv).
In this example, the global state is stored in RAM and will be lost when the web server reboots. To support load-balanced web servers and stateless function deployments, consider storing the state in a data store such as [NATS KV](https://docs.nats.io/using-nats/developer/develop_jetstream/kv).
:::
### Per-user session state
In a HTTP application, per-user state information is partitioned by a HTTP cookie. Cookies that identify a user while they're using a site are known as "session cookies". When the HTTP handler receives a request, it can read the session ID of the user from the cookie and retrieve any required state.
In an HTTP application, per-user state information is partitioned by an HTTP cookie. Cookies that identify a user while they're using a site are known as "session cookies". When the HTTP handler receives a request, it can read the session ID of the user from the cookie and retrieve any required state.
### Signal only patching
### Signal-only patching
Since the page's elements aren't changing dynamically, we can use the `datastar.PatchStore` function to send only the signals that have changed. This is a more efficient way to update the page without even needing to send HTML fragments.
Since the page's elements aren't changing dynamically, we can use the `MarshalAndMergeSignals` function to send only the signals that have changed. This is a more efficient way to update the page without even needing to send HTML fragments.
:::tip
Datastar will merge updates to the store similar to a JSON merge patch. This means you can do dynamic partial updates to the store and the page will update accordingly. [Gabs](https://pkg.go.dev/github.com/Jeffail/gabs/v2#section-readme) is used here to handle dynamic JSON in Go.
Datastar will merge updates to signals similar to a JSON merge patch. This means you can do dynamic partial updates to the store and the page will update accordingly. [Gabs](https://pkg.go.dev/github.com/Jeffail/gabs/v2#section-readme) is used here to handle dynamic JSON in Go.
[^1]: You can control the data that is sent to the server by prefixing local signals with `_`. This will prevent them from being sent to the server on every request.
[^1]: You can control the data sent to the server by prefixing local signals with `_`. This will prevent them from being sent to the server on every request.

0 comments on commit 34660b3

Please sign in to comment.