Skip to content

Latest commit

 

History

History
336 lines (261 loc) · 9.21 KB

File metadata and controls

336 lines (261 loc) · 9.21 KB

Getting Started with HxComponents

This guide walks you through setting up and registering your first HxComponents application.

Project Structure

myproject/
├── main.go
├── go.mod
├── components/
│   └── counter/
│       ├── counter.go
│       ├── counter.templ
│       └── counter_templ.go  (generated by templ)
└── pages/
    └── index.templ

Installation

Step 1: Install Dependencies

go get github.com/a-h/templ
go get github.com/go-chi/chi/v5
go get github.com/ocomsoft/HxComponents
go install github.com/a-h/templ/cmd/templ@latest

Step 2: Create Your Component

See the migration guides for examples of creating components from React, Vue, or Svelte.

Step 3: Register Components and Setup Server

main.go:

package main

import (
	"log"
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
	"github.com/ocomsoft/HxComponents/components"
	"myproject/components/counter"
	"myproject/pages"
)

func main() {
	// Create the component registry
	registry := components.NewRegistry()

	// Register components with their URL paths
	// The string parameter becomes the URL: /component/counter
	components.Register[*counter.CounterComponent](registry, "counter")

	// You can register multiple components
	// components.Register[*todolist.TodoListComponent](registry, "todolist")
	// components.Register[*userform.UserFormComponent](registry, "userform")

	// Setup router
	router := chi.NewRouter()
	router.Use(middleware.Logger)      // Log all requests
	router.Use(middleware.Recoverer)   // Recover from panics

	// Mount component handlers (supports both GET and POST)
	router.Get("/component/*", registry.Handler)
	router.Post("/component/*", registry.Handler)

	// Serve your pages
	router.Get("/", func(w http.ResponseWriter, r *http.Request) {
		if err := pages.IndexPage().Render(r.Context(), w); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	})

	// Serve static files (CSS, JS, images)
	router.Handle("/static/*", http.StripPrefix("/static/",
		http.FileServer(http.Dir("./static"))))

	log.Println("Server starting on http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", router))
}

Step 4: Create Your Index Page

pages/index.templ:

package pages

import "myproject/components/counter"

templ IndexPage() {
	<!DOCTYPE html>
	<html>
		<head>
			<title>My HxComponents App</title>
			<!-- Load HTMX -->
			<script src="https://unpkg.com/htmx.org@1.9.10"></script>
		</head>
		<body>
			<h1>Welcome to HxComponents</h1>

			<!-- Render component with initial state -->
			@counter.Counter(counter.CounterComponent{Count: 0})
		</body>
	</html>
}

Step 5: Generate Templates and Run

# Generate templ files (run this whenever you change .templ files)
templ generate

# Run your server
go run main.go

Visit http://localhost:8080 and your component will be live!

Component Registration Options

// Basic registration
components.Register[*MyComponent](registry, "mycomponent")

// The component will be available at:
// - GET  /component/mycomponent?param1=value1&param2=value2
// - POST /component/mycomponent (with form data)

// Components can have initial state when embedded in pages
comp := &counter.CounterComponent{Count: 10}
@Counter(*comp)

How Components are Resolved

  1. Client makes request to /component/counter with form data or query params
  2. Registry finds the registered component by name
  3. Registry creates a new instance of the component
  4. Registry parses form data into component fields (using form tags)
  5. Registry calls lifecycle hooks in order:
    • BeforeEvent(ctx, eventName) if implemented
    • On{EventName}() method if hxc-event parameter is present
    • AfterEvent(ctx, eventName) if implemented
    • Process(ctx) if implemented
  6. Component is rendered using its Render() method
  7. HTML is returned to the client
  8. HTMX swaps the content based on hx-target and hx-swap attributes

Component Lifecycle

Request → Parse Form Data → BeforeEvent → On{EventName} → AfterEvent → Process → Render → Response

Lifecycle Hooks

BeforeEvent(ctx context.Context, eventName string) error

  • Called before any event handler
  • Use for authentication, loading data from database/session
  • Return error to abort the request
  • Context provides request-scoped values and cancellation

