W↓
All docs
🔑
Sign Up/Sign In
data-star.dev/guide/ (+1)
Public Link
Apr 12, 2025, 4:55:40 AM - complete - 39 kB
Apr 12, 2025, 4:55:40 AM - complete - 39 kB
Apr 12, 2025, 4:54:55 AM - complete - 39 kB
Starting URLs:
https://data-star.dev/guide/getting_started
Crawl Prefixes:
https://data-star.dev/guide/
https://data-star.dev/how_tos/
## Page: https://data-star.dev/guide/getting_started Datastar brings the functionality provided by libraries like Alpine.js (frontend reactivity) and htmx (backend reactivity) together, into one cohesive solution. It’s a lightweight, extensible framework that allows you to: 1. Manage state and build reactivity into your frontend using HTML attributes. 2. Modify the DOM and state by sending events from your backend. With Datastar, you can build any UI that a full-stack framework like React, Vue.js or Svelte can, but with a much simpler, hypermedia-driven approach. We're so confident that Datastar can be used as a JavaScript framework replacement that we challenge anyone to find a use-case for a web app that Datastar _cannot_ be used to build! ## Installation# The quickest way to use Datastar is to include it in your HTML using a script tag hosted on a CDN. <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.11/bundles/datastar.js"></script> If you prefer to host the file yourself, download your own bundle using the bundler, then include it from the appropriate path. <script type="module" src="/path/to/datastar.js"></script> You can alternatively install Datastar via npm. We don’t recommend this for most use-cases, as it requires a build step, but it can be useful for legacy frontend projects. npm install @starfederation/datastar ## Data Attributes# At the core of Datastar are `data-*` attributes (hence the name). They allow you to add reactivity to your frontend in a declarative way, and to interact with your backend. Datastar uses signals to manage state. You can think of signals as reactive variables that automatically track and propagate changes in expressions. They can be created and modified using data attributes on the frontend, or events sent from the backend. Don’t worry if this sounds complicated; it will become clearer as we look at some examples. ### `data-bind`# Datastar provides us with a way to set up two-way data binding on an element using the `data-bind` attribute, which can be placed on any HTML element on which data can be input or choices selected from (`input`, `textarea`, `select`, `checkbox` and `radio` elements, as well as web components). <input data-bind-input /> This creates a new signal that can be called using `$input`, and binds it to the element’s value. If either is changed, the other automatically updates. An alternative syntax, in which the value is used as the signal name, is also available. This can be useful depending on the templating language you are using. <input data-bind="input" /> ### `data-text`# To see this in action, we can use the `data-text` attribute. <input data-bind-input /> <div data-text="$input"> I will be replaced with the contents of the input signal </div> Input: Output: This sets the text content of an element to the value of the signal `$input`. The `$` prefix is required to denote a signal. Note that `data-*` attributes are evaluated in the order they appear in the DOM, so the `data-text` attribute must come _after_ the `data-bind` attribute. See the attribute plugins reference for more information. The value of the `data-text` attribute is a Datastar expression that is evaluated, meaning that we can use JavaScript in it. <input data-bind-input /> <div data-text="$input.toUpperCase()"> Will be replaced with the uppercase contents of the input signal </div> Input: Output: ### `data-computed`# The `data-computed` attribute creates a new signal that is computed based on a reactive expression. The computed signal is read-only, and its value is automatically updated when any signals in the expression are updated. <input data-bind-input /> <div data-computed-repeated="$input.repeat(2)"> <div data-text="$repeated"> Will be replaced with the contents of the repeated signal </div> </div> This results in the `$repeated` signal’s value always being equal to the value of the `$input` signal repeated twice. Computed signals are useful for memoizing expressions containing other signals. Input: Output: ### `data-show`# The `data-show` attribute can be used to show or hide an element based on whether an expression evaluates to `true` or `false`. <input data-bind-input /> <button data-show="$input != ''">Save</button> This results in the button being visible only when the input is _not_ an empty string (this could also be written as `!input`). Input: Output: ### `data-class`# The `data-class` attribute allows us to add or remove a class to or from an element based on an expression. <input data-bind-input /> <button data-class-hidden="$input == ''">Save</button> If the expression evaluates to `true`, the `hidden` class is added to the element; otherwise, it is removed. Input: Output: The `data-class` attribute can also be used to add or remove multiple classes from an element using a set of key-value pairs, where the keys represent class names and the values represent expressions. <button data-class="{hidden: $input == '', 'font-bold': $input == 1}">Save</button> ### `data-attr`# The `data-attr` attribute can be used to bind the value of any HTML attribute to an expression. <input data-bind-input /> <button data-attr-disabled="$input == ''">Save</button> This results in a `disabled` attribute being given the value `true` whenever the input is an empty string. Input: Output: The `data-attr` attribute can also be used to set the values of multiple attributes on an element using a set of key-value pairs, where the keys represent attribute names and the values represent expressions. <button data-attr="{disabled: $input == '', title: $input}">Save</button> ### `data-signals`# So far, we’ve created signals on the fly using `data-bind` and `data-computed`. All signals are merged into a global set of signals that are accessible from anywhere in the DOM. We can also create signals using the `data-signals` attribute. <div data-signals-input="1"></div> Using `data-signals` _merges_ one or more signals into the existing signals. Values defined later in the DOM tree override those defined earlier. Signals can be namespaced using dot-notation. <div data-signals-form.input="2"></div> The `data-signals` attribute can also be used to merge multiple signals using a set of key-value pairs, where the keys represent signal names and the values represent expressions. <div data-signals="{input: 1, form: {input: 2}}"></div> ### `data-on`# The `data-on` attribute can be used to attach an event listener to an element and execute an expression whenever the event is triggered. <input data-bind-input /> <button data-on-click="$input = ''">Reset</button> This results in the `$input` signal’s value being set to an empty string whenever the button element is clicked. This can be used with any valid event name such as `data-on-keydown`, `data-on-mouseover`, etc. Input: Output: So what else can we do now that we have declarative signals and expressions? Anything we want, really! See if you can follow the code below based on what you’ve learned so far, _before_ trying the demo. <div data-signals="{response: '', answer: 'bread'}" data-computed-correct="$response.toLowerCase() == $answer" > <div id="question">What do you put in a toaster?</div> <button data-on-click="$response = prompt('Answer:') ?? ''">BUZZ</button> <div data-show="$response != ''"> You answered “<span data-text="$response"></span>”. <span data-show="$correct">That is correct ✅</span> <span data-show="!$correct"> The correct answer is “ <span data-text="$answer"></span> ” 🤷 </span> </div> </div> What do you put in a toaster? You answered “”. That is correct ✅ The correct answer is “” 🤷 We’ve just scratched the surface of frontend reactivity. Now let’s take a look at how we can bring the backend into play. ## Backend Setup# Datastar uses Server-Sent Events (SSE) to stream zero or more events from the web server to the browser. There’s no special backend plumbing required to use SSE, just some syntax. Fortunately, SSE is straightforward and provides us with some advantages. First, set up your backend in the language of your choice. Familiarize yourself with sending SSE events, or use one of the backend SDKs to get up and running even faster. We’re going to use the SDKs in the examples below, which set the appropriate headers and format the events for us. The following code would exist in a controller action endpoint in your backend. ;; Import the SDK's api and your adapter (require '[starfederation.datastar.clojure.api :as d*] '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]]) ;; in a ring handler (defn handler [request] ;; Create a SSE response (->sse-response request {on-open (fn [sse] ;; Merge html fragments into the DOM (d*/merge-fragment! sse "<div id=\"question\">What do you put in a toaster?</div>") ;; Merge signals into the signals (d*/merge-signals! sse "{response: '', answer: 'bread'}"))})) using StarFederation.Datastar.DependencyInjection; // Adds Datastar as a service builder.Services.AddDatastar(); app.MapGet("/", async (IDatastarServerSentEventService sse) => { // Merges HTML fragments into the DOM. await sse.MergeFragmentsAsync(@"<div id=""question"">What do you put in a toaster?</div>"); // Merges signals into the signals. await sse.MergeSignalsAsync("{response: '', answer: 'bread'}"); }); import (datastar "github.com/starfederation/datastar/sdk/go") // Creates a new `ServerSentEventGenerator` instance. sse := datastar.NewSSE(w,r) // Merges HTML fragments into the DOM. sse.MergeFragments( `<div id="question">What do you put in a toaster?</div>` ) // Merges signals into the signals. sse.MergeSignals([]byte(`{response: '', answer: 'bread'}`)) import ServerSentEventGenerator import ServerSentEventGenerator.Server.Snap -- or whatever is appropriate -- Merges HTML fragments into the DOM. send (withDefaults mergeFragments "<div id=\"question\">What do you put in a toaster?</div>") -- Merges signals into the signals. send (withDefaults mergeSignals "{response: '', answer: 'bread'}") use starfederation\datastar\ServerSentEventGenerator; // Creates a new `ServerSentEventGenerator` instance. $sse = new ServerSentEventGenerator(); // Merges HTML fragments into the DOM. $sse->mergeFragments( '<div id="question">What do you put in a toaster?</div>' ); // Merges signals into the signals. $sse->mergeSignals(['response' => '', 'answer' => 'bread']); require 'datastar' # Create a Datastar::Dispatcher instance datastar = Datastar.new(request:, response:) # In a Rack handler, you can instantiate from the Rack env # datastar = Datastar.from_rack_env(env) # Start a streaming response datastar.stream do |sse| # Merges fragment into the DOM sse.merge_fragments %(<div id="question">What do you put in a toaster?</div>) # Merges signals sse.merge_signals(response: '', answer: 'bread') end use datastar::prelude::*; use async_stream::stream; Sse(stream! { // Merges HTML fragments into the DOM. yield MergeFragments::new("<div id='question'>What do you put in a toaster?</div>").into(); // Merges signals into the signals. yield MergeSignals::new("{response: '', answer: 'bread'}").into(); }) // Creates a new `ServerSentEventGenerator` instance (this also sends required headers) ServerSentEventGenerator.stream(req, res, (stream) => { // Merges HTML fragments into the DOM. stream.mergeFragments(`<div id="question">What do you put in a toaster?</div>`); // Merges signals into the signals. stream.mergeSignals({'response': '', 'answer': 'bread'}); }); const datastar = @import("datastar").httpz; // Creates a new `ServerSentEventGenerator`. var sse = try datastar.ServerSentEventGenerator.init(res); // Merges HTML fragments into the DOM. try sse.mergeFragments("<div id='question'>What do you put in a toaster?</div>", .{}); // Merges signals into the signals. try sse.mergeSignals(.{ .response = "", .answer = "bread" }, .{}); The `mergeFragments()` method merges the provided HTML fragment into the DOM, replacing the element with `id="question"`. An element with the ID `question` must _already_ exist in the DOM. The `mergeSignals()` method merges the `response` and `answer` signals into the frontend signals. With our backend in place, we can now use the `data-on-click` attribute to trigger the `@get()` action, which sends a `GET` request to the `/actions/quiz` endpoint on the server when a button is clicked. <div data-signals="{response: '', answer: ''}" data-computed-correct="$response.toLowerCase() == $answer" > <div id="question"></div> <button data-on-click="@get('/actions/quiz')">Fetch a question</button> <button data-show="$answer != ''" data-on-click="$response = prompt('Answer:') ?? ''" > BUZZ </button> <div data-show="$response != ''"> You answered “<span data-text="$response"></span>”. <span data-show="$correct">That is correct ✅</span> <span data-show="!$correct"> The correct answer is “<span data-text="$answer"></span>” 🤷 </span> </div> </div> Now when the `Fetch a question` button is clicked, the server will respond with an event to modify the `question` element in the DOM and an event to modify the `response` and `answer` signals. We’re driving state from the backend! You answered “”. That is correct ✅ The correct answer is “” 🤷 ### `data-indicator`# The `data-indicator` attribute sets the value of a signal to `true` while the request is in flight, otherwise `false`. We can use this signal to show a loading indicator, which may be desirable for slower responses. <div id="question"></div> <button data-on-click="@get('/actions/quiz')" data-indicator-fetching > Fetch a question </button> <div data-class-loading="$fetching" class="indicator"></div> You answered “”. That is correct ✅ The correct answer is “” 🤷 The `data-indicator` attribute can also be written with signal name in the attribute value. <button data-on-click="@get('/actions/quiz')" data-indicator="fetching" > We’re not limited to just `GET` requests. Datastar provides backend plugin actions for each of the methods available: `@get()`, `@post()`, `@put()`, `@patch()` and `@delete()`. Here’s how we could send an answer to the server for processing, using a `POST` request. <button data-on-click="@post('/actions/quiz')"> Submit answer </button> One of the benefits of using SSE is that we can send multiple events (HTML fragments, signal updates, etc.) in a single response. (d*/merge-fragment! sse "<div id=\"question\">...</div>") (d*/merge-fragment! sse "<div id=\"instructions\">...</div>") (d*/merge-signals! sse "{answer: '...'}") (d*/merge-signals! sse "{prize: '...'}") sse.MergeFragmentsAsync(@"<div id=""question"">...</div>"); sse.MergeFragmentsAsync(@"<div id=""instructions"">...</div>"); sse.MergeSignalsAsync("{answer: '...'}"); sse.MergeSignalsAsync("{prize: '...'}"); sse.MergeFragments(`<div id="question">...</div>`) sse.MergeFragments(`<div id="instructions">...</div>`) sse.MergeSignals([]byte(`{answer: '...'}`)) sse.MergeSignals([]byte(`{prize: '...'}`)) import ServerSentEventGenerator import ServerSentEventGenerator.Server.Snap -- or whatever is appropriate send (withDefaults MergeFragments "<div id=\"question\">...</div>") send (withDefaults MergeFragments "<div id=\"instructions\">...</div>") send (withDefaults MergeFragments "{answer: '...'}") send (withDefaults MergeFragments "{prize: '...'}") $sse->mergeFragments('<div id="question">...</div>'); $sse->mergeFragments('<div id="instructions">...</div>'); $sse->mergeSignals(['answer' => '...']); $sse->mergeSignals(['prize' => '...']); datastar.stream do |sse| sse.merge_fragments('<div id="question">...</div>') sse.merge_fragments('<div id="instructions">...</div>') sse.merge_signals(answer: '...') sse.merge_signals(prize: '...') end yield MergeFragments::new("<div id='question'>...</div>").into() yield MergeFragments::new("<div id='instructions'>...</div>").into() yield MergeSignals::new("{answer: '...'}").into() yield MergeSignals::new("{prize: '...'}").into() stream.mergeFragments('<div id="question">...</div>'); stream.mergeFragments('<div id="instructions">...</div>'); stream.mergeSignals({'answer': '...'}); stream.mergeSignals({'prize': '...'}); try sse.mergeFragments("<div id='question'>...</div>"), .{}; try sse.mergeFragments("<div id='instructions'>...</div>", .{}); try sse.mergeSignals(.{ .answer = "..." }, .{}); try sse.mergeSignals(.{ .prize = "..." }, .{}); ## Actions# Actions in Datastar are helper functions that are available in `data-*` attributes and have the syntax `@actionName()`. We already saw the backend plugin actions above. Here are a few other useful actions. ### `@setAll()`# The `@setAll()` action sets the value of all matching signals to the expression provided in the second argument. The first argument can be one or more space-separated paths in which `*` can be used as a wildcard. <button data-on-click="@setAll('foo.*', $bar)"></button> This sets the values of all signals namespaced under the `foo` signal to the value of `$bar`. This can be useful for checking multiple checkbox fields in a form, for example: <input type="checkbox" data-bind-checkboxes.checkbox1 /> Checkbox 1 <input type="checkbox" data-bind-checkboxes.checkbox2 /> Checkbox 2 <input type="checkbox" data-bind-checkboxes.checkbox3 /> Checkbox 3 <button data-on-click="@setAll('checkboxes.*', true)">Check All</button> Checkbox 1 Checkbox 2 Checkbox 3 ### `@toggleAll()`# The `@toggleAll()` action toggles the value of all matching signals. The first argument can be one or more space-separated paths in which `*` can be used as a wildcard. <button data-on-click="@toggleAll('foo.*')"></button> This toggles the values of all signals namespaced under the `foo` signal (to either `true` or `false`). This can be useful for toggling multiple checkbox fields in a form, for example: <input type="checkbox" data-bind-checkboxes.checkbox1 /> Checkbox 1 <input type="checkbox" data-bind-checkboxes.checkbox2 /> Checkbox 2 <input type="checkbox" data-bind-checkboxes.checkbox3 /> Checkbox 3 <button data-on-click="@toggleAll('checkboxes.*')">Toggle All</button> Checkbox 1 Checkbox 2 Checkbox 3 View the reference overview. --- ## Page: https://data-star.dev/guide/going_deeper Datastar’s philosophy is: let the browser do what it does best—render HTML—while enabling declarative reactivity. At its core, Datastar makes **namespaced signals declarative**. Let’s unpack that. ### 1\. Declarative# Declarative code is amazing. It allows you to simply request the result you want, without having to think about the steps required to make it happen. Consider this imperative (non-declarative) way of conditionally placing a class on an element using JavaScript. if (foo == 1) { document.getElementById('myelement').classList.add('bold'); } else { document.getElementById('myelement').classList.remove('bold'); } Datastar allows us to write this logic declaratively while embracing locality-of-behavior, by placing it directly on the element we want to affect. <div data-class-bold="$foo == 1"></div> ### 2\. Signals# Datastar uses signals to manage frontend state. You can think of signals as reactive variables that automatically track and propagate changes in expressions. Signals can be created and modified using `data-*` attributes on the frontend, or events sent from the backend. They can also be used in Datastar expressions. <div data-signals-foo=""></div> <div data-text="$foo"></div> <button data-on-click="$foo = 'hello'"></button> Behind the scenes, Datastar converts `$foo` to `ctx.signals.signal('foo').value`, and then evaluates the expression in a sandboxed context. This means that JavaScript can be used in Datastar expressions. <button data-on-click="$foo = $foo.toUpperCase()"> Convert to uppercase </button> ### 3\. Namespaced Signals# Signals in Datastar have a trick up their sleeve: they can be namespaced. <div data-signals-foo.bar="1"></div> Or, using object syntax: <div data-signals="{foo: {bar: 1}}"></div> Or, using two-way binding: <input data-bind-foo.bar /> Note that only the leaf nodes are actually signals. So in the example above, `bar` is a signal but `foo`(the namespace) is not, meaning that while using `$foo.bar` in an expression is possible, using `$foo` is not. Namespaced signals can be useful for targetting signals in a more granular way on the backend. Another practical use-case might be when you have repetition of state on a page. The following example shows how to toggle the value of all signals starting with `menu.open.` at once when a button is clicked. <div data-signals="{menu: {isopen: {desktop: false, mobile: false}}}"> <button data-on-click="@toggleAll('menu.isopen.*')"> Open/close menu </button> </div> The beauty of this is that you don’t need to write a bunch of code to set up and maintain state. You just use `data-*` attributes and think declaratively! ## Datastar Actions# Actions are helper functions that can be used in Datastar expressions. They allow you to perform logical operations without having to write procedural JavaScript. <button data-on-click="@setAll('foo.*', $mysignal.toUpperCase())"> Convert all to uppercase </button> ### Backend Actions# The `@get()` action sends a `GET` request to the backend using `fetch`, and expects an event stream response containing zero or more Datastar SSE events. <button data-on-click="@get('/endpoint')"></button> An event stream response is nothing more than a response containing a `Content-Type: text/event-stream` header. SSE events can update the DOM, adjust signals, or run JavaScript directly in the browser. event: datastar-merge-fragments data: fragments <div id="hello">Hello, world!</div> event: datastar-merge-signals data: signals {foo: {bar: 1}} event: datastar-execute-script data: script console.log('Success!') Using one of the backend SDKs will help you get up and running faster. Here is all of the backend code required to produce the events above in each of the SDKs. (require '[starfederation.datastar.clojure.api :as d*] '[starfederation.datastar.clojure.adapter.http-kit :refer [->sse-response on-open]]) (defn handler [request] (->sse-response request {on-open (fn [sse] (d*/merge-fragment! sse "<div id=\"hello\">Hello, world!</div>") (d*/merge-signals! sse "{foo: {bar: 1}}") (d*/execute-script! sse "console.log('Success!')"))})) using StarFederation.Datastar.DependencyInjection; // Adds Datastar as a service builder.Services.AddDatastar(); app.MapGet("/", async (IDatastarServerSentEventService sse) => { await sse.MergeFragmentsAsync(@"<div id=""question"">What do you put in a toaster?</div>"); await sse.MergeSignalsAsync("{foo: {bar: 1}}"); await sse.ExecuteScriptAsync(@"console.log(""Success!"")"); }); import (datastar "github.com/starfederation/datastar/sdk/go") // Creates a new `ServerSentEventGenerator` instance. sse := datastar.NewSSE(w,r) sse.MergeFragments(`<div id="hello">Hello, world!</div>`) sse.MergeSignals([]byte(`{foo: {bar: 1}}`)) sse.ExecuteScript(`console.log('Success!')`) import ServerSentEventGenerator import ServerSentEventGenerator.Server.Snap -- or whatever is appropriate send (withDefaults mergeFragments "<div id=\"hello\">Hello, world!</div>" def def def def) send (withDefaults mergeSignals "{foo: {bar: 1}}" def def) send (withDefaults executeScript "console.log('Success!')" def def def) use starfederation\datastar\ServerSentEventGenerator; // Creates a new `ServerSentEventGenerator` instance. $sse = new ServerSentEventGenerator(); $sse->mergeFragments('<div id="hello">Hello, world!</div>'); $sse->mergeSignals(['foo' => ['bar' => 1]]); $sse->executeScript('console.log("Success!")'); require 'datastar' # Create a Datastar::Dispatcher instance datastar = Datastar.new(request:, response:) datastar.stream do |sse| sse.merge_fragments('<div id="hello">Hello, world!</div>') sse.merge_signals(foo: { bar: 1 }) sse.execute_script('console.log("Success!")') end use datastar::prelude::*; use async_stream::stream; Sse(stream! { yield MergeFragments::new("<div id='hello'>Hello, world!</div>").into(); yield MergeSignals::new("{foo: {bar: 1}}").into(); yield ExecuteScript::new("console.log('Success!')).into(); }) ServerSentEventGenerator.stream(req, res, (stream) => { stream.mergeFragments('<div id="hello">Hello, world!</div>'); stream.mergeSignals({'foo': {'bar': 1}}); stream.executeScript('console.log("Success!")'); }); const datastar = @import("datastar").httpz; // Creates a new `ServerSentEventGenerator`. sse = try datastar.ServerSentEventGenerator.init(res); try sse.mergeFragments("<div id='hello'>Hello, world!</div>", .{}); try sse.mergeSignals(.{ .foo = .{ .bar = 1 } }, .{}); try sse.executeScript("console.log('Success!')", .{}); Every request is sent with a `{datastar: *}` object that includes all existing signals (except for local signals whose keys begin with an underscore). This allows frontend state to be shared with the backend, and for the backend to “drive the frontend” (control its state and behavior dynamically). ## Embracing Simplicity# Datastar is smaller than Alpine.js and htmx, yet provides the functionality of both libraries combined. The package size is not _just_ a vanity metric. By embracing simplicity, and building on first principles, everything becomes cleaner and leaner. But don’t take our word for it – explore the source code and see for yourself! Datastar is both a core library (~5 KiB) and a “batteries included” framework (~14 KiB), allowing you to create custom bundles and write your own plugins. Datastar is a hypermedia framework. Hypermedia is the idea that the web is a network of interconnected resources, and it is the reason the web has been so successful. However, the rise of the frontend frameworks and SPAs has led to a lot of confusion about how to use hypermedia. Browsers don’t care about your application – they care about rendering hypermedia. For example, if you visit a membership website as a guest, you’ll likely see a generic landing page and a login option. Only once you log in will you see links to member-only content. This has huge benefits. * Each interaction determines the next valid state. * When implemented correctly, all logic resides in the backend, eliminating the need for frontend routing, validation, etc. * HTML can be generated from any language. ## Unlearning# When approaching Datastar, especially when coming from other frontend frameworks, be prepared to _unlearn_ some bad practices. These may not seem like bad practices initially; they may even feel natural to you. Here are a few things you should look out for. 1. **Overuse of procedural code for DOM manipulation.** Avoid writing procedural JavaScript to manually update the DOM. Use declarative, HTML-based `data-*` attributes and SSE events instead. 2. **Putting state and logic in signals.** Avoid recreating the sins of SPAs by putting state and logic in signals. Signals should only exist for what users can interact with, and for sharing state with the backend. 3. **Managing state on the frontend.** Avoid excessive frontend state management. Instead, let the backend drive state by managing data persistence and logic, ensuring a single source of truth. Focus on keeping the frontend lightweight and reactive. We’re very confident that Datastar can do _anything_ that React, Vue.js, or Svelte can do, faster and with less code. We’ll take on anyone that disagrees! When you embrace hypermedia, everything becomes much _less_ complicated. Put state in the right place, and it becomes a lot easier to reason about. --- ## Page: https://data-star.dev/guide/datastar_expressions Datastar expressions are strings that are evaluated by Datastar attributes and actions. While they are similar to JavaScript, there are some important differences that make them more powerful for declarative hypermedia applications. * Basic Usage * Namespaced Signals * Multiple Statements and Formatting * Event Context * Summary ## Basic Usage# The following example outputs `1` not because `$foo` is defined in the global scope (it’s not), but because we’ve defined `foo` as a signal with the initial value `1`, and are using `$foo` in a `data-*` attribute. <div data-signals-foo="1"> <div data-text="$foo"></div> </div> When Datastar evaluates the expression `$foo`, it first converts it to `ctx.signals.signal('foo').value`, and then evaluates that expression in a sandboxed context, in which `ctx` represents the current expression context. This means that JavaScript can be used in Datastar expressions. <div data-text="$foo.length"></div> In the above expression, `$foo.length` is first converted to `ctx.signals.signal('foo').value.length` and then evaluated as follows. return (()=> { return (ctx.signals.signal('foo').value.length); })() Normal JavaScript operators are also available in Datastar expressions. This includes (but isn’t limited to) `&&`, `||`, and the ternary operator. These last three can be useful when reacting to signal changes. For example, the following would only trigger a post action if the signal is logically true. <button data-on-click="$landingGearRetracted && @post('/launch')"></div <button data-on-click="$landingGearState == 'retracted' && @post('/launch')"></div The ternary operator is useful to choose among two options: <div data-attr-class="$theme == 'dark' ? 'bg-black text-white' : 'bg-white text-black'" ## Namespaced Signals# The following example is invalid because the namespace `$foo` is _not_ a signal – only leaf nodes are signals. <div data-signals-foo.bar="1"> <div data-text="$foo"></div> <!-- Invalid --> </div> The following example is valid because both `$foo.bar` and `$baz` are signals. <div data-signals-foo.bar="1" data-signals-baz="1"> <div data-text="$foo.bar"></div> <!-- Valid --> <div data-text="$baz"></div> <!-- Valid --> </div> Note that `data-*` attributes are evaluated in the order they appear in the DOM, so the `data-text` attributes must come _after_ the `data-signals-*` attributes in the example above. See the attribute plugins reference for more information. ## Multiple Statements and Formatting# Multiple statements can be used in a single expression by separating them with a semicolon. <div data-signals-foo="1"> <button data-on-click="$foo++; @post('/endpoint')"></button> </div> Expressions may span multiple lines, but a semicolon must be used to separate statements. Unlike JavaScript, line breaks alone are not sufficient to separate statements. <div data-signals-foo="1"> <button data-on-click=" $foo++; @post('/endpoint') "></button> </div> There are two things to be aware about the final (or only!) statement in a datastar expression: 1. The final statement in an expression does not require a semicolon 2. The return value of the final statement is implicitly returned. The return value of an expression is often discarded, but depending on which attribute plugin you are using, it may be relevant. <button data-on-click=" $count++; $message = 'Clicked ' + $count + ' times' "></button> ## Event Context# When using `data-on-*` attributes, the event object is available as `evt`: <input data-on-input="$value = evt.target.value"> This gives you access to the standard browser event properties and methods inside your expressions. ## Expression Summary# For quick reference, here are the key aspects of Datastar expressions: * **Signals:** Signals are accessed using the `$` prefix (e.g., `$count`, `$userName`, `$items.selected`). * JS global identifiers starting with `$` are not usable in Datastar expressions. * **Actions:** Actions are called using the `@` prefix (e.g., `@post('/endpoint')`). * **Statement Delimiter:** Statements must be separated by semicolons (`;`), not just line breaks. * **Implicit Return:** The last statement is automatically returned without needing the `return` keyword. * **Special Variables:** * `evt`: The event object (available in `data-on-*` attributes). --- ## Page: https://data-star.dev/guide/stop_overcomplicating_it Most of the time, if you run into issues when using Datastar, **you are probably overcomplicating it™**. As explained in going deeper, Datastar is a _hypermedia_ framework. If you approach it like a _JavaScript_ framework, you are likely to run into complications. So how does one use a hypermedia framework? ## The Datastar Way# Between attribute plugins and action plugins, Datastar provides you with everything you need to build hypermedia-driven applications. Using this approach, the backend drives state to the frontend and acts as the single source of truth, determining what actions the user can take next. Any additional JavaScript functionality you require that does _not_ work via `data-*` attributes and `datastar-execute-script` SSE events should ideally be extracted out into external scripts or, better yet, web components. Always encapsulate state and send **_props down, events up_**. ### External Scripts# When using external scripts, pass data into functions via arguments and return a result _or_ listen for custom events dispatched from them (_props down, events up_). In this way, the function is encapsulated – all it knows is that it receives input via an argument, acts on it, and optionally returns a result or dispatches a custom event – and `data-*` attributes can be used to drive reactivity. <div data-signals-result="''"> <input data-bind-foo data-on-input="$result = myfunction($foo)" > <span data-text="$result"></span> </div> function myfunction(data) { return `You entered ${data}`; } If your function call is asynchronous then it will need to dispatch a custom event containing the result. While asynchronous code _can_ be placed within Datastar expressions, Datastar will _not_ await it. <div data-signals-result="''" data-on-mycustomevent__window="$result = evt.detail.value" > <input data-bind-foo data-on-input="myfunction($foo)" > <span data-text="$result"></span> </div> async function myfunction(data) { const value = await new Promise((resolve) => { setTimeout(() => resolve(`You entered ${data}`), 1000); }); window.dispatchEvent( new CustomEvent('mycustomevent', {detail: {value}}) ); } ### Web Components# Web components allow you create reusable, encapsulated, custom elements. They are native to the web and require no external libraries or frameworks. Web components unlock custom elements – HTML tags with custom behavior and styling. When using web components, pass data into them via attributes and listen for custom events dispatched from them (_props down, events up_). In this way, the web component is encapsulated – all it knows is that it receives input via an attribute, acts on it, and optionally dispatches a custom event containing the result – and `data-*` attributes can be used to drive reactivity. <div data-signals-result="''"> <input data-bind-foo /> <my-component data-attr-src="$foo" data-on-mycustomevent="$result = evt.detail.value" ></my-component> <span data-text="$result"></span> </div> class MyComponent extends HTMLElement { static get observedAttributes() { return ['src']; } attributeChangedCallback(name, oldValue, newValue) { const value = `You entered ${newValue}`; this.dispatchEvent( new CustomEvent('mycustomevent', {detail: {value}}) ); } } customElements.define('my-component', MyComponent); Since the `value` attribute is allowed on web components, it is also possible to use `data-bind` to bind a signal to the web component’s value. Note that a `change` event must be dispatched so that the event listener used by `data-bind` is triggered by the value change. <input data-bind-foo /> <my-component data-attr-src="$foo" data-bind-result ></my-component> <span data-text="$result"></span> class MyComponent extends HTMLElement { static get observedAttributes() { return ['src']; } attributeChangedCallback(name, oldValue, newValue) { this.value = `You entered ${newValue}`; this.dispatchEvent(new Event('change')); } } customElements.define('my-component', MyComponent);