Datastar Integration
Datastar is at the heart of irgo's interactivity model. It uses Server-Sent Events (SSE) and reactive signals to deliver real-time updates from your Go handlers to the browser.
The Hypermedia Pattern
The traditional SPA approach:
User Click → JavaScript → API Call → JSON Response → JavaScript → DOM UpdateThe Datastar/irgo approach:
User Click → Datastar Request → Go Handler → SSE Stream → DOM MorphYour Go handlers use SSE to stream HTML updates. Datastar morphs them into the DOM. No client-side rendering, no state synchronization, no JavaScript frameworks.
Core Datastar Attributes
Making Requests
<!-- GET request --><button data-on:click="@get('/api/users')">Load Users</button><!-- POST request --><button data-on:click="@post('/api/users')">Create User</button><!-- PUT request --><button data-on:click="@put('/api/users/123')">Update</button><!-- DELETE request --><button data-on:click="@delete('/api/users/123')">Delete</button><!-- PATCH request --><button data-on:click="@patch('/api/users/123')">Patch</button>Signals (Reactive State)
Datastar uses signals for client-side reactive state. Define them with data-signals:
<!-- Initialize signals --><div data-signals="{name: '', count: 0, items: []}"> <!-- Two-way binding --> <input type="text" data-bind:name placeholder="Enter name" /> <!-- Display signal value --> <span data-text="$name"></span> <!-- Use in expressions --> <span data-text="'Count: ' + $count"></span></div>Binding and Display
<!-- Two-way input binding --><input data-bind:email type="email" /><!-- Checkbox binding --><input type="checkbox" data-bind:isActive /><!-- Display text --><span data-text="$username"></span><!-- Conditional display --><div data-show="$isLoggedIn">Welcome back!</div><!-- Dynamic classes --><div data-class:active="$isSelected">Item</div><div data-class:hidden="!$visible">Content</div>Event Handling
<!-- Click events --><button data-on:click="@post('/api/action')">Submit</button><!-- With signal values (sent automatically) --><button data-on:click="@post('/api/save')">Save</button><!-- Update signals on click --><button data-on:click="$count++">Increment</button><!-- Keyboard events --><input data-on:keyup="@get('/search')" /><!-- With modifiers --><input data-on:keyup__debounce.300ms="@get('/search')" /><form data-on:submit__prevent="@post('/submit')"><!-- Mouse events --><div data-on:mousemove__throttle.100ms="@get('/track')"> Track mouse</div>Event Modifiers
<!-- Debounce (wait for pause in events) --><input data-on:keyup__debounce.300ms="@get('/search')" /><!-- Throttle (limit event frequency) --><div data-on:scroll__throttle.100ms="@get('/load-more')"><!-- Prevent default --><form data-on:submit__prevent="@post('/submit')"><!-- Stop propagation --><button data-on:click__stop="@delete('/item')"><!-- Once (fire only once) --><div data-on:click__once="@get('/init')"><!-- Combine modifiers --><input data-on:keyup__debounce.500ms__prevent="@get('/search')" />Go Handler Patterns
Basic SSE Response
// Datastar handlers return error (not string, error)func CreateUser(ctx *router.Context) error { var signals struct { Name string `json:"name"` Email string `json:"email"` } ctx.ReadSignals(&signals) user := createUser(signals.Name, signals.Email) sse := ctx.SSE() return sse.PatchTempl(templates.UserCard(user))}Multiple Updates
func ToggleTodo(ctx *router.Context) error { id := ctx.Param("id") todo := toggleTodo(id) count := getTodoCount() sse := ctx.SSE() // Update the todo item sse.PatchTempl(templates.TodoItem(todo)) // Update the counter (different element) sse.PatchTempl(templates.TodoCount(count)) return nil}Updating Signals from Server
func SubmitForm(ctx *router.Context) error { var signals struct { Title string `json:"title"` } ctx.ReadSignals(&signals) // Process the form... item := createItem(signals.Title) sse := ctx.SSE() // Add the new item to DOM sse.PatchTempl(templates.ItemCard(item)) // Clear the input by updating the signal sse.PatchSignals(map[string]any{"title": ""}) return nil}Removing Elements
func DeleteItem(ctx *router.Context) error { id := ctx.Param("id") deleteItem(id) sse := ctx.SSE() // Remove the element from DOM return sse.RemoveElements("#item-" + id)}Redirects
func CreateProject(ctx *router.Context) error { var signals struct { Name string `json:"name"` } ctx.ReadSignals(&signals) project := createProject(signals.Name) sse := ctx.SSE() return sse.Redirect("/projects/" + project.ID)}Template Patterns
Basic Form
templ CreateForm() { <form data-signals="{title: ''}" data-on:submit__prevent="@post('/items')"> <input type="text" data-bind:title placeholder="Enter title" required /> <button type="submit">Create</button> </form> <div id="items-list"></div>}Todo Item with Actions
templ TodoItem(todo Todo) { <div id={ "todo-" + todo.ID } class="todo-item"> <input type="checkbox" checked?={ todo.Done } data-on:click={ "@patch('/todos/" + todo.ID + "')" } /> <span class={ templ.KV("completed", todo.Done) }> { todo.Title } </span> <button data-on:click={ "@delete('/todos/" + todo.ID + "')" }> Delete </button> </div>}Search with Debounce
templ SearchBox() { <div data-signals="{query: ''}"> <input type="text" data-bind:query data-on:keyup__debounce.300ms="@get('/search')" placeholder="Search..." /> <div id="search-results"></div> </div>}Loading Indicator
templ LoadButton() { <div data-signals="{loading: false}"> <button data-on:click="$loading = true; @get('/data').then(() => $loading = false)" data-attr:disabled="$loading" > <span data-show="!$loading">Load Data</span> <span data-show="$loading">Loading...</span> </button> </div>}With Datastar, you can use signals for UI state (loading, modals, tabs) while keeping business data on the server. This hybrid approach gives you the best of both worlds.
Common Patterns
Modal Dialog
templ ModalTrigger() { <div data-signals="{showModal: false}"> <button data-on:click="$showModal = true"> Open Modal </button> <div class="modal-backdrop" data-show="$showModal" data-on:click="$showModal = false"> <div class="modal" data-on:click__stop=""> <h2>Confirm Action</h2> <p>Are you sure?</p> <button data-on:click="@post('/confirm'); $showModal = false"> Confirm </button> <button data-on:click="$showModal = false"> Cancel </button> </div> </div> </div>}Tabs
templ Tabs() { <div data-signals="{activeTab: 'overview'}"> <nav class="tab-nav"> <button data-class:active="$activeTab === 'overview'" data-on:click="$activeTab = 'overview'; @get('/tabs/overview')" > Overview </button> <button data-class:active="$activeTab === 'settings'" data-on:click="$activeTab = 'settings'; @get('/tabs/settings')" > Settings </button> </nav> <div id="tab-content"> <!-- Content loads here --> </div> </div>}Infinite Scroll
templ FeedItem(item Item, isLast bool, nextPage int) { if isLast { <div id={ "item-" + item.ID } data-intersect={ fmt.Sprintf("@get('/feed?page=%d')", nextPage) } > @ItemCard(item) </div> } else { <div id={ "item-" + item.ID }> @ItemCard(item) </div> }}SSE Response Events
The Datastar Go SDK provides these SSE response methods:
| Method | Description |
|---|---|
PatchTempl(c) | Morph templ component into DOM |
PatchElements(id, html) | Morph raw HTML into element |
PatchSignals(map) | Update client-side signals |
RemoveElements(selector) | Remove elements from DOM |
Redirect(url) | Navigate to new URL |
Console(level, msg) | Log to browser console |
Initialization
Use data-init to run code when an element is added to the DOM:
<!-- Load data on page load --><div data-init="@get('/api/initial-data')"> Loading...</div><!-- Initialize signals and fetch --><div data-signals="{items: []}" data-init="@get('/api/items')"> <div id="items-container"></div></div>Next Steps
- Templ Templates - Template syntax reference
- Routing & Handlers - Handler patterns
- Datastar Documentation - Full Datastar reference
