Introduction

htmgo is a lightweight pure go way to build interactive websites / web applications using go & htmx. We give you the utilities to build html using pure go code in a reusable way (go functions are components) while also providing htmx functions to add interactivity to your app.

func DocsPage(ctx *h.RequestContext) *h.Page {
	assets := ctx.Get("embeddedMarkdown").(fs.FS)
	pages := dirwalk.WalkPages("md/docs", assets)

	return h.NewPage(base.RootPage(
		h.Div(
			h.Class("flex flex-col md:flex-row gap-4 justify-center mb-12"),
			partials.DocSidebar(pages),
			h.Div(
				h.Class("flex flex-col justify-center items-center mt-6"),
				h.List(pages, func(page *dirwalk.Page, index int) *h.Element {
					return h.Div(
						h.Class("border-b border-b-slate-300 w-full pb-8 mb-8"),
						MarkdownContent(ctx, 
                            page.FilePath, 
                            partials.CreateAnchor(page.Parts)),
					)
				}),
			),
		),
	))
}

The site you are reading now was written with htmgo!


Quick overview

  1. Server side rendered html, deploy as a single binary

  2. Built in live reloading

  3. Built in support for various libraries such as tailwindcss, htmx

  4. Go functions are components, no special syntax neccessary to learn

  5. Many composable utility functions to streamline development and reduce boilerplate

    func ChangeTab(ctx *h.RequestContext) *h.Partial {
    	service := tasks.NewService(ctx.ServiceLocator())
    	list, _ := service.List()
    
    	tab := ctx.QueryParam("tab")
    
    	return h.SwapManyPartialWithHeaders(ctx,
    		h.PushQsHeader(ctx, h.NewQs("tab", tab)),
    		List(list, tab),
    		Footer(list, tab),
    	)
    }
    

    Example: h.SwapManyPartialWithHeaders to swap out multiple elements on the page with your response, as well as set a new query string parameter.


See #core-concepts for more information.

Getting Started

Prerequisites:
  1. Go: https://go.dev/doc/install
  2. Familiarity with https://htmx.org and html/hypermedia
    1. If you have not read the htmx docs, please do so before continuting, a lot of concepts below will be much more clear after.

1. Install htmgo
GOPROXY=direct go install github.com/maddalax/htmgo/cli/htmgo@latest

2. Create new project Once htmgo cli tool is installed, run

htmgo template

this will ask you for a new app name, and it will clone our starter template to a new directory it creates with your app name.


3. Running the dev server htmgo has built in live reload on the dev server, to use this, run this command in the root of your project

htmgo watch

If you prefer to restart the dev server yourself (no live reload), use

htmgo run
4. Core concepts

View the core concepts of how to use htmgo, such as adding pages, using partials, routing, etc.


5. Building for production htmgo cli can be used to build the application for production as a single binary

htmgo build

it will be output to ./dist


Pages

Pages are the entry point of an htmgo application.

A simple page may look like:

// route will be automatically registered based on the file name
func HelloHtmgoPage(ctx *h.RequestContext) *h.Page {
	return h.NewPage(
		h.Html(
			h.HxExtension(h.BaseExtensions()),
			h.Head(
				h.Link("/public/main.css", "stylesheet"),
				h.Script("/public/htmgo.js"),
			),
			h.Body(
				h.Pf("Hello, htmgo!"),
			),
		),
	)
}

htmgo uses std http with chi router as its web server, *h.RequestContext is a thin wrapper around *http.Request. A page must return *h.Page, and accept *h.RequestContext as a parameter


Auto Registration

htmgo uses file based routing. This means that we will automatically generate and register your routes with chi based on the files you have in the 'pages' directory.

For example, if you have a directory structure such as:

pages
  index.go
  users.go
  users.$id //id parameter can be accessed in your page with ctx.Param("id")

it will get registered into chi router as follows:

/
/users
/users/:id

You may put any functions you like in your pages file, auto registration will ONLY register functions that return *h.Page


Tips:

Generally it is it recommended to abstract common parts of your page into its own component and re-use it, such as script tags, including styling, etc.

Example:

func RootPage(children ...h.Ren) *h.Element {
	return h.Html(
		h.HxExtension(h.BaseExtensions()),
		h.Head(
			h.Meta("viewport", "width=device-width, initial-scale=1"),
			h.Link("/public/main.css", "stylesheet"),
			h.Script("/public/htmgo.js"),
			h.Style(`
				html {
					scroll-behavior: smooth;
				}
			`),
		),
		h.Body(
			h.Class("bg-stone-50 min-h-screen overflow-x-hidden"),
			partials.NavBar(false),
			h.Fragment(children...),
		),
	)
}
func UserPage(ctx *h.RequestContext) *h.Page {
	return h.NewPage(
		base.RootPage(
			h.Div(
				h.Pf("User ID: %s", ctx.Param("id")),
			),
		))
}

