# Irgo Framework - AI Assistant Guide > This document instructs AI coding assistants on how to write applications using the Irgo framework. ## Framework Overview Irgo is a mobile application framework that uses: - **Go** as the backend runtime (compiled via gomobile) - **Datastar** for frontend interactivity (SSE-based hypermedia) - **templ** for type-safe HTML templates - **WebView** to render the UI (iOS WKWebView, Android WebView) The key insight: there's no network - SSE requests from Datastar are intercepted by the WebView and routed directly to Go handlers running in-process. ## Core Principles 1. **Hypermedia-Driven**: Return HTML fragments via SSE, not JSON. Datastar morphs HTML directly into the DOM. 2. **Server-Side State**: All state lives in Go. The WebView is a thin rendering layer. 3. **Signals for UI State**: Use Datastar signals for transient UI state (loading, modals, form inputs). 4. **Type Safety**: Use templ for compile-time template checking. ## Project Structure When working on an Irgo project, expect this structure: ``` project/ ├── main.go # Entry point, dev server ├── app/app.go # Router setup ├── handlers/ # HTTP handlers (business logic) ├── templates/ # templ files (.templ) ├── static/ # CSS, JS, images ├── mobile/mobile.go # Mobile bridge └── ios/ # Xcode project ``` ## Writing Handlers ### Handler Signatures Irgo has two types of handlers: ```go // Page handlers return (string, error) for full page renders func(ctx *router.Context) (string, error) // Datastar handlers return error and use ctx.SSE() for responses func(ctx *router.Context) error ``` ### Handler Examples ```go // Page handler - return rendered template r.GET("/", func(ctx *router.Context) (string, error) { return renderer.Render(templates.HomePage()) }) // Datastar GET handler r.DSGet("/items/{id}", func(ctx *router.Context) error { id := ctx.Param("id") item, err := getItem(id) if err != nil { return ctx.NotFound("Item not found") } return ctx.SSE().PatchTempl(templates.ItemDetail(item)) }) // Datastar POST with signals r.DSPost("/items", func(ctx *router.Context) error { var signals struct { Name string `json:"name"` } ctx.ReadSignals(&signals) item := createItem(signals.Name) sse := ctx.SSE() sse.PatchTempl(templates.ItemRow(item)) sse.PatchSignals(map[string]any{"name": ""}) // Clear input return nil }) // Datastar DELETE r.DSDelete("/items/{id}", func(ctx *router.Context) error { id := ctx.Param("id") deleteItem(id) return ctx.SSE().RemoveElements("#item-" + id) }) ``` ### Context Methods ```go ctx.Param("name") // URL path parameter: /users/{name} ctx.Query("key") // Query parameter: ?key=value ctx.FormValue("field") // Form field value ctx.Header("X-Custom") // Request header ctx.IsDatastar() // True if Datastar request ctx.ReadSignals(&v) // Parse Datastar signals into struct ctx.SSE() // Get SSE writer for responses ctx.Redirect("/path") // Redirect ``` ### SSE Response Methods ```go sse := ctx.SSE() sse.PatchTempl(component) // Morph templ component into DOM sse.PatchElements(id, html) // Morph raw HTML into element sse.PatchSignals(signals) // Update client-side signals sse.RemoveElements(selector)// Remove elements from DOM sse.Redirect(url) // Navigate to new URL sse.Console(level, msg) // Log to browser console ``` ## Writing Templates ### Template Syntax (templ) templ uses Go-like syntax with HTML: ```go package templates // Component with parameters templ Button(text string, action string) { } // Component with children templ Card(title string) {

{ title }

{ children... }
} // Using components templ HomePage() { @Layout("Home") { @Card("Welcome") {

Hello, world!

@Button("Learn More", "@get('/about')") } } } // Conditional rendering templ UserStatus(user *User) { if user != nil { Welcome, { user.Name } } else { Sign In } } // Loops templ ItemList(items []Item) { } // Conditional classes templ TodoItem(todo Todo) {
{ todo.Title }
} // Dynamic attributes templ Input(name string, value string, required bool) { } ``` ### Layout Pattern Always use a layout component for full pages: ```go templ Layout(title string) { { title } { children... } } templ AboutPage() { @Layout("About") {

