Skip to content

Latest commit

 

History

History
246 lines (189 loc) · 7.22 KB

File metadata and controls

246 lines (189 loc) · 7.22 KB

Views

Catuaba uses Templ for type-safe, compiled HTML templates. Views live in app/views/ and are organized by resource.

How Templ works

Templ files (.templ) are compiled to Go code at build time. They provide:

  • Type safety — template arguments are Go types, caught at compile time
  • IDE support — autocomplete, go-to-definition, error highlighting
  • Performance — 5-10x faster than html/template
  • Composability — components call other components like functions

Directory structure

app/views/
├── layouts/
│   └── base.templ          # HTML5 base layout (head, nav, flash, footer)
├── components/
│   ├── nav.templ            # Navigation bar
│   ├── flash.templ          # Flash messages (auto-dismiss with Alpine.js)
│   ├── pagination.templ     # Pagination with HTMX
│   └── form_field.templ     # Reusable form field
├── pages/
│   ├── home.templ           # Landing page
│   └── not_found.templ      # 404 page
└── {resource}/              # Generated by scaffold
    ├── index.templ          # List/table view
    ├── show.templ           # Detail view
    └── form.templ           # Create/edit form (shared)

Base layout

Every page uses the base layout, which provides the HTML shell, navigation, flash messages, and footer:

// app/views/layouts/base.templ
templ Base(title string, appName string, flashMsg string, flashType string) {
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
            <title>{ title } - { appName }</title>
            <link rel="stylesheet" href="/static/css/app.css"/>
            <script src="https://unpkg.com/htmx.org@2.0.4"></script>
            <script defer src="https://unpkg.com/alpinejs@3.14.8/dist/cdn.min.js"></script>
        </head>
        <body class="min-h-screen bg-gray-50" hx-boost="true">
            @components.Nav(appName)
            if flashMsg != "" {
                @components.Flash(flashMsg, flashType)
            }
            <main class="flex-1">
                { children... }
            </main>
            <footer>...</footer>
        </body>
    </html>
}

Use it in any view:

templ MyPage() {
    @layouts.Base("Page Title", "", "", "") {
        <h1>Hello!</h1>
    }
}

Scaffold views

catuaba g scaffold Post title:string body:text published:boolean generates three views:

Index (app/views/posts/index.templ)

A table with all records, pagination, and action links:

templ Index(items []models.Post, page int, totalPages int, flashMsg string, flashType string) {
    @layouts.Base("Posts", "", flashMsg, flashType) {
        // Header with "New Post" button
        // Table with columns for each field
        // Pagination component
    }
}

Features:

  • Pagination — page/limit from query params, rendered with the Pagination component
  • Boolean fields — displayed as colored badges ("Yes" / "No")
  • Text fields — truncated in the table, full text on show page
  • Action links — View and Edit for each row

Show (app/views/posts/show.templ)

A detail view with all fields and Edit/Delete actions:

templ Show(item models.Post, flashMsg string, flashType string) {
    @layouts.Base("Post Details", "", flashMsg, flashType) {
        // Back link
        // Card with field labels and values
        // Edit button + Delete form with confirmation
        // CreatedAt and UpdatedAt timestamps
    }
}

Form (app/views/posts/form.templ)

A shared form for both creating and editing:

templ Form(item models.Post, isEdit bool, errors map[string]string, flashMsg string, flashType string) {
    @layouts.Base(formTitle(isEdit), "", flashMsg, flashType) {
        // Error display (if any)
        // Form fields based on type:
        //   string  → <input type="text">
        //   text    → <textarea>
        //   integer/float → <input type="number">
        //   boolean → <input type="hidden" value="false"> + <input type="checkbox">
        //   datetime → <input type="datetime-local">
        // Submit button (Create / Update)
    }
}

The isEdit flag controls the form action URL and button label. Helper functions formTitle() and formAction() are defined at the bottom of the file.

Boolean checkbox pattern: A hidden input with value="false" is placed before the checkbox so unchecked state sends false to the server.

Components

Flash messages

Auto-dismiss after 5 seconds using Alpine.js:

@components.Flash("Post created!", "success")

Types: success (green), error (red), warning (yellow).

Pagination

HTMX-powered pagination (partial page updates):

@components.Pagination(currentPage, totalPages, "/posts")

Navigation

Responsive navbar with optional auth links (when --auth is used).

HTMX integration

The base layout includes:

  • hx-boost="true" on <body> — all links and forms use AJAX by default
  • CSRF token injection via htmx:configRequest event — sends the X-CSRF-Token header automatically
  • Pagination uses hx-get and hx-target="#content" for partial page updates

Adding HTMX interactions

You can add HTMX attributes to any element:

// Inline delete with confirmation
<button
    hx-post={ fmt.Sprintf("/posts/%d/delete", item.ID) }
    hx-confirm="Are you sure?"
    hx-target="#content"
    hx-swap="outerHTML"
    class="text-red-600 hover:underline"
>
    Delete
</button>

// Live search
<input
    type="search"
    name="q"
    hx-get="/posts"
    hx-trigger="keyup changed delay:300ms"
    hx-target="#content"
    placeholder="Search posts..."
/>

Alpine.js integration

Alpine.js is included for client-side interactivity:

// Toggle visibility
<div x-data="{ open: false }">
    <button @click="open = !open">Toggle</button>
    <div x-show="open" x-transition>Content here</div>
</div>

// Dropdown menu
<div x-data="{ open: false }" @click.outside="open = false">
    <button @click="open = !open">Menu</button>
    <div x-show="open" class="absolute mt-2 bg-white shadow rounded">
        <a href="/profile">Profile</a>
        <a href="/settings">Settings</a>
    </div>
</div>

Generating standalone views

For pages that don't belong to a resource:

catuaba g view about

Creates app/views/pages/about.templ with the base layout.

CSS with Tailwind

Views use Tailwind CSS classes directly in the HTML. The CSS is compiled from static/css/input.css to static/css/app.css by the Tailwind CLI.

During development, make dev runs the Tailwind watcher that recompiles on save.

Development workflow

  1. Edit .templ files
  2. Templ watcher recompiles to _templ.go files
  3. wgo detects the Go file change and rebuilds the server
  4. Browser refreshes (or HTMX swaps the content)

All three watchers run in parallel with make dev.

Learn more