On{EventName}(ctx context.Context) error

  • Event handler method (e.g., OnSubmit, OnAddItem)
  • Called when hxc-event parameter matches the event name
  • Context provides request-scoped values and cancellation
  • Return error to indicate failure

AfterEvent(ctx context.Context, eventName string) error

  • Called after successful event handler
  • Use for saving data, triggering webhooks, notifications
  • Return error to indicate failure
  • Context provides request-scoped values and cancellation

Process(ctx context.Context) error

  • Called after all events, before rendering
  • Use for final data transformations
  • Return error to indicate failure
  • Context provides request-scoped values and cancellation

Constructor Pattern with Init

Components can use a constructor function pattern to be easily instantiated in templ templates while still having initialization logic. This is done using the Init interface.

The Init Interface

type Initializer interface {
    Init(ctx context.Context) error
}

The Init method is called:

  • After form decoding (in HTTP handlers)
  • Before validation
  • Before event handling
  • Before processing

Example: Card Component with Constructor

Component struct:

package card

import (
    "context"
    "fmt"
    "time"
)

type CardComponent struct {
    Title       string `form:"title"`
    Count       int    `form:"count"`
    Description string `json:"-"` // Computed field
    Timestamp   string `json:"-"` // Computed field
}

// Constructor function for use in templ templates
func NewCard(ctx context.Context, title string, count int) *CardComponent {
    c := &CardComponent{
        Title: title,
        Count: count,
    }
    // Call Init to set up defaults and computed fields
    _ = c.Init(ctx)
    return c
}

// Init implements the Initializer interface
func (c *CardComponent) Init(ctx context.Context) error {
    // Set defaults
    if c.Title == "" {
        c.Title = "Untitled Card"
    }

    // Compute derived fields
    c.Description = fmt.Sprintf("This card contains %d item(s)", c.Count)
    c.Timestamp = time.Now().Format("2006-01-02 15:04:05")

    // You could also load data from database here using ctx
    // user, err := db.GetUser(ctx, userID)
    // if err != nil {
    //     return err
    // }

    return nil
}

// Render implements templ.Component
func (c *CardComponent) Render(ctx context.Context, w io.Writer) error {
    return CardTemplate(*c).Render(ctx, w)
}

Usage in Templ Templates

package pages

import "myproject/components/card"

templ Dashboard() {
    <div class="dashboard">
        <h1>Dashboard</h1>

        // Use constructor to create components with specific values
        @card.NewCard(ctx, "Active Users", 1250)
        @card.NewCard(ctx, "Revenue", 45000)
        @card.NewCard(ctx, "Orders", 328)

        // All cards get Init() called automatically
        // Computed fields like Description and Timestamp are set
    </div>
}

Usage as HTMX Component

The same component works as an HTMX endpoint:

<form hx-post="/component/card" hx-target="#result">
    <input name="title" placeholder="Card Title"/>
    <input name="count" type="number" value="0"/>
    <button>Create Card</button>
</form>
<div id="result"></div>

When submitted, the registry will:

  1. Decode form data into CardComponent
  2. Call Init(ctx) to set defaults and computed fields
  3. Call other lifecycle hooks if present
  4. Render the component

Benefits of Constructor Pattern

  1. Type-Safe: Constructor enforces required parameters at compile time
  2. Reusable: Same component works in templates AND as HTMX endpoint
  3. Initialized: Computed fields and defaults are always set
  4. Context-Aware: Can load data from database in Init(ctx)
  5. Clean Templates: @NewCard(ctx, "Title", 100) is cleaner than setting struct fields

Updated Lifecycle

Request → Parse Form → Init → Validate → BeforeEvent → On{EventName} → AfterEvent → Process → Render → Response
                        ↑
                        New step!

Development Workflow

  1. Create component struct with form tags for inputs
  2. Add event handlers as On{EventName}() error methods
  3. Create templ template for rendering
  4. Implement lifecycle hooks if needed (BeforeEvent, AfterEvent)
  5. Register component in main.go
  6. Run templ generate to compile templates
  7. Test your component

Next Steps