About Us

} } ``` ### Fragment Pattern For Datastar partial updates, return just the fragment: ```go // Full page (initial load) templ TodosPage(todos []Todo) { @Layout("Todos") {

My Todos

@TodoList(todos) @AddTodoForm()
} } // Fragment (for Datastar updates) templ TodoList(todos []Todo) {
for _, todo := range todos { @TodoItem(todo) }
} // Single item fragment (identified by ID) templ TodoItem(todo Todo) {
{ todo.Title }
} ``` ## Datastar Patterns ### Core Attributes ```html
Shown when true
Toggles 'active' class
``` ### Event Handling ```html
``` ### Event Modifiers - `__debounce.Xms` - Wait for pause in events - `__throttle.Xms` - Limit event frequency - `__prevent` - Prevent default - `__stop` - Stop propagation - `__once` - Fire only once ### Initialization ```html
Loading...
``` ## Common Patterns ### CRUD Operations ```go // handlers/items.go package handlers type Item struct { ID string Name string } var items = make(map[string]Item) func MountItems(r *router.Router, renderer *render.TemplRenderer) { // List (full page) r.GET("/items", func(ctx *router.Context) (string, error) { list := getItems() return renderer.Render(templates.ItemsPage(list)) }) // Create (Datastar handler) r.DSPost("/items", func(ctx *router.Context) error { var signals struct { Name string `json:"name"` } ctx.ReadSignals(&signals) item := createItem(signals.Name) sse := ctx.SSE() sse.PatchTempl(templates.ItemRow(item)) sse.PatchSignals(map[string]any{"name": ""}) return nil }) // Read r.DSGet("/items/{id}", func(ctx *router.Context) error { id := ctx.Param("id") item, ok := items[id] if !ok { return ctx.NotFound("Item not found") } return ctx.SSE().PatchTempl(templates.ItemDetail(item)) }) // Update r.DSPatch("/items/{id}", func(ctx *router.Context) error { id := ctx.Param("id") var signals struct { Name string `json:"name"` } ctx.ReadSignals(&signals) item := updateItem(id, signals.Name) return ctx.SSE().PatchTempl(templates.ItemRow(item)) }) // Delete r.DSDelete("/items/{id}", func(ctx *router.Context) error { id := ctx.Param("id") deleteItem(id) return ctx.SSE().RemoveElements("#item-" + id) }) } ``` ### Search/Filter ```go // templates/search.templ templ SearchForm() {
} // handlers/search.go r.DSGet("/search", func(ctx *router.Context) error { var signals struct { Query string `json:"query"` } ctx.ReadSignals(&signals) results := search(signals.Query) return ctx.SSE().PatchTempl(templates.SearchResults(results)) }) ``` ### Infinite Scroll ```go templ ItemListWithPagination(items []Item, page int, hasMore bool) { for i, item := range items { if i == len(items)-1 && hasMore {
@ItemRow(item)
} else { @ItemRow(item) } } } ``` ### Tabs ```go templ TabContainer() {
@TabOne()
} ``` ### Modal Dialog ```go templ ModalTrigger() {
} ``` ### Form Validation ```go templ FormField(name, label, value, error string) {
{ error }
} // Handler r.DSPost("/validate/email", func(ctx *router.Context) error { var signals struct { Email string `json:"email"` } ctx.ReadSignals(&signals) if !isValidEmail(signals.Email) { return ctx.SSE().PatchTempl(templates.ValidationError("email", "Invalid email address")) } return ctx.SSE().RemoveElements("#email-error") }) ``` ## Real-Time Updates Datastar uses SSE natively for all interactions. For server push, use long-running SSE connections: ### Polling ```go templ LiveStatus() {
Checking...
} ``` ### Server Push ```go import "github.com/stukennedy/irgo/pkg/datastar" // Create hub hub := datastar.NewHub() go hub.Run() // Handle SSE connections r.GET("/events", func(ctx *router.Context) error { return hub.HandleSSE(ctx.ResponseWriter, ctx.Request, datastar.WithRoom("dashboard"), ) }) // Push updates from anywhere func updateDashboard(stats Stats) { html := renderer.RenderToString(templates.DashboardStats(stats)) hub.BroadcastToRoom("dashboard", datastar.PatchElements(html)) } ``` ## Mobile-Specific Patterns ### Safe Area Handling ```go templ MobileLayout(title string) { { children... } } ``` ### Touch-Friendly Buttons ```go templ MobileButton(text string) { } ``` ## Do's and Don'ts ### DO: 1. **Return HTML via SSE** from Datastar handlers 2. **Use templ** for all HTML generation 3. **Use signals** for UI state (loading, modals, form inputs) 4. **Use ctx.SSE().PatchTempl()** for DOM updates 5. **Use element IDs** to target where updates go 6. **Use ctx.SSE().PatchSignals()** to update client state 7. **Keep handlers simple** - one responsibility each 8. **Use Layout for full pages**, fragments for partial updates ### DON'T: 1. **Don't return JSON** - this is hypermedia, not an API 2. **Don't use JavaScript frameworks** - Datastar handles interactivity 3. **Don't manipulate DOM in JavaScript** - let Datastar do it 4. **Don't store business state in signals** - keep it in Go 5. **Don't use hx-* attributes** - use data-* Datastar attributes 6. **Don't forget element IDs** - Datastar needs them to target updates 7. **Don't return full pages for Datastar requests** - return fragments ## File Naming Conventions - Handlers: `handlers/*.go` (e.g., `handlers/items.go`) - Templates: `templates/*.templ` (e.g., `templates/items.templ`) - Generated: `templates/*_templ.go` (auto-generated, don't edit) - Layouts: `templates/layout.templ` - Components: `templates/components.templ` - Pages: `templates/.templ` (e.g., `templates/home.templ`) ## Testing Patterns ```go // handlers/items_test.go func TestCreateItem(t *testing.T) { r := setupTestRouter() client := irgotest.NewClient(r.Handler()) // Test Datastar request resp := client.Datastar().PostJSON("/items", map[string]any{ "name": "Test Item", }) resp.AssertOK(t) resp.AssertSSEEvent(t, "datastar-patch-elements") resp.AssertContains(t, "Test Item") } ``` ## Quick Reference ### Router ```go r.GET("/path", pageHandler) // Full page r.DSGet("/path", datastarHandler) // Datastar SSE r.DSPost("/path", datastarHandler) r.DSPut("/path", datastarHandler) r.DSPatch("/path", datastarHandler) r.DSDelete("/path", datastarHandler) r.Route("/prefix", func(r *router.Router) { ... }) r.Static("/static", "static") ``` ### Context ```go ctx.Param("id") // URL param ctx.Query("key") // Query string ctx.FormValue("name") // Form field ctx.IsDatastar() // Is Datastar request? ctx.ReadSignals(&v) // Parse signals ctx.SSE() // Get SSE writer ctx.Redirect("/path") // Redirect ``` ### SSE Writer ```go sse := ctx.SSE() sse.PatchTempl(component) // Morph component sse.PatchElements(id, html) // Morph raw HTML sse.PatchSignals(signals) // Update signals sse.RemoveElements(selector) // Remove elements sse.Redirect(url) // Navigate ``` ### Datastar Attributes ```html data-signals="{...}" data-bind:name data-text="$signal" data-show="$condition" data-class:name="$cond" data-on:event="action" data-on:event__mod="action" data-init="action" data-intersect="action" ``` ### Datastar Actions ```html @get('/path') @post('/path') @put('/path') @patch('/path') @delete('/path') $signal = value $signal++ ``` ### templ Syntax ```go templ Name(args) { } // Define component @Component() // Use component { variable } // Output value { children... } // Slot for children if cond { } else { } // Conditional for _, x := range xs { } // Loop class={ templ.KV("a", bool) } // Conditional class attr?={ bool } // Conditional attribute ```