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 HTMX interactions.

Handler

handlers/todos.go
package handlersimport (    "myapp/templates"    "github.com/stukennedy/irgo/pkg/render"    "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) (string, error) {    return renderer.Render(templates.TodoList(todos))}func CreateTodo(ctx *router.Context) (string, error) {    title := ctx.FormValue("title")    if title == "" {        return "", ctx.BadRequest("Title is required")    }    todo := Todo{        ID:    fmt.Sprintf("%d", nextID),        Title: title,        Done:  false,    }    nextID++    todos = append(todos, todo)    return renderer.Render(templates.TodoItem(todo))}func ToggleTodo(ctx *router.Context) (string, error) {    id := ctx.Param("id")    for i := range todos {        if todos[i].ID == id {            todos[i].Done = !todos[i].Done            return renderer.Render(templates.TodoItem(todos[i]))        }    }    return "", ctx.NotFound("Todo not found")}func DeleteTodo(ctx *router.Context) (string, error) {    id := ctx.Param("id")    for i := range todos {        if todos[i].ID == id {            todos = append(todos[:i], todos[i+1:]...)            return "", nil // hx-swap="delete" removes the element        }    }    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        hx-post="/todos"        hx-target="#todo-list"        hx-swap="beforeend"        hx-on::after-request="this.reset()"        class="todo-form"    >        <input            type="text"            name="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 }            hx-post={ "/todos/" + todo.ID + "/toggle" }            hx-target={ "#todo-" + todo.ID }            hx-swap="outerHTML"        />        <span class={ templ.KV("completed", todo.Done) }>            { todo.Title }        </span>        <button            hx-delete={ "/todos/" + todo.ID }            hx-target={ "#todo-" + todo.ID }            hx-swap="delete"            hx-confirm="Delete this todo?"            class="delete-btn"        >            ×        </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.ListTodos)    r.POST("/todos", handlers.CreateTodo)    r.POST("/todos/{id}/toggle", handlers.ToggleTodo)    r.DELETE("/todos/{id}", handlers.DeleteTodo)    return r}

Search with Debounce

Real-time search with debounced input.

templates/search.templ
templ SearchPage() {    @Layout("Search") {        <div class="search-container">            <input                type="text"                name="q"                placeholder="Search users..."                hx-get="/search"                hx-trigger="keyup changed delay:300ms"                hx-target="#results"                hx-indicator="#spinner"            />            <span id="spinner" class="htmx-indicator">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) (string, error) {    query := ctx.Query("q")    if query == "" {        return "", nil    }    users := searchUsers(query)    return renderer.Render(templates.SearchResults(users, query))}

Modal Dialog

A reusable modal pattern with HTMX.

templates/modal.templ
templ ModalContainer() {    <div id="modal-container"></div>}templ ConfirmModal(title string, message string, action string) {    <div class="modal-backdrop" hx-on:click="this.remove()">        <div class="modal" hx-on:click="event.stopPropagation()">            <h2>{ title }</h2>            <p>{ message }</p>            <div class="modal-actions">                <button                    hx-post={ action }                    hx-target="#modal-container"                    hx-swap="innerHTML"                    class="btn-danger"                >                    Confirm                </button>                <button                    hx-on:click="this.closest('.modal-backdrop').remove()"                    class="btn-secondary"                >                    Cancel                </button>            </div>        </div>    </div>}templ DeleteUserButton(userID string) {    <button        hx-get={ "/modal/delete-user/" + userID }        hx-target="#modal-container"        hx-swap="innerHTML"        class="btn-danger"    >        Delete User    </button>}
handlers/modal.go
func ShowDeleteModal(ctx *router.Context) (string, error) {    userID := ctx.Param("id")    return renderer.Render(templates.ConfirmModal(        "Delete User",        "Are you sure you want to delete this user?",        "/users/" + userID,    ))}func DeleteUser(ctx *router.Context) (string, error) {    userID := ctx.Param("id")    deleteUser(userID)    // Close modal and trigger refresh    ctx.Trigger("userDeleted")    return "", nil}

Infinite Scroll

Load more items as the user scrolls.

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                    hx-get={ fmt.Sprintf("/feed?page=%d", page+1) }                    hx-trigger="revealed"                    hx-swap="afterend"                >                    @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 with HTMX.

templates/tabs.templ
templ TabsPage(activeTab string) {    @Layout("Dashboard") {        <div class="tabs-container">            <nav class="tab-nav">                <button                    class={ "tab", templ.KV("active", activeTab == "overview") }                    hx-get="/dashboard/overview"                    hx-target="#tab-content"                    hx-push-url="true"                >                    Overview                </button>                <button                    class={ "tab", templ.KV("active", activeTab == "analytics") }                    hx-get="/dashboard/analytics"                    hx-target="#tab-content"                    hx-push-url="true"                >                    Analytics                </button>                <button                    class={ "tab", templ.KV("active", activeTab == "settings") }                    hx-get="/dashboard/settings"                    hx-target="#tab-content"                    hx-push-url="true"                >                    Settings                </button>            </nav>            <div id="tab-content">                { children... }            </div>        </div>    }}templ OverviewTab() {    <div class="tab-panel">        <h2>Overview</h2>        <p>Your dashboard overview content here.</p>    </div>}

Running the Examples

The todo example is included in the irgo repository:

git clone https://github.com/stukennedy/irgo.gitcd irgo/examples/todogo mod tidybun installirgo dev

Next Steps