Partials

Partials are where things get interesting. Partials allow you to start adding interactivity to your website by swapping in content, setting headers, redirecting, etc.

Partials have a similiar structure to pages. A simple partial may look like:

func CurrentTimePartial(ctx *h.RequestContext) *h.Partial {
	now := time.Now()
	return h.NewPartial(
		h.Div(
			h.Pf("The current time is %s", now.Format(time.RFC3339)),
		),
	)
}

This will get automatically registered in the same way that pages are registered, based on the file path. This allows you to reference partials directly via the function itself when rendering them, instead of worrying about the route.

Example: I want to build a page that renders the current time, updating every second. Here is how that may look:


pages/time.go

package pages

func CurrentTimePage(ctx *h.RequestContext) *h.Page {
	return h.NewPage(
		base.RootPage(
			h.GetPartial(
				partials.CurrentTimePartial,
				"load, every 1s"),
		))
}

partials/time.go

package partials

func CurrentTimePartial(ctx *h.RequestContext) *h.Partial {
	now := time.Now()
	return h.NewPartial(
		h.Div(
			h.Pf("The current time is %s", now.Format(time.RFC3339)),
		),
	)
}

When the page load, the partial will be loaded in via htmx, and then swapped in every 1 second. With this little amount of code and zero written javascript, you have a page that shows the current time and updates every second.

Components

Components are re-usable bits of logic to render HTML. Similar to how in React components are Javascript functions, in htmgo, components are pure go functions.

A component can be pure, or it can have data fetching logic inside of it. Since htmgo uses htmx for interactivity, there is NO re-rendering of your UI automatically from the framework, which means you can safely put data fetching logic inside of components since you can be sure they will only be called by your own code.


Example:

func Card(ctx *h.RequestContext) *h.Element {
	service := tasks.NewService(ctx.ServiceLocator())
	list, _ := service.List()

	return h.Div(
		h.Id("task-card"),
		h.Class("bg-white w-full rounded shadow-md"),
		CardBody(list, getActiveTab(ctx)),
	)
}

My card component here fetches all my tasks I have on my list, and renders each task. If you are familar with React, then you would likely place this fetch logic inside of a useEffect or (useQuery library) so it is not constantly refetched as the component re-renders.

With htmgo, the only way to update content on the page is to use htmx to swap out the content from loading a partial. Therefore you control exactly when this Card component is called, not the framework behind the scenes.

See #interactivity-swapping for more information

HTML Tags

htmgo provides many methods to render html tags:

h.Html(children ...Ren) *Element
h.Head(children ...Ren) *Element
h.Div(children ...Ren) *Element
h.Button(children ...Ren) *Element
h.P(children ...Ren) *Element
h.H1(children ...Ren) *Element
h.H2(children ...Ren) *Element
h.Tag(tag string, children ...Ren) *Element
... etc

All methods can be found in the h package in htmgo/framework

See #conditionals for more information about conditionally rendering tags or attributes.

Attributes

Attributes are one of the main ways we can add interactivity to the pages with htmx. If you have not read over the htmx documentation, please do so before continuing.

htmgo provides many methods to add attributes

h.Class(string)
h.ClassX(string, h.ClassMap)
h.Href(string)
h.Attribute(key, value)
h.AttributeIf(condition, key, value)
h.AttributePairs(values...string) // set multiple attributes, must be an even number of parameters
h.Attributes(h.AttributeMap) // set multiple attributes as key/value pairs
h.Id(string)
h.Trigger(hx.Trigger) //htmx trigger using additional functions to construct the trigger
h.TriggerString(string) // htmx trigger in pure htmx string form

If / Else Statements

If / else statements are useful when you want to conditionally render attributes or elements / components.

htmgo provides a couple of utilities to do so:

h.If(condition, node)
h.Ternary(condition, node, node2)
h.ElementIf(condition, element) // this is neccessary if a method requires you to pass in *h.element
h.IfElse(condition, node, node2) //essentially an alias to h.Ternary
h.IfElseLazy(condition, func()node, func()node2) // useful for if something should only be called based on the condition
h.AttributeIf(condition, key string, value string) // adds an attribute if condition is true
h.ClassIf(condition, class string) // adds a class if condition is true
h.ClassX(classes, m.ClassMap{}) // allows you to include classes, but also render specific classes conditionally

Examples:

  • Render border-green-500 or border-slate-400 conditionally
h.ClassX("w-10 h-10 border rounded-full", map[string]bool {
				"border-green-500": task.CompletedAt != nil,
				"border-slate-400": task.CompletedAt == nil,
})
  • Render an icon if the task is complete
