Examples
Learn irgo through complete, working examples. Each example demonstrates key patterns and can be used as a starting point for your own apps.
Todo App
The classic todo app demonstrates CRUD operations, form handling, and Datastar interactions.
Handler
handlers/todos.go
package handlersimport ( "fmt" "myapp/templates" "github.com/stukennedy/irgo/pkg/router")type Todo struct { ID string Title string Done bool}var todos = []Todo{}var nextID = 1func ListTodos(ctx *router.Context) error { return ctx.SSE().PatchTempl(templates.TodoList(todos))}func CreateTodo(ctx *router.Context) error { var signals struct { Title string `json:"title"` } ctx.ReadSignals(&signals) if signals.Title == "" { return ctx.BadRequest("Title is required") } todo := Todo{ ID: fmt.Sprintf("%d", nextID), Title: signals.Title, Done: false, } nextID++ todos = append(todos, todo) sse := ctx.SSE() sse.PatchTempl(templates.TodoItem(todo)) sse.PatchSignals(map[string]any{"title": ""}) // Clear input return nil}func ToggleTodo(ctx *router.Context) error { id := ctx.Param("id") for i := range todos { if todos[i].ID == id { todos[i].Done = !todos[i].Done return ctx.SSE().PatchTempl(templates.TodoItem(todos[i])) } } return ctx.NotFound("Todo not found")}func DeleteTodo(ctx *router.Context) error { id := ctx.Param("id") for i := range todos { if todos[i].ID == id { todos = append(todos[:i], todos[i+1:]...) return ctx.SSE().RemoveElements("#todo-" + id) } } return ctx.NotFound("Todo not found")}Templates
templates/todos.templ
package templatestempl TodoPage() { @Layout("Todos") { <div class="container"> <h1>My Todos</h1> @TodoForm() <div id="todo-list"> for _, todo := range todos { @TodoItem(todo) } </div> </div> }}templ TodoForm() { <form data-signals="{title: ''}" data-on:submit__prevent="@post('/todos')" class="todo-form" > <input type="text" data-bind:title placeholder="What needs to be done?" required autofocus /> <button type="submit">Add</button> </form>}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 + "')" } class="delete-btn" > x </button> </div>}templ TodoList(todos []Todo) { for _, todo := range todos { @TodoItem(todo) } if len(todos) == 0 { <p class="empty-state">No todos yet. Add one above!</p> }}Routes
app/app.go
func NewRouter() *router.Router { r := router.New() r.GET("/", handlers.TodoPage) // Full page render r.DSPost("/todos", handlers.CreateTodo) r.DSPatch("/todos/{id}", handlers.ToggleTodo) r.DSDelete("/todos/{id}", handlers.DeleteTodo) return r}Search with Debounce
Real-time search with debounced input using Datastar modifiers.
templates/search.templ
templ SearchPage() { @Layout("Search") { <div class="search-container" data-signals="{query: '', loading: false}"> <input type="text" data-bind:query data-on:keyup__debounce.300ms="$loading = true; @get('/search')" placeholder="Search users..." /> <span data-show="$loading" class="spinner">Searching...</span> <div id="results"></div> </div> }}templ SearchResults(users []User, query string) { if len(users) == 0 { <p class="no-results">No users found for "{ query }"</p> } else { <ul class="user-list"> for _, user := range users { @UserCard(user) } </ul> }}handlers/search.go
func Search(ctx *router.Context) error { var signals struct { Query string `json:"query"` } ctx.ReadSignals(&signals) if signals.Query == "" { return nil } users := searchUsers(signals.Query) sse := ctx.SSE() sse.PatchTempl(templates.SearchResults(users, signals.Query)) sse.PatchSignals(map[string]any{"loading": false}) return nil}Modal Dialog
A reusable modal pattern using Datastar signals for state.
templates/modal.templ
templ ModalContainer() { <div data-signals="{showModal: false, modalAction: ''}"> <div id="modal-container"></div> </div>}templ ConfirmModal(title string, message string, action string) { <div class="modal-backdrop" data-show="$showModal" data-on:click="$showModal = false"> <div class="modal" data-on:click__stop=""> <h2>{ title }</h2> <p>{ message }</p> <div class="modal-actions"> <button data-on:click={ fmt.Sprintf("@post('%s'); $showModal = false", action) } class="btn-danger" > Confirm </button> <button data-on:click="$showModal = false" class="btn-secondary" > Cancel </button> </div> </div> </div>}templ DeleteUserButton(userID string) { <button data-on:click={ fmt.Sprintf("$modalAction = '/users/%s'; $showModal = true", userID) } class="btn-danger" > Delete User </button>}handlers/modal.go
func DeleteUser(ctx *router.Context) error { userID := ctx.Param("id") deleteUser(userID) sse := ctx.SSE() sse.RemoveElements("#user-" + userID) return nil}Infinite Scroll
Load more items as the user scrolls using Datastar's intersection observer.
templates/feed.templ
templ Feed(posts []Post, page int, hasMore bool) { <div class="feed"> for i, post := range posts { if i == len(posts)-1 && hasMore { // Last item triggers loading more <div data-intersect={ fmt.Sprintf("@get('/feed?page=%d')", page+1) }> @PostCard(post) </div> } else { @PostCard(post) } } if !hasMore && len(posts) > 0 { <p class="end-of-feed">You've reached the end!</p> } </div>}templ PostCard(post Post) { <article class="post-card"> <h3>{ post.Title }</h3> <p>{ post.Excerpt }</p> <time>{ post.CreatedAt.Format("Jan 2, 2006") }</time> </article>}Tabs
Tab navigation using Datastar signals for active state.
templates/tabs.templ
templ TabsPage() { @Layout("Dashboard") { <div class="tabs-container" data-signals="{activeTab: 'overview'}"> <nav class="tab-nav"> <button data-class:active="$activeTab === 'overview'" data-on:click="$activeTab = 'overview'; @get('/dashboard/overview')" > Overview </button> <button data-class:active="$activeTab === 'analytics'" data-on:click="$activeTab = 'analytics'; @get('/dashboard/analytics')" > Analytics </button> <button data-class:active="$activeTab === 'settings'" data-on:click="$activeTab = 'settings'; @get('/dashboard/settings')" > Settings </button> </nav> <div id="tab-content"> <!-- Content loads here --> </div> </div> }}templ OverviewTab() { <div id="tab-content" class="tab-panel"> <h2>Overview</h2> <p>Your dashboard overview content here.</p> </div>}Running the Examples
Create a new irgo project and try these patterns:
irgo new myappcd myappgo mod tidybun installirgo devNext Steps
- Getting Started - Create your own project
- Datastar Integration - More Datastar patterns
- GitHub - Browse the source code
