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 Update

The Datastar/irgo approach:

User Click → Datastar Request → Go Handler → SSE Stream → DOM Morph

Your 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>}
Tip

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:

MethodDescription
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