h.If(task.CompletedAt != nil, CompleteIcon())
  • Render different elements based on a condition
h.IfElse(editing, EditTaskForm(), ViewTask())

Note: This will execute both EditTaskForm and ViewTask, no matter if the condition is true or false, since a function is being called here.

If you do not want to call the function at all unless the condition is true, use h.IfElseLazy

h.IfElseLazy(editing, EditTaskForm, ViewTask)

Loops / Dealing With Lists

Very commonly you will need to render a list or slice of items onto the page. Frameworks generally solve this in different ways, such as React uses regular JS .map function to solve it.

We offer the same conveniences in htmgo.

h.List(items, func(item, index)) *h.Element
h.IterMap(map, mapper func(key, value) *Element) *Element 

Example:

  • Render a list of tasks
h.List(list, func(item *ent.Task, index int) *h.Element {
    if tab == TabComplete && item.CompletedAt == nil {
       return h.Empty()
    }
    return Task(item, false)
})
  • Render a map
  values := map[string]string{
  		"key": "value",
  	}
  
  	IterMap(values, func(key string, value string) *Element {
  		return Div(
  			Text(key),
  			Text(value),
  		)
  	})

Interactivity

  1. Adding interactivity to your website is done through htmx by utilizing various attributes/headers. This should cover most use cases. htmgo offers utility methods to make this process a bit easier

Here are a few methods we offer:

Partial Response methods

SwapManyPartialWithHeaders(ctx *RequestContext, headers *Headers, swaps ...*Element) *Partial
SwapPartial(ctx *RequestContext, swap *Element) *Partial
SwapManyPartial(ctx *RequestContext, swaps ...*Element) *Partial
SwapManyXPartial(ctx *RequestContext, swaps ...SwapArg) *Partial
GetPartialPath(partial PartialFunc) string
GetPartialPathWithQs(partial PartialFunc, qs *Qs) string

Swapping can also be done by adding a child to an element

OobSwapWithSelector(ctx *RequestContext, selector string, content *Element, option ...SwapOption) *Element
OobSwap(ctx *RequestContext, content *Element, option ...SwapOption) *Element
SwapMany(ctx *RequestContext, elements ...*Element)

Usage:

  1. I have a Card component that renders a list of tasks. I want to add a new button that completes all the tasks and updates the Card component with the completed tasks.

/components/task.go

func Card(ctx *h.RequestContext) *h.Element {
	service := tasks.NewService(ctx.ServiceLocator())
	list, _ := service.List()

	return h.Div(
		h.Id("task-card"),
		h.Class("bg-white w-full rounded shadow-md"),
		CardBody(list, getActiveTab(ctx)),
    CompleteAllButton(list)
	)
}
func CompleteAllButton(list []*ent.Task) *h.Element {
	notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool {
		return item.CompletedAt == nil
	}))

	return h.Button(
		h.TextF("Complete %s tasks", notCompletedCount),
		h.PostPartialWithQs(CompleteAll,
			h.NewQs("complete",
				h.Ternary(notCompletedCount > 0, "true", "false"),
			)),
	)
}

/partials/task.go

func CompleteAll(ctx *h.RequestContext) *h.Partial {
	service := tasks.NewService(ctx.ServiceLocator())
	service.SetAllCompleted(ctx.QueryParam("complete") == "true")
	return h.SwapPartial(ctx,
		Card(ctx),
	)
}

When the CompleteAll button is clicked, a POST will be sent to the CompleteAll partial, which will complete all the tasks and then swap out the Card content with the updated list of tasks. Pretty cool right?

SwapManyPartial can be used to swap out multiple items on the page instead of a single one.

Note: These partial swap methods use https://htmx.org/attributes/hx-swap-oob/ behind the scenes, so it must match the swap target by id.

If you are only wanting to swap the element that made the xhr request for the partial in the first place, just use h.NewPartial instead, it will use the default htmx swapping, and not hx-swap-oob.

Events

Sometimes you need to update elements client side without having to do a network call. For this you generally have to target an element with javascript and set an attribute, change the innerHTML, etc.

To make this work while still keeping a pure go feel, we offer a few utility methods to execute various javascript on an element.

Example: When the form is submitted, set the button text to submitting and disable it, and vise versa after submit is done.

func MyForm() *h.Element {
	return h.Form(
		h.Button(
			h.Text("Submit"),
			h.HxBeforeRequest(
				js.SetDisabled(true),
				js.SetText("Submitting..."),
			),
			h.HxAfterRequest(
				js.SetDisabled(false),
				js.SetText("Submit"),
			),
		),
	)
}

The structure of this comes down to:

  1. Add an event handler to the element
  2. Add commands (found in the js package) as children to that event handler

Event Handlers:

HxBeforeRequest(cmd ...Command) *LifeCycle
HxAfterRequest(cmd ...Command) *LifeCycle
HxOnMutationError(cmd ...Command) *LifeCycle
OnEvent(event hx.Event, cmd ...Command) *LifeCycle
OnClick(cmd ...Command) *LifeCycle
HxOnAfterSwap(cmd ...Command) *LifeCycle
HxOnLoad(cmd ...Command) *LifeCycle

If you use the OnEvent directly, event names may be any HTML DOM events, or any HTMX events.

Commands:

js.AddAttribute(string, value)
js.RemoveAttribute(string)
js.AddClass(string, value)
js.SetText(string)
js.Increment(count)
js.SetInnerHtml(Ren)
js.SetOuterHtml(Ren)
js.SetDisabled(bool)
js.RemoveClass(string)
js.Alert(string)
js.EvalJs(string) // eval arbitrary js, use 'self' to get the current element as a reference
js.InjectScript(string)
js.InjectScriptIfNotExist(string)
js.GetPartial(PartialFunc)
js.GetPartialWithQs(PartialFunc, Qs)
js.PostPartial(PartialFunc)
js.PostPartialWithQs(PartialFunc, Qs)
js.GetWithQs(string, Qs)
js.PostWithQs(string, Qs)
js.ToggleClass(string)
js.ToggleClassOnElement(string, string)

Example: Evaluating arbitrary JS

func MyButton() *h.Element {
	return h.Button(
		h.Text("Submit"),
		h.OnClick(
        // make sure you use 'self' instead of 'this' 
        // for referencing the current element
			h.EvalJs(`
				   if(Math.random() > 0.5) {
				       self.innerHTML = "Success!";
				   }
		       `,
			),
		),
	)
}

tip: If you are using Jetbrains IDE's, you can write // language=js as a comment above the function call (h.EvalJS) and it will automatically give you syntax highlighting on the raw JS.

// language=js
h.EvalJs(`
     if(Math.random() > 0.5) {
         self.innerHTML = "Success!";
     }
     `,
),

Caching Components

You may want to cache components to improve performance. This is especially useful for components that are expensive to render or make external requests for data.

To cache a component in htmgo, we offer:

// No arguments passed to the component
h.Cached(duration time.Duration, cb GetElementFunc)
// One argument passed to the component
h.CachedT(duration time.Duration, cb GetElementFunc)
// Two arguments passed to the component
h.CachedT2(duration time.Duration, cb GetElementFunc)
// Three arguments passed to the component
h.CachedT3(duration time.Duration, cb GetElementFunc)
// Four arguments passed to the component
h.CachedT4(duration time.Duration, cb GetElementFunc)

The duration parameter is the time the component should be cached for. The cb parameter is a function that returns the component.

When a request is made for a cached component, the component is rendered and stored in memory. Subsequent requests for the same component within the cache duration will return the cached component instead of rendering it again.

Usage:

func ExpensiveComponent(ctx *h.RequestContext) *h.Element { 
  // Some expensive call
  data := http.Get("https://api.example.com/data")	
  return h.Div(
    h.Text(data),
  )
}

var CachedComponent = h.CachedT(5*time.Minute, func(ctx *h.RequestContext) *h.Element {
  return ExpensiveComponent(ctx)
})

func IndexPage(ctx *h.RequestContext) *h.Page {
  return h.NewPage(
    CachedComponent(ctx),
  )
}

Real Example: I want to make a navbar that renders how many github stars my repository has. I don't want to make a request to the GitHub API everytime someone visits my page, so I will cache the component for 15 minutes.

var CachedGithubStars = h.CachedT(time.Minute*15, func(t *h.RequestContext) *h.Element {
    return GithubStars(t)
})

func GithubStars(ctx *h.RequestContext) *h.Element {
  stars := http.Get("https://api.github.com/repos/maddalax/htmgo/stargazers")
  return h.Div(
    h.Text(stars),
  )
}

Note: We are using CachedT because the component takes one argument, the RequestContext. If the component takes more arguments, use CachedT2, CachedT3, etc.

Important:

  1. The cached value is stored globally in memory, so it is shared across all requests. Do not store request-specific data in a cached component. Only cache components that you are OK with all users seeing the same data.
  2. The arguments passed into cached component DO NOT affect the cache key. You will get the same cached component regardless of the arguments passed in. This is different from what you may be used to from something like React useMemo.
  3. Ensure the declaration of the cached component is outside the function that uses it. This is to prevent the component from being redeclared on each request.

Troubleshooting:

command not found: htmgo ensure you installed htmgo above and ensure GOPATH is set in your shell