Created
January 22, 2026 03:32
-
-
Save delaneyj/3d471380432fa3eae7cc488730f7700d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package site | |
| import ( | |
| "fmt" | |
| "github.com/dustin/go-humanize" | |
| build "github.com/starfederation/datastar/build" | |
| datastar "github.com/starfederation/datastar/sdk/go" | |
| ) | |
| templ Home() { | |
| {{ | |
| cdnText := `<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar/bundles/datastar.js"></script>` | |
| }} | |
| {{ | |
| usageSample := `<input data-bind-title /> | |
| <div data-text="title.value.toUpperCase()"></div> | |
| <button data-on-click="sse('/endpoint', {method: 'post'})">Save</button>` | |
| }} | |
| @Page( | |
| "Datastar - The hypermedia framework.", | |
| "Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework.", | |
| "/", | |
| ) { | |
| <div class="flex flex-col w-full min-h-screen bg-base-200 md:bg-gradient-to-br md:items-center from-base-300 to-base-100"> | |
| <div class="flex flex-col items-stretch gap-7 p-4 md:pt-8 md:max-w-3xl max-w-none"> | |
| <div class="flex flex-col items-center gap-7"> | |
| <img class="w-48 sm:w-56 md:w-72" src={ staticPath("images/rocket.webp") }/> | |
| <div class="flex flex-col items-end font-bold uppercase font-brand"> | |
| <div class="text-3xl md:text-5xl text-primary">Datastar</div> | |
| <div class="text-sm text-secondary">v{ datastar.Version }</div> | |
| </div> | |
| <div class="text-center font-brand"> | |
| <div class="text-xl md:text-2xl"> | |
| The hypermedia framework. | |
| </div> | |
| </div> | |
| </div> | |
| <p class="md:text-xl"> | |
| Datastar helps you build reactive web applications with the simplicity of server-side rendering and the power of a full-stack SPA framework. | |
| </p> | |
| <a | |
| class="flex items-center w-full gap-1 btn btn-accent btn-lg" | |
| href={ templ.SafeURL("/guide") } | |
| > | |
| @icon("simple-icons:rocket") | |
| { "Get started" } | |
| </a> | |
| <a | |
| class="flex items-center w-full gap-4 btn btn-primary btn-lg" | |
| href={ templ.SafeURL("/examples") } | |
| > | |
| @icon("svg-spinners:bars-scale-middle") | |
| { "Show me some examples!" } | |
| @icon("svg-spinners:bars-scale-middle") | |
| </a> | |
| <p> | |
| Include Datastar with a single | |
| <span class="text-lg font-bold text-primary"> | |
| { humanize.CommafWithDigits( datastar.VersionClientByteSizeGzip / 1024.0, 1) }KiB | |
| </span> | |
| file and start adding reactivity to your frontend immediately. Write your backend in the language of your choice! Official SDKs are available to help you get up and running even faster, with more languages on the way. | |
| <div class="flex flex-wrap justify-center gap-2"> | |
| for _, lang := range build.Consts.SDKLanguages { | |
| <a href={ templ.SafeURL(lang.SdkUrl) } class="flex flex-col justify-center items-center"> | |
| @icon(lang.Icon, "class", "text-9xl") | |
| <div class="uppercase text-sm font-bold">{ lang.Name }</div> | |
| </a> | |
| } | |
| </div> | |
| </p> | |
| <p>Getting started is as easy as adding a single script tag to your HTML.</p> | |
| <div class="w-full shadow-xl card bg-base-100"> | |
| <div class="card-body"> | |
| <div class="flex items-center gap-4"> | |
| <code | |
| class="flex-1 overflow-auto text-primary text-ellipsis" | |
| > | |
| <pre>{ cdnText }</pre> | |
| </code> | |
| <button | |
| class="btn btn-primary btn-ghost" | |
| data-on-click={ fmt.Sprintf("clipboard('%s')", cdnText) } | |
| > | |
| @icon("material-symbols:content-copy") | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <p>Then start adding frontend reactivity using declarative <code>data-*</code> attributes.</p> | |
| <div class="w-full shadow-xl card bg-base-100"> | |
| <div class="card-body"> | |
| <div class="flex items-center gap-4"> | |
| <code | |
| class="flex-1 overflow-auto text-primary text-ellipsis" | |
| > | |
| <pre>{ usageSample }</pre> | |
| </code> | |
| </div> | |
| </div> | |
| </div> | |
| <p>Get involved with the community and help shape the future of Datastar!</p> | |
| <div class="flex flex-wrap w-full gap-4"> | |
| <a | |
| class="flex items-center justify-center flex-1 btn bg-discord" | |
| href="https://discord.gg/bnRNgZjgPh" | |
| > | |
| @icon("simple-icons:discord") | |
| Discuss on Discord | |
| </a> | |
| <a | |
| class="flex items-center justify-center flex-1 btn bg-github" | |
| href="https://github.com/starfederation/datastar/tree/main/library" | |
| > | |
| @icon("simple-icons:github") | |
| View the Source Code | |
| </a> | |
| </div> | |
| <p> | |
| At only 12 KiB, Datastar is smaller than Alpine.js and htmx, yet it provides the functionality of both libraries combined. The package size is not <em>just</em> a vanity metric. By embracing simplicity, and building on first principles, everything becomes cleaner and leaner. But don’t take our word for it – <a href="https://github.com/starfederation/datastar/tree/main/library">explore the source code</a> and see for yourself! | |
| </p> | |
| <img class="w-full" src="/chart"/> | |
| <div | |
| id="todosMVC" | |
| data-on-load={ datastar.GetSSE("/api/todos") } | |
| > | |
| <p>Todos Example</p> | |
| <p>If you are seeing this message, please clear your cookies and refresh the page.</p> | |
| <p>We recently updated the site and the old cookies are causing issues.</p> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| } | |
| type TodoViewMode int | |
| const ( | |
| TodoViewModeAll TodoViewMode = iota | |
| TodoViewModeActive | |
| TodoViewModeCompleted | |
| TodoViewModeLast | |
| ) | |
| var TodoViewModeStrings = []string{"All", "Active", "Completed"} | |
| type Todo struct { | |
| Text string `json:"text"` | |
| Completed bool `json:"completed"` | |
| } | |
| type TodoMVC struct { | |
| Todos []*Todo `json:"todos"` | |
| EditingIdx int `json:"editingIdx"` | |
| Mode TodoViewMode `json:"mode"` | |
| } | |
| templ TodosMVCView(mvc *TodoMVC) { | |
| {{ | |
| hasTodos := len(mvc.Todos) > 0 | |
| left, completed := 0, 0 | |
| for _, todo := range mvc.Todos { | |
| if !todo.Completed { | |
| left++ | |
| } else { | |
| completed++ | |
| } | |
| } | |
| input := "" | |
| if mvc.EditingIdx >= 0 { | |
| input = mvc.Todos[mvc.EditingIdx].Text | |
| } | |
| }} | |
| <div id="todosMVC" class="w-full shadow-xl card bg-base-100 ring-4 ring-primary"> | |
| <div class="card-body"> | |
| <div | |
| class="flex flex-col w-full gap-4" | |
| data-signals={ fmt.Sprintf("{input:'%s'}", input) } | |
| > | |
| <p class="text-sm"> | |
| This mini application is driven by a | |
| <span class="italic font-bold uppercase text-primary">single get request!</span> | |
| As you interact with the UI, the backend state is updated and new partial HTML fragments are sent down to the client via Server-Sent Events. You can make simple apps or full blown SPA replacements with this pattern. Open your dev tools and watch the network tab to see the magic happen (you will want to look for the "/todos" Network/EventStream tab). | |
| </p> | |
| <section class="flex flex-col gap-2"> | |
| <header class="flex flex-col gap-2"> | |
| <div class="flex items-baseline gap-2"> | |
| <h1 class="text-4xl font-bold uppercase font-brand md:text-6xl text-primary">todos</h1> | |
| <h3 class="text-lg">example</h3> | |
| </div> | |
| <h2 class="text-sm"> | |
| The input is bound to a local signals, but this is not a single page application. It is like having <a class="link-primary" href="https://htmx.org" target="_blank">HTMX</a> + <a class="link-primary" href="https://alpinejs.dev/" target="_blank">Alpine.js</a> but with just one API to learn and much easier to extend. | |
| </h2> | |
| <div class="flex items-center gap-2"> | |
| if hasTodos { | |
| <div class="tooltip" data-tip="toggle all todos"> | |
| <button | |
| id="toggleAll" | |
| class="btn btn-lg" | |
| data-on-click={ datastar.PostSSE("/api/todos/-1/toggle") } | |
| data-testid="toggle_all_todos" | |
| data-indicator="toggleAllFetching" | |
| data-attrs-disabled="toggleAllFetching" | |
| > | |
| @icon("material-symbols:checklist") | |
| </button> | |
| </div> | |
| } | |
| if mvc.EditingIdx <0 { | |
| @TodoInput(-1) | |
| } | |
| @sseIndicator("toggleAllFetching") | |
| </div> | |
| </header> | |
| if hasTodos { | |
| <section> | |
| <ul class="divide-y divide-primary" data-testid="todos_list"> | |
| for i, todo := range mvc.Todos { | |
| @TodoRow(mvc.Mode, todo, i, i == mvc.EditingIdx) | |
| } | |
| </ul> | |
| </section> | |
| <footer class="flex flex-wrap items-center justify-between gap-2"> | |
| <span class="todo-count"> | |
| <strong data-testid="todo_count"> | |
| { fmt.Sprint(left) } | |
| if (len(mvc.Todos) > 1) { | |
| items | |
| } else { | |
| item | |
| } | |
| </strong> left | |
| </span> | |
| <div class="join"> | |
| for i := TodoViewModeAll; i < TodoViewModeLast; i++ { | |
| if i == mvc.Mode { | |
| <div class="btn btn-xs btn-primary join-item" data-testid={ TodoViewModeStrings[i] + "_mode" }>{ TodoViewModeStrings[i] }</div> | |
| } else { | |
| <button | |
| class="btn btn-xs join-item" | |
| data-on-click={ datastar.PutSSE("/api/todos/mode/%d", i) } | |
| data-testid={ TodoViewModeStrings[i] + "_mode" } | |
| > | |
| { TodoViewModeStrings[i] } | |
| </button> | |
| } | |
| } | |
| </div> | |
| <div class="join"> | |
| if completed > 0 { | |
| <div class="tooltip" data-tip={ fmt.Sprintf("clear %d completed todos", completed) }> | |
| <button | |
| class="btn btn-error btn-xs join-item" | |
| data-on-click={ datastar.DeleteSSE("/api/todos/-1") } | |
| data-testid="clear_todos" | |
| > | |
| @icon("material-symbols:delete") | |
| </button> | |
| </div> | |
| } | |
| <div class="tooltip" data-tip="Reset list"> | |
| <button | |
| class="btn btn-warning btn-xs join-item" | |
| data-on-click={ datastar.PutSSE("/api/todos/reset") } | |
| data-testid="reset_todos" | |
| > | |
| @icon("material-symbols:delete-sweep") | |
| </button> | |
| </div> | |
| </div> | |
| </footer> | |
| <footer class="flex justify-center text-xs"> | |
| <div>Click to edit, click away to cancel, press enter to save.</div> | |
| </footer> | |
| } | |
| </section> | |
| </div> | |
| </div> | |
| </div> | |
| } | |
| templ TodoInput(i int) { | |
| <input | |
| id="todoInput" | |
| data-testid="todos_input" | |
| class="flex-1 w-full italic input input-bordered input-lg" | |
| placeholder="What needs to be done?" | |
| data-bind-input | |
| data-on-keydown={ fmt.Sprintf(` | |
| if (evt.key !== 'Enter' || !input.value.trim().length) return; | |
| sse('/api/todos/%d/edit', {method:'put'}); | |
| input.value = ''; | |
| `, i) } | |
| if i >= 0 { | |
| data-on-click.outside:capture={ datastar.PutSSE("/api/todos/cancel") } | |
| } | |
| /> | |
| } | |
| templ TodoRow(mode TodoViewMode, todo *Todo, i int, isEditing bool) { | |
| {{ | |
| indicatorID := fmt.Sprintf("indicator%d", i) | |
| fetchingSignalName := fmt.Sprintf("fetching%d", i) | |
| }} | |
| if isEditing { | |
| @TodoInput(i) | |
| } else if ( | |
| mode == TodoViewModeAll) || | |
| (mode == TodoViewModeActive && !todo.Completed) || | |
| (mode == TodoViewModeCompleted && todo.Completed) { | |
| <li class="flex items-center gap-8 p-1 p-2 group" id={ fmt.Sprintf("todo%d", i) }> | |
| <label | |
| id={ fmt.Sprintf("toggle%d", i) } | |
| class="text-4xl cursor-pointer" | |
| data-on-click={ datastar.PostSSE("/api/todos/%d/toggle", i) } | |
| data-indicator={ fetchingSignalName } | |
| > | |
| if todo.Completed { | |
| @icon("material-symbols:check-box-outline") | |
| } else { | |
| @icon("material-symbols:check-box-outline-blank") | |
| } | |
| </label> | |
| <label | |
| id={ indicatorID } | |
| class="flex-1 text-lg cursor-pointer select-none" | |
| data-on-click={ datastar.GetSSE("/api/todos/%d/edit", i) } | |
| data-indicator={ fetchingSignalName } | |
| > | |
| { todo.Text } | |
| </label> | |
| @sseIndicator(fetchingSignalName) | |
| <button | |
| id={ fmt.Sprintf("delete%d", i) } | |
| class="invisible btn btn-error group-hover:visible" | |
| data-on-click={ datastar.DeleteSSE("/api/todos/%d", i) } | |
| data-testid={ fmt.Sprintf("delete_todo%d", i) } | |
| data-indicator={ fetchingSignalName } | |
| data-attrs-disabled={ fetchingSignalName + ".value" } | |
| > | |
| @icon("material-symbols:close") | |
| </button> | |
| </li> | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment