New Docs (#63)

* scripting enhancements

* tests

* cleanup / tests

* new docs wip

* add more docs

* more updates

* add caching docs

* add sse docs

* more docs

* sidebar, and fix navigation blocks

* remove old docs

* set proper meta

* fixes
This commit is contained in:
maddalax 2024-10-30 13:27:42 -05:00 committed by GitHub
parent df9c7f9cf7
commit 35877a1b2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1948 additions and 1268 deletions

View file

@ -196,6 +196,10 @@ func Hidden() Ren {
return Attribute("style", "display:none")
}
func Controls() Ren {
return Attribute("controls", "")
}
func Class(value ...string) *AttributeR {
return Attribute("class", MergeClasses(value...))
}

View file

@ -342,6 +342,10 @@ func Img(children ...Ren) *Element {
return Tag("img", children...)
}
func Video(children ...Ren) *Element {
return Tag("video", children...)
}
func Src(src string) *AttributeR {
return Attribute("src", src)
}

Binary file not shown.

View file

@ -0,0 +1,14 @@
{
"includeLanguages": {
"go": "html"
},
"experimental": {
"configFile": null,
"classRegex": [
["Class\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["ClassX\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["ClassIf\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["Classes\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"]
]
}
}

View file

@ -0,0 +1,11 @@
{
"tailwindCSS.includeLanguages": {
"go": "html"
},
"tailwindCSS.experimental.classRegex": [
["Class\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["ClassX\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["ClassIf\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["Classes\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`"]
]
}

View file

@ -11,4 +11,4 @@ watch_files: ["**/*.go", "**/*.css", "**/*.md"]
# files or directories to ignore when automatically registering routes for pages
# supports glob patterns through https://github.com/bmatcuk/doublestar
automatic_page_routing_ignore: ["root.go"]
automatic_page_routing_ignore: ["base/root.go", "docs2/base.go"]

View file

@ -0,0 +1,28 @@
package urlhelper
import (
"github.com/maddalax/htmgo/framework/h"
"net/url"
)
func ToAbsoluteUrl(ctx *h.RequestContext, path string) string {
// Define the relative path you want to add
relativePath := path
// Parse the current request URL
currentURL := ctx.Request.URL
// Set scheme and host from the request to create an absolute URL
scheme := "http"
if ctx.Request.TLS != nil {
scheme = "https"
}
currentURL.Host = ctx.Request.Host
currentURL.Scheme = scheme
// Combine the base URL with the relative path
absoluteURL := currentURL.ResolveReference(&url.URL{Path: relativePath})
// Output the full absolute URL
return absoluteURL.String()
}

View file

@ -1,64 +0,0 @@
## 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.
```go
func DocsPage(ctx *h.RequestContext) *h.Page {
pages := dirwalk.WalkPages("md/docs")
return h.NewPage(
h.Div(
h.Class("flex flex-col md:flex-row gap-4"),
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"),
MarkdownContent(ctx, page),
)
}),
),
),
)
}
```
**[The site you are reading now](https://github.com/maddalax/htmgo/tree/master/htmgo-site) was written with htmgo!**
<br>
**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 necessary to learn
5. Many composable utility functions to streamline development and reduce boilerplate
```go
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.
<br>
See [#core-concepts](#core-concepts-pages) for more information.

View file

@ -1,63 +0,0 @@
## 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 continuing, a lot of concepts below will be much more clear after.
<br>
##### 1. **Install htmgo**
```bash
GOPROXY=direct go install github.com/maddalax/htmgo/cli/htmgo@latest
```
**2. Create new project**
Once htmgo cli tool is installed, run
```bash
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.
<br>
**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
```bash
htmgo watch
```
If you prefer to restart the dev server yourself (no live reload), use
```bash
htmgo run
```
##### **4. Core concepts**
View the [core concepts](/docs#core-concepts-pages) of how to use htmgo, such as adding pages, using partials, routing, etc.
<br>
**5. Building for production**
htmgo cli can be used to build the application for production as a single binary
```bash
htmgo build
```
it will be output to **./dist**
<br>

View file

@ -1,6 +0,0 @@
## Other languages and related projects
If you're not a Go user but are interested in the idea of what htmgo is, you might want to check out these other projects:
#### Python
- [fastht.ml](https://fastht.ml/) - Modern web applications in pure Python, Built on solid web foundations, not the latest fads - with FastHTML you can get started on anything from simple dashboards to scalable web applications in minutes.

View file

@ -1,94 +0,0 @@
## Pages
Pages are the entry point of an htmgo application.
A simple page may look like:
```go
// 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](https://pkg.go.dev/net/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
<br>
**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:
```bash
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:
```bash
/
/users
/users/:id
```
You may put any functions you like in your pages file, auto registration will **ONLY** register functions that return ***h.Page**
<br>
**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:
```go
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...),
),
)
}
```
```go
func UserPage(ctx *h.RequestContext) *h.Page {
return h.NewPage(
base.RootPage(
h.Div(
h.Pf("User ID: %s", ctx.Param("id")),
),
))
}
```

View file

@ -1,58 +0,0 @@
## 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 similar structure to pages. A simple partial may look like:
```go
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:
<br>
**pages/time.go**
```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**
```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.

View file

@ -1,29 +0,0 @@
## 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.
<br>
**Example:**
```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)),
)
}
```
My card component here fetches all my tasks I have on my list, and renders each task.
If you are familiar 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](#interactivity-swapping) for more information

View file

@ -1,19 +0,0 @@
## HTML Tags
htmgo provides many methods to render html tags:
```go
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](#control-if-else) for more information about conditionally rendering tags or attributes.

View file

@ -1,22 +0,0 @@
## Attributes
Attributes are one of the main ways we can add interactivity to the pages with [htmx](http://htmx.org). If you have not read over the htmx documentation, please do so before continuing.
htmgo provides many methods to add attributes
```go
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
```

View file

@ -1,22 +0,0 @@
## Rendering Raw Html
In some cases, you may want to render raw HTML instead of using htmgo's functions.
This can be done by using the following methods:
```go
h.UnsafeRaw(string)
h.UnsafeRawF(string, ...interface{})
h.UnsafeRawScript(string)
```
Usage:
```go
h.UnsafeRaw("<div>Raw HTML</div>")
h.UnsafeRawF("<div>%s</div>", "Raw HTML")
h.UnsafeRawScript("alert('Hello World')")
```
Important: Be careful when using these methods, these methods do not escape the HTML content
and should **never** be used with user input unless you have sanitized the input.
Sanitizing input can be done using the `html.EscapeString` function or by using https://github.com/microcosm-cc/bluemonday.

View file

@ -1,51 +0,0 @@
## Conditional 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:
```go
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
```go
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
```go
h.If(task.CompletedAt != nil, CompleteIcon())
```
- Render different elements based on a condition
```go
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**
```go
h.IfElseLazy(editing, EditTaskForm, ViewTask)
```

View file

@ -1,38 +0,0 @@
## 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.
```go
h.List(items, func(item, index)) *h.Element
h.IterMap(map, mapper func(key, value) *Element) *Element
```
**Example:**
- Render a list of tasks
```go
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
```go
values := map[string]string{
"key": "value",
}
IterMap(values, func(key string, value string) *Element {
return Div(
Text(key),
Text(value),
)
})
```

View file

@ -1,85 +0,0 @@
## Interactivity / Swapping
1. Adding interactivity to your website is done through [htmx](http://htmx.org) 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
```go
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
```go
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**
```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)
)
}
```
```go
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**
```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.

View file

@ -1,45 +0,0 @@
## Events Handlers / Commands
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 vice versa after submit is done.
```go
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
<br>
**Event Handlers:**
```go
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
```

View file

@ -1,85 +0,0 @@
## Evaluating Javascript In Event Handlers
Event handlers are useful by attaching **commands** to elements to execute javascript on the client side.
See [#interactivity-events](#interactivity-events) for more information on event handlers.
<br>
**Note:** Each command you attach to the event handler will be passed 'self' and 'event' (if applicable) as arguments.
'self' is the current element, and 'event' is the event object.
If you use the OnEvent directly, event names may be any [HTML DOM](https://www.w3schools.com/jsref/dom_obj_event.asp) events, or any [HTMX events](https://htmx.org/events/).
Commands:
```go
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)
// The following methods are used to evaluate JS on nearby elements.
// Use 'element' to get the element as a reference for the EvalJs methods.
js.EvalJsOnParent(string)
js.EvalJsOnSibling(string, string)
js.EvalJsOnChildren(string, string)
js.SetClassOnParent(string)
js.RemoveClassOnParent(string)
js.SetClassOnChildren(string, string)
js.RemoveClassOnChildren(string, string)
js.SetClassOnSibling(string, string)
js.RemoveClassOnSibling(string, string)
```
For more usages: see https://htmgo.dev/examples/form
**Example:** Evaluating arbitrary JS
```go
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.
```go
// language=js
h.EvalJs(`
if(Math.random() > 0.5) {
self.innerHTML = "Success!";
}
`,
),
```

View file

@ -1,57 +0,0 @@
## Performance
### Caching Components Globally
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:
```go
// 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)
```
For caching components per user, see [Caching Components Per User](#performance-caching-per-user).
<br>
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:**
```go
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),
)
}
```
**Note:** We are using CachedT because the component takes one argument, the RequestContext.
If the component takes more arguments, use CachedT2, CachedT3, etc.
**Important Note When Using CachedT and NOT CachedPerKeyT:**
1. When using h.CachedT(T2, T3, etc) and not **CachedPerKey**, 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.

View file

@ -1,80 +0,0 @@
### Caching Components Per User
If you need to cache a component per user, you can use the `CachedPerKey` functions.
These functions allow you to cache a component by a specific key. This key can be any string that uniquely identifies the user.
Note: I'm using the term 'user' to simply mean a unique identifier. This could be a user ID, session ID, or any other unique identifier.
To cache a component by unique identifier / key in htmgo, we offer:
```go
// No arguments passed to the component, the component can be cached by a specific key
h.CachedPerKey(duration time.Duration, cb GetElementFuncWithKey)
// One argument passed to the component, the component can be cached by a specific key
h.CachedPerKeyT1(duration time.Duration, cb GetElementFuncWithKey)
// Two argument passed to the component, the component can be cached by a specific key
h.CachedPerKeyT2(duration time.Duration, cb GetElementFuncWithKey)
// Three arguments passed to the component, the component can be cached by a specific key
h.CachedPerKeyT3(duration time.Duration, cb GetElementFuncWithKey)
// Four arguments passed to the component, the component can be cached by a specific key
h.CachedPerKeyT4(duration time.Duration, cb GetElementFuncWithKey)
```
The `duration` parameter is the time the component should be cached for. The `cb` parameter is a function that returns the component and the key.
When a request is made for a cached component, the component is rendered and stored in memory. Subsequent requests for the same component with the same key within the cache duration will return the cached component instead of rendering it again.
**Usage:**
```go
var CachedUserDocuments = h.CachedPerKeyT(time.Minute*15, func(ctx *h.RequestContext) (string, h.GetElementFunc) {
userId := getUserIdFromSession(ctx)
return userId, func() *h.Element {
return UserDocuments(ctx)
}
})
func UserDocuments(ctx *h.RequestContext) *h.Element {
docService := NewDocumentService(ctx)
// Expensive call
docs := docService.getDocuments()
return h.Div(
h.Class("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"),
h.List(docs, func(doc Document, index int) *h.Element {
return h.Div(
h.Class("p-4 bg-white border border-gray-200 rounded-md"),
h.H3(doc.Title),
h.P(doc.Description),
)
}),
)
}
func MyPage(ctx *h.RequestContext) *h.Page {
// Note this is not a real way to create a context, just an example
user1 := &h.RequestContext{
Session: "user_1_session",
}
user2 := &h.RequestContext{
Session: "user_2_session",
}
// Different users will get different cached components
return h.NewPage(
CachedUserDocuments(user1),
CachedUserDocuments(user2),
)
}
```
**Note:** We are using CachedPerKeyT because the component takes one argument, the RequestContext.
If the component takes more arguments, use CachedPerKeyT2, CachedPerKeyT3, etc.
**Important**
1. The cached value is stored globally in memory by key, it is shared across all requests. Ensure if you are storing request-specific data in a cached component, you are using a unique key for each user.
2. The arguments passed into cached component **DO NOT** affect the cache key. The only thing that affects the cache key is the key returned by the `GetElementFuncWithKey` function.
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.

View file

@ -1,64 +0,0 @@
## Server Sent Events (SSE)
htmgo supports server-sent events (SSE) out of the box.
This allows you to push data from the server to the client in real-time.
Example of this can be found in the [chat-app](https://htmgo.dev/examples/chat) example.
## How it works ##
1. The client sends a request to the server to establish a connection.
2. The server holds the connection open and sends data (in our case, most likely elements) to the client whenever there is new data to send.
3. The htmgo SSE extension uses https://htmx.org/attributes/hx-swap-oob/ to swap out the elements that the server sends.
**Note**: SSE is **unidirectional** (the server can only send data to the client).
For the client to send data to the server, normal xhr behavior should be used (form submission, triggers, etc).
## Usage
1. Add the SSE connection attribute and the path to the handler that will handle the connection.
```go
h.Attribute("sse-connect", fmt.Sprintf("/chat/%s", roomId))
```
The following **Event Handlers** can be used to react to SSE connections.
```go
h.HxOnSseOpen
h.HxBeforeSseMessage
h.HxAfterSseMessage
h.HxOnSseError
h.HxOnSseClose
h.HxOnSseConnecting
```
**Example:** Adding an event listener handle SSE errors.
```go
h.HxOnSseError(
js.EvalJs(fmt.Sprintf(`
const reason = e.detail.event.data
if(['invalid room', 'no session', 'invalid user'].includes(reason)) {
window.location.href = '/?roomId=%s';
} else if(e.detail.event.code === 1011) {
window.location.reload()
} else if (e.detail.event.code === 1008 || e.detail.event.code === 1006) {
window.location.href = '/?roomId=%s';
} else {
console.error('Connection closed:', e.detail.event)
}
`, roomId, roomId)),
),
```
**Example:** Clearing the input field after sending a message.
```go
func MessageInput() *h.Element {
return h.Input("text",
h.Id("message-input"),
h.Required(),
h.HxAfterSseMessage(
js.SetValue(""),
),
)
}
```

View file

@ -1,34 +0,0 @@
## HTMX Extensions
htmgo provides a few extra htmx extensions to make common tasks easier.
Some of these extensions are optional, and some of these are required for htmgo to work correctly.
The following extensions are provided by htmgo:
- [Trigger Children](#htmx-extensions-trigger-children)
- [Mutation Error](#htmx-extensions-mutation-error)
- [SSE](#pushing-data-server-sent-events)
- [Path Deps](https://github.com/bigskysoftware/htmx-extensions/blob/main/src/path-deps/README.md)
Default extensions should be included in your project by adding the following attribute to your html tag.
```go
h.Html(
h.HxExtension(h.BaseExtensions())
)
```
If you need to combine multiple extensions, you can use:
```go
h.HxExtensions(h.BaseExtensions(), "my-extension"),
```
or
```go
h.JoinExtensions(
h.HxExtension("sse"),
h.HxExtension("my-extension"),
),
```
**Important**: h.BaseExtensions will add the the 'htmgo' extension, which is a required extension for inline scripts to work properly, please always include it in your project.

View file

@ -1,13 +0,0 @@
## HTMX Extensions - Trigger Children
The `trigger-children` extension allows you to trigger an event on all children and siblings of an element.
This is useful for things such as:
1. Letting a child element (such as a button) inside a form know the form was submitted
<br>
**Example:** https://htmgo.dev/examples/form
In this example: The trigger-children extension will trigger **hx-before-request** and **hx-after-request**
on all children of the form when the form is submitted, and the button reacts to that by showing a loading state.

View file

@ -1,24 +0,0 @@
## HTMX Extensions - Mutation Error
The `mutation-error` extension allows you to trigger an event when a request returns a >= 400 status code.
This is useful for things such as:
1. Letting a child element (such as a button) inside a form know there was an error.
<br>
**Example:**
```go
h.Form(
h.HxTriggerChildren(),
h.HxMutationError(
js.Alert("An error occurred"),
),
h.Button(
h.Type("submit"),
h.Text("Submit"),
),
)
```
It can also be used on children elements that do not make an xhr request, if you combine it with the `hx-trigger-children` extension.

View file

@ -1,53 +0,0 @@
## Tailwind intellisense
Tailwind's language server allows you to specify custom configuration on what it should match to start giving you tailwind intellisense.
![](/public/tailwind-intellisense.png)
To make this work, you will need to update the tailwind lsp config with the config below:
Main thing to note here is
1. "go" is added to the includeLanguages list
2. classRegex is updated to match the tailwind classes in the go code.
### Jetbrains IDE's (GoLand)
```json
{
"includeLanguages": {
"go": "html"
},
"experimental": {
"configFile": null,
"classRegex": [
["Class\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["ClassX\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["ClassIf\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["Classes\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"]
]
}
}
```
To find this configuration in GoLand you can go to `Settings -> Languages & Frameworks -> Style Sheets -> Tailwind CSS` and update the configuration there.
These changes are additive, add these options to your existing tailwind lsp config, instead of replacing the entire file.
See more: https://github.com/tailwindlabs/tailwindcss/issues/7553#issuecomment-735915659
<br>
### Visual Studio Code
For VSCode, you should be able to update your settings.json with the following values:
```json
{
"tailwindCSS.includeLanguages": {
"go": "html"
},
"tailwindCSS.experimental.classRegex": [
["Class\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["ClassX\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["ClassIf\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
["Classes\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`"]
]
}
```

View file

@ -1,4 +0,0 @@
## Converting Raw HTML to Go
In some cases, you may want to convert raw HTML to Go code.
A tool to do this automatically is available here: https://htmgo.dev/html-to-go

View file

@ -1,60 +0,0 @@
## Htmgo Format
htmgo has a built-in formatter that can be used to format htmgo element blocks.
It is available through the 'htmgo' cli tool that is installed with htmgo.
**Note:** if you have previously installed htmgo, you will need to run `GOPROXY=direct go install github.com/maddalax/htmgo/cli/htmgo@latest` to update the cli tool.
<br>
To use it, run the following command:
```bash
// format all .go files in the current directory recursively
htmgo format .
// format the file specified
htmgo format ./my-file.go
```
This will format all htmgo element blocks in your project.
**Example:**
```go
h.Div(
h.Class("flex gap-2"), h.Text("hello"), h.Text("world"),
)
```
**Output:**
```go
h.Div(
h.Class("flex gap-2"),
h.Text("hello"),
h.Text("world"),
)
```
## Running htmgo format on save
### Jetbrains IDE's
1. Go to Settings -> Tools -> File Watchers -> + custom
2. Set the following values:
```yaml
Name: htmgo format
File Type: Go
Scope: Current File
Program: htmgo
Arguments: format $FilePath$
Output paths to refresh: $FilePath$
Working directory: $ProjectFileDir$
```
3. Save the file watcher and ensure it is enabled
4. Go to `Settings -> Tools -> Actions On Save` and ensure the `htmgo format` action is enabled

View file

@ -1,4 +0,0 @@
## Troubleshooting:
**command not found: htmgo**
ensure you installed htmgo above and ensure GOPATH is set in your shell

View file

@ -1,4 +1,4 @@
package pages
package base
import (
"github.com/google/uuid"
@ -8,9 +8,30 @@ import (
var Version = uuid.NewString()[0:6]
func RootPage(ctx *h.RequestContext, children ...h.Ren) *h.Page {
type RootPageProps struct {
Title string
Description string
Canonical string
Children h.Ren
}
func ConfigurableRootPage(ctx *h.RequestContext, props RootPageProps) *h.Page {
title := "htmgo"
description := "build simple and scalable systems with go + htmx"
canonical := ctx.Request.URL.String()
if props.Canonical != "" {
canonical = props.Canonical
}
if props.Title != "" {
title = props.Title
}
if props.Description != "" {
description = props.Description
}
return h.NewPage(
h.Html(
h.HxExtension(
@ -25,8 +46,8 @@ func RootPage(ctx *h.RequestContext, children ...h.Ren) *h.Page {
h.Meta("author", "htmgo"),
h.Meta("description", description),
h.Meta("og:title", title),
h.Meta("og:url", "https://htmgo.dev"),
h.Link("canonical", "https://htmgo.dev"),
h.Meta("og:url", ctx.Request.URL.String()),
h.Link("canonical", canonical),
h.Link("https://cdn.jsdelivr.net/npm/@docsearch/css@3", "stylesheet"),
h.Meta("og:description", description),
h.LinkWithVersion("/public/main.css", "stylesheet", Version),
@ -39,7 +60,7 @@ func RootPage(ctx *h.RequestContext, children ...h.Ren) *h.Page {
),
h.Body(
h.Class("bg-white h-screen"),
h.Fragment(children...),
props.Children,
h.Script("https://cdn.jsdelivr.net/npm/@docsearch/js@3"),
h.UnsafeRawScript(`
docsearch({
@ -56,6 +77,17 @@ func RootPage(ctx *h.RequestContext, children ...h.Ren) *h.Page {
)
}
func RootPage(ctx *h.RequestContext, children ...h.Ren) *h.Page {
return ConfigurableRootPage(
ctx,
RootPageProps{
Title: "htmgo",
Description: "build simple and scalable systems with go + htmx",
Children: h.Fragment(children...),
},
)
}
func PageWithNav(ctx *h.RequestContext, children ...h.Ren) *h.Page {
return RootPage(
ctx,

View file

@ -1,66 +0,0 @@
package pages
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/internal/dirwalk"
"htmgo-site/partials"
"io/fs"
)
func DocsPage(ctx *h.RequestContext) *h.Page {
assets := ctx.Get("embeddedMarkdown").(fs.FS)
pages := dirwalk.WalkPages("md/docs", assets)
return RootPage(
ctx,
h.Div(
h.Class("flex h-full"),
h.Aside(
h.Class("hidden md:block md:min-w-60 text-white overflow-y-auto"),
partials.DocSidebar(pages),
),
h.Div(
h.Class("flex flex-col flex-1 overflow-hidden"),
partials.NavBar(ctx, partials.NavBarProps{
Expanded: false,
ShowPreRelease: false,
}),
h.Main(
h.Div(
h.Class("w-full md:hidden bg-neutral-50 overflow-y-auto mb-4 border-b border-b-slate-300"),
partials.DocSidebar(pages),
),
h.Class("overflow-y-auto justify-center overflow-x-hidden pb-6 items-center w-full"),
h.Div(
h.Class("flex flex-col mx-auto"),
h.Div(
h.Class("flex flex-col justify-center items-center md:mt-6 mx-auto"),
h.List(pages, func(page *dirwalk.Page, index int) *h.Element {
anchor := partials.CreateAnchor(page.Parts)
return h.Div(
h.Class("border-b border-b-slate-300 w-full pb-8 p-4 md:px-0 -mb-2"),
MarkdownContent(ctx, page.FilePath, anchor),
h.Div(
h.Class("ml-4 pl-1 mt-2 bg-rose-200"),
h.If(
anchor == "core-concepts-partials",
h.GetPartial(partials.CurrentTimePartial, "load, every 1s"),
),
),
)
}),
),
h.Div(
h.Class("flex justify-center items-center mt-6"),
h.A(
h.Text("Back to Top"),
h.Class("py-2 px-3 bg-slate-800 rounded text-white"),
h.Href("#quick-start-introduction"),
),
),
),
),
),
),
)
}

View file

@ -0,0 +1,165 @@
package docs
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/pages/base"
"htmgo-site/partials"
"strings"
)
func Title(title string) *h.Element {
return h.H1(
h.Text(title),
h.Class("text-2xl font-bold"),
)
}
func SubTitle(title string) *h.Element {
return h.H2(
h.Text(title),
h.Class("text-xl font-bold"),
)
}
func StepTitle(title string) *h.Element {
return h.H2(
h.Text(title),
h.Class("text-lg font-bold"),
)
}
func NextStep(classes string, prev *h.Element, next *h.Element) *h.Element {
return h.Div(
h.Class("flex gap-2 justify-between", classes),
prev,
next,
)
}
func NextBlock(text string, url string) *h.Element {
return h.A(
h.Href(url),
h.Class("w-[50%] border border-slate-300 p-4 rounded text-right hover:border-blue-400 cursor-pointer"),
h.P(
h.Text("Next"),
h.Class("text-slate-600 text-sm"),
),
h.P(
h.Text(text),
h.Class("text-blue-500 hover:text-blue-400"),
),
)
}
func PrevBlock(text string, url string) *h.Element {
return h.A(
h.Href(url),
h.Class("w-[50%] border border-slate-300 p-4 rounded text-left hover:border-blue-400 cursor-pointer"),
h.P(
h.Text("Previous"),
h.Class("text-slate-600 text-sm"),
),
h.P(
h.Text(text),
h.Class("text-blue-500 hover:text-blue-400"),
),
)
}
func Image(src string) *h.Element {
return h.Img(
h.Src(src),
h.Class("rounded w-full"),
)
}
func Text(text string) *h.Element {
split := strings.Split(text, "\n")
return h.Div(
h.Class("flex flex-col gap-2 leading-relaxed text-slate-900 break-words"),
h.List(split, func(item string, index int) *h.Element {
return h.P(
h.UnsafeRaw(item),
)
}),
)
}
func Inline(elements ...h.Ren) *h.Element {
return h.Div(
h.Class("flex gap-1 items-center"),
h.Children(elements...),
)
}
func HelpText(text string) *h.Element {
return h.Div(
h.Class("text-slate-600 text-sm"),
h.UnsafeRaw(text),
)
}
func Link(text string, href string, additionalClasses ...string) *h.Element {
additionalClasses = append(additionalClasses, "text-blue-500 hover:text-blue-400")
return h.A(
h.Href(href),
h.Text(text),
h.Class(
additionalClasses...,
),
)
}
func DocPage(ctx *h.RequestContext, children ...h.Ren) *h.Page {
title := "htmgo"
for _, section := range sections {
for _, page := range section.Pages {
if page.Path == ctx.Request.URL.Path {
title = page.Title
break
}
}
}
return base.ConfigurableRootPage(
ctx,
base.RootPageProps{
Title: title,
Description: "build simple and scalable systems with go + htmx",
Children: h.Div(
h.Class("flex h-full"),
h.Aside(
h.Class("hidden md:block md:min-w-60 text-white overflow-y-auto"),
DocSidebar(),
),
h.Div(
h.Class("flex flex-col flex-1 overflow-hidden"),
partials.NavBar(ctx, partials.NavBarProps{
Expanded: false,
ShowPreRelease: false,
}),
h.Main(
h.Div(
h.Class("w-full md:hidden bg-neutral-50 overflow-y-auto mb-4 border-b border-b-slate-300"),
DocSidebar(),
),
h.Class("overflow-y-auto overflow-x-hidden pb-6 items-center w-full"),
h.Div(
h.Class("flex flex-col mx-auto"),
h.Div(
h.Class("flex flex-col justify-center items-center md:mt-6 mx-auto"),
h.Div(
h.Class(
"w-full flex flex-col max-w-[90vw] md:max-w-[65vw] xl:max-w-4xl",
),
h.Children(children...),
),
),
),
),
),
),
},
)
}

View file

@ -1,10 +1,36 @@
## Htmgo Configuration:
package config
Certain aspects of htmgo can be configured via a `htmgo.yml` file in the root of your project.
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
Here is an example configuration file:
func HtmgoConfig(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Htmgo Config"),
Text(`
Certain aspects of htmgo can be configured via a htmgo.yml file in the root of your project.
Here is an example configuration file:
`),
ui.CodeSnippet(ui.CodeSnippetProps{
Code: htmgoConfig,
Lang: "yaml",
HideLineNumbers: true,
}),
NextStep(
"mt-4",
PrevBlock("Formatter", DocPath("/misc/formatter")),
NextBlock("Examples", "/examples"),
),
),
)
}
```yaml
const htmgoConfig = `
# htmgo configuration
# if tailwindcss is enabled, htmgo will automatically compile your tailwind and output it to assets/dist
@ -23,4 +49,4 @@ automatic_page_routing_ignore: ["root.go"]
# files or directories to ignore when automatically registering routes for partials
# supports glob patterns through https://github.com/bmatcuk/doublestar
automatic_partial_routing_ignore: []
```
`

View file

@ -0,0 +1,73 @@
package control
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func IfElse(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("If / Else Statements"),
Text(`
If / else statements are useful when you want to conditionally render attributes or elements / components.
htmgo provides a couple of utilities to do so:
`),
Text("Example: Rendering an icon if the task is complete"),
ui.GoCodeSnippet(IfElseExample),
Text("Example: Using ternary operator to call different partials based on a condition"),
ui.GoCodeSnippet(TenaryExample),
Text(`Example: Rendering multiple classes based on a condition`),
ui.GoCodeSnippet(ConditionalClassExample),
Text("Example: Rendering a single class based on a condition"),
ui.GoCodeSnippet(ClassIfElseExample),
Text("Example: Rendering different elements based on a condition"),
ui.GoCodeSnippetSingleLine(IfElseExample2),
Text(`
<b>Note:</b> This will execute both <b>EditTaskForm</b> and <b>ViewTask</b>, 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 <b>h.IfElseLazy</b>
`),
ui.GoCodeSnippetSingleLine(IfElseExample3),
NextStep(
"mt-4",
PrevBlock("Raw HTML", DocPath("/core-concepts/raw-html")),
NextBlock("Rendering Lists", DocPath("/control/loops")),
),
),
)
}
const IfElseExample = `
h.Div(
h.If(
task.CompletedAt != nil,
CompleteIcon()
)
)
`
const TenaryExample = `h.Div(
h.PostPartialWithQs(
h.Ternary(!editing, StartEditing, SaveEditing),
h.NewQs("id", record.Id),
),
)
`
const ConditionalClassExample = `h.ClassX("w-10 h-10 border rounded-full", map[string]bool {
"border-green-500": task.CompletedAt != nil,
"border-slate-400": task.CompletedAt == nil,
})`
const IfElseExample2 = `h.IfElse(editing, EditTaskForm(), ViewTask())`
const IfElseExample3 = `h.IfElseLazy(editing, EditTaskForm, ViewTask)`
const ClassIfElseExample = `
h.Div(
h.ClassIf(task.CompletedAt != nil, "border-green-500"),
)
`

View file

@ -0,0 +1,55 @@
package control
import . "htmgo-site/pages/docs"
import "htmgo-site/ui"
import "github.com/maddalax/htmgo/framework/h"
func Loops(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Loops / Dealing With Lists"),
Text(`
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.
htmgo provides a couple of utilities to do so:
`),
Text("Example: Rendering a list of tasks"),
ui.GoCodeSnippet(ListExample),
Text("Example: Rendering a map"),
ui.GoCodeSnippet(MapExample),
NextStep(
"mt-4",
PrevBlock("Conditionals", DocPath("/control/if-else")),
NextBlock("Adding Interactivity", DocPath("/interactivity/swapping")),
),
),
)
}
const ListExample = `
var items = []string{"item1", "item2", "item3"}
h.List(items, func(item string, index int) *h.Element {
if tab == TabComplete && item.CompletedAt == nil {
return h.Empty()
}
return h.Div(
h.Text(item),
)
})
`
const MapExample = `
var values = map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "value3",
}
h.IterMap(values, func(key string, value string) *h.Element {
return h.Div(
h.Text(key),
h.Text(value),
)
})
`

View file

@ -0,0 +1,41 @@
package core_concepts
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func Components(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Components"),
Text(`
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.
`),
ComponentExample(),
NextStep(
"mt-4",
PrevBlock("Partials", DocPath("/core-concepts/partials")),
NextBlock("Tags and Attributes", DocPath("/core-concepts/tags-and-attributes")),
),
),
)
}
func ComponentExample() *h.Element {
return h.Div(
Text("Example:"),
ui.GoCodeSnippet(PagesSnippet),
Text(`
My card component here fetches all my tasks I have on my list, and renders each task.
If you are familiar with React, then you would likely place this fetch logic inside of a useEffect or (useQuery library) so it is not constantly re-fetched 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.
You'll learn more about swapping in the next few pages.
`),
)
}

View file

@ -0,0 +1,113 @@
package core_concepts
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
var ExcludeRootSnippet = `automatic_page_routing_ignore: ["pages/root.go"]`
var AbstractedRootPageUsageSnippet = `func UserPage(ctx *h.RequestContext) *h.Page {
return base.RootPage(
h.Div(
h.Pf("User ID: %s", ctx.Param("id")),
),
}`
var RootPageSnippet = `func RootPage(children ...h.Ren) *h.Page {
return h.NewPage(
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.Body(
h.Class("bg-stone-50 min-h-screen overflow-x-hidden"),
ui.NavBar(),
h.Fragment(children...),
),
)
)
}
`
var PagesSnippet = `// 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!"),
),
),
)
}`
func Pages(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Pages"),
Text(`
Pages are the entry point of an htmgo application.
A simple page may look like:
`),
ui.GoCodeSnippet(PagesSnippet),
h.Text(`
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
`),
autoRegistration(),
tips(),
NextStep(
"mt-4",
PrevBlock("Getting Started", DocPath("/installation")),
NextBlock("Partials", DocPath("/core-concepts/partials")),
),
),
)
}
func autoRegistration() *h.Element {
return h.Div(
h.Class("flex flex-col gap-2"),
SubTitle("Auto Registration"),
Text(`
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 like so below, it will get registered into chi router as follows:
index.go -> /index
users.go -> /users
users.$id.go -> /users/:id
`),
HelpText(`Note: id parameter can be accessed in your page with ctx.Param("id")`),
Text(`
You may put any functions you like in your pages file, auto registration will ONLY register functions that return *h.Page
`),
)
}
func tips() *h.Element {
return h.Div(
h.Class("flex flex-col gap-2"),
SubTitle("Tips:"),
Text(`
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:
`),
ui.GoCodeSnippet(RootPageSnippet),
Text("Usage:"),
ui.GoCodeSnippet(AbstractedRootPageUsageSnippet),
Text("You need to then update <strong>htmgo.yml</strong> to exclude that file from auto registration"),
ui.SingleLineBashCodeSnippet(ExcludeRootSnippet),
HelpText("In this example, my root page is in a file called root.go in the pages dir, so I need to exclude it from auto registration, otherwise htmgo wil try to generate a route for it."),
)
}

View file

@ -0,0 +1,84 @@
package core_concepts
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/partials"
"htmgo-site/ui"
)
var PartialsSnippet = `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)),
),
)
}`
var examplePageSnippet = `func CurrentTimePage(ctx *h.RequestContext) *h.Page {
return RootPage(
h.GetPartial(partials.CurrentTimePartial, "load, every 1s")
)
}`
var examplePartialSnippet = `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)),
),
)
}`
func Partials(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Partials"),
Text(`
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 similar structure to pages. A simple partial may look like:
`),
ui.GoCodeSnippet(PartialsSnippet),
h.Text(`
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(),
NextStep(
"mt-4",
PrevBlock("Pages", DocPath("/core-concepts/pages")),
NextBlock("Components", DocPath("/core-concepts/components")),
),
),
)
}
func example() *h.Element {
return h.Div(
h.Class("flex flex-col gap-2"),
SubTitle("Simple Example"),
Text(`
I want to build a page that renders the current time, updating every second. Here is how that may look:
`),
h.Pf(
"pages/time.go",
h.Class("font-semibold"),
),
ui.GoCodeSnippet(examplePageSnippet),
h.Pf(
"partials/time.go",
h.Class("font-semibold"),
),
ui.GoCodeSnippet(examplePartialSnippet),
Text(
`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.`),
h.Div(
h.GetPartial(partials.CurrentTimePartial, "load, every 1s"),
),
)
}

View file

@ -0,0 +1,45 @@
package core_concepts
import "htmgo-site/ui"
import "github.com/maddalax/htmgo/framework/h"
import . "htmgo-site/pages/docs"
func RawHtml(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Raw HTML"),
Text(`
In some cases, you may want to render raw html instead of using htmgo's functions.
This can be done by using the following methods:
`),
Text("Rendering raw html:"),
ui.GoCodeSnippetSingleLine(RawHtmlExample),
Text("Rendering with formatting:"),
ui.GoCodeSnippetSingleLine(RawHtmlExample2),
Text("Rendering a script:"),
ui.GoCodeSnippetSingleLine(RawHtmlExample3),
Text(`
Important: Be careful when using these methods, these methods do not escape the HTML content
and should never be used with user input unless you have sanitized the input.
`),
h.P(
h.Text("Sanitizing input can be done using "),
Link("https://pkg.go.dev/html#EscapeString", "html.EscapeString"),
h.Text(" or by using "),
Link("bluemonday", "https://github.com/microcosm-cc/bluemonday."),
h.Text(" for more control over sanitization."),
),
NextStep(
"mt-4",
PrevBlock("Tags and Attributes", DocPath("/core-concepts/tags-and-attributes")),
NextBlock("Conditionals", DocPath("/control/if-else")),
),
),
)
}
const RawHtmlExample = `h.UnsafeRaw("<div>Raw HTML</div>")`
const RawHtmlExample2 = `h.UnsafeRawF("<div>%s</div>", "Raw HTML")`
const RawHtmlExample3 = `h.UnsafeRawScript("alert('Hello World')")`

View file

@ -0,0 +1,84 @@
package core_concepts
import "htmgo-site/ui"
import "github.com/maddalax/htmgo/framework/h"
import . "htmgo-site/pages/docs"
func TagsAndAttributes(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Tags and Attributes"),
Text(`
In htmgo, html is built using a set of functions that return <b>*h.Element</b>.
These functions are all defined in the <b>'h'</b> package in htmgo/framework
htmgo provides methods to render most if not all html tags and attributes.
`),
Text(`Example:`),
ui.GoCodeSnippet(TagExample),
Text(`
All methods can be found in the 'h' package in htmgo/framework
`),
Text("<b>h.Tag</b> and <b>h.Attribute</b> are available to use when you need to render a tag or attribute that htmgo does not provide a method for."),
ui.GoCodeSnippet(TagExampleUsingTagFunc),
Text(`
<b>Attributes</b> are one of the main ways we can add interactivity to the pages with htmx.
htmgo provides various methods to add attributes to elements, as well as adding attributes based on a condition.
`),
ui.GoCodeSnippet(AttributeExample),
HelpText("In this example we are conditionally adding an attribute based on if there is an error on not, you'll learn more about conditionals in the next few pages."),
Text("Example using htmx attributes:"),
ui.GoCodeSnippet(HxAttributeExample),
NextStep(
"mt-4",
PrevBlock("Components", DocPath("/core-concepts/components")),
NextBlock("Raw HTML", DocPath("/core-concepts/raw-html")),
),
),
)
}
const TagExample = `h.Div(
h.Class("flex gap-2"),
h.Button(
h.Text("Submit"),
),
)
`
const TagExampleUsingTagFunc = `h.Tag("my-custom-tag",
h.Class("flex gap-2"),
h.Button(
h.Attribute("x-custom-attr", "my-value"),
h.Text("Submit"),
),
)
`
const AttributeExample = `h.Div(
h.Class("flex gap-2"),
h.Id("my-div"),
h.If(
error != "",
h.Class("p-4 bg-rose-400 text-white rounded"),
)
)
`
const HxAttributeExample = `h.Tr(
h.Class("flex gap-2"),
h.HxInclude("input")
h.Td(
h.Input("text",
h.Class("p-4 rounded"),
h.Placeholder("Type something"),
h.Name("my-input"),
)
),
h.Td(
h.Button(
h.Text("Submit"),
)
),
)`

View file

@ -0,0 +1,42 @@
package htmx_extensions
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func MutationError(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Mutation Error"),
Text(`
The 'mutation-error' extension allows you to trigger an event when a request returns a >= 400 status code.
This is useful for things such as letting a child element (such as a button) inside a form know there was an error.
`),
Text(`<b>Example:</b>`),
ui.GoCodeSnippet(MutationErrorExample),
Text(`It can also be used on children elements that do not make an xhr request, if you combine it with the TriggerChildren extension.`),
NextStep(
"mt-4",
PrevBlock("Trigger Children", DocPath("/htmx-extensions/trigger-children")),
NextBlock("Tailwind Intellisense", DocPath("/misc/tailwind-intellisense")),
),
),
)
}
const MutationErrorExample = `
h.Form(
h.HxTriggerChildren(),
h.HxMutationError(
js.Alert("An error occurred"),
),
h.Button(
h.Type("submit"),
h.Text("Submit"),
),
)
`

View file

@ -0,0 +1,80 @@
package htmx_extensions
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func Overview(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("HTMX Extensions"),
Text(`
htmgo provides a few extra htmx extensions to make common tasks easier.
Some of these extensions are optional, and some of these are required for htmgo to work correctly.
`),
Text(`
The following extensions are provided by htmgo:
`),
Link("Trigger Children", "/docs/htmx-extensions/trigger-children"),
Link("Mutation Error", "/docs/htmx-extensions/mutation-error"),
Link("Path Deps", "https://github.com/bigskysoftware/htmx-extensions/blob/main/src/path-deps/README.md"),
h.P(
h.Class("mt-3"),
h.Text("Default extensions should be included in your project by adding the following attribute to your html tag."),
ui.GoCodeSnippet(DefaultExtensions),
h.Text("If you need to combine multiple extensions, you can use:"),
ui.GoCodeSnippet(CombineMultipleExtensions),
h.Text("or"),
ui.GoCodeSnippet(CombineMultipleExtensions2),
),
Text(`
<b>Important:</b> h.BaseExtensions will add the 'htmgo' extension, which is a required extension for inline scripts to work properly, please always include it in your project.
`),
NextStep(
"mt-4",
PrevBlock("Pushing Data", DocPath("/pushing-data/sse")),
NextBlock("Trigger Children", DocPath("/htmx-extensions/trigger-children")),
),
),
)
}
const DefaultExtensions = `
h.Html(
h.HxExtension(h.BaseExtensions())
)
`
const CombineMultipleExtensions = `
h.HxExtensions(
h.BaseExtensions(), "my-extension"
)
`
const CombineMultipleExtensions2 = `
h.JoinExtensions(
h.HxExtension("sse"),
h.HxExtension("my-extension"),
)
`
const htmxExtensions = `
h.HxOnLoad
h.HxOnAfterSwap
h.OnClick
h.OnSubmit
h.HxBeforeSseMessage
h.HxAfterSseMessage
h.OnClick
h.OnSubmit
h.HxOnSseError
h.HxOnSseClose
h.HxOnSseConnecting
h.HxOnSseOpen
h.HxAfterRequest
h.HxOnMutationError
`

View file

@ -0,0 +1,37 @@
package htmx_extensions
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
)
func TriggerChildren(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Trigger Children"),
Text(`
The 'trigger-children' extension allows you to trigger an event on all children and siblings of an element.
This is useful for things such as letting a child element (such as a button) inside a form know the form was submitted
`),
Link("View Example", "https://htmgo.dev/examples/form"),
HelpText(`In this example: The trigger-children extension will trigger hx-before-request and hx-after-request on all children of the form when the form is submitted, and the button reacts to that by showing a loading state.`),
NextStep(
"mt-4",
PrevBlock("HTMX Extensions", DocPath("/htmx-extensions/overview")),
NextBlock("Mutation Error", DocPath("/htmx-extensions/mutation-error")),
),
),
)
}
const TriggerChildrenExample = `
func MyForm() *h.Element {
return h.Form(
h.Button(
h.Text("Submit"),
),
)
}
`

View file

@ -0,0 +1,8 @@
package docs
import "github.com/maddalax/htmgo/framework/h"
func Index(ctx *h.RequestContext) *h.Page {
ctx.Redirect("/docs/introduction", 302)
return h.EmptyPage()
}

View file

@ -0,0 +1,48 @@
package docs
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/ui"
)
func Installation(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Getting Started"),
h.Ul(
h.Text("Prerequisites:"),
h.Class("list-disc list-outside"),
h.Li(
Inline(
Link("Go 1.23 or above", "https://go.dev/doc/install"),
),
),
Inline(
Text("Familiarity with "),
Link("https://htmx.org", "https://htmx.org"),
Text(" and html/hypermedia"),
),
),
HelpText("If you have not read the htmx docs, please do so before continuing, a lot of concepts below will be much more clear after."),
StepTitle("1. Install htmgo"),
ui.SingleLineBashCodeSnippet(`GOPROXY=direct go install github.com/maddalax/htmgo/cli/htmgo@latest`),
StepTitle("2. Create new project"),
ui.SingleLineBashCodeSnippet(`htmgo template myapp`),
HelpText("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."),
StepTitle("3. Running the dev server"),
ui.SingleLineBashCodeSnippet(`htmgo watch`),
HelpText("htmgo has built in live reload on the dev server, to use this, run this command in the root of your project"),
HelpText("If you prefer to run the dev server yourself (no live reload), use `htmgo run`"),
StepTitle("4. Building for production"),
ui.SingleLineBashCodeSnippet(`htmgo build`),
HelpText("it will be output to `./dist`"),
NextStep(
"mt-4",
PrevBlock("Introduction", DocPath("/introduction")),
NextBlock("Core Concepts", DocPath("/core-concepts/pages")),
),
),
)
}

View file

@ -0,0 +1,126 @@
package interactivity
import (
"fmt"
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/ui"
)
import . "htmgo-site/pages/docs"
func EventsAndCommands(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Events Handler / Commands"),
Text(`
In some cases, 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, htmgo offers a few utility methods to execute various javascript on an element.
`),
Text("Example: When the form is submitted, set the button text to submitting and disable it, and vice versa after submit is done."),
ui.GoCodeSnippet(EventsExample1),
Text(`
The structure of this comes down to:
1. Add an event handler to the element
2. Add commands (found in the <b>'js'</b> package) as children to that event handler
`),
Text(`The current list of event handlers we have utility methods for so far are:`),
ui.CodeSnippet(ui.CodeSnippetProps{
Code: CurrentHandlersSnippet,
Lang: "bash",
HideLineNumbers: true,
}),
h.P(
h.Text("If there is not an existing method for the event you need, you can use the h.OnEvent method to add a handler for any "),
Link("DOM event", "https://www.w3schools.com/jsref/dom_obj_event.asp"),
h.Text(" or "),
Link("htmx event.", "https://htmx.org/events/"),
),
Text("If there is not an existing method for the event you need, you can use the <b>h.OnEvent</b> method to add an event handler for any DOM or htmx event."),
ui.GoCodeSnippet(OnEventBlurSnippet),
h.P(
h.Text(`For more details on how they work, see the source for `),
Link("lifecycle.", "https://github.com/maddalax/htmgo/blob/master/framework/h/lifecycle.go"),
h.Text(" Any method that returns *Lifecycle can be used as an event handler, and any method that returns *Command can be used as a command."),
),
h.P(
h.Text(`The current list of commands supported can be found `),
Link("here.", "https://github.com/maddalax/htmgo/blob/master/framework/js/commands.go"),
),
HelpText("Note: Each command you attach to the event handler will be passed 'self' and 'event' (if applicable) as arguments. self is the current element, and event is the event object."),
Text("Example: Evaluating arbitrary Javascript"),
ui.GoCodeSnippet(EvalArbitraryJavascriptSnippet),
HelpText("Tips: 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."),
h.P(
h.Text("More examples and usage can be found on the "),
Link("examples page, ", "/examples/js-set-text-on-click"),
h.Text("in the 'Interactivity' section."),
),
NextStep(
"mt-4",
PrevBlock("Swapping", DocPath("/interactivity/swapping")),
NextBlock("Caching Components", DocPath("/performance/caching-globally")),
),
),
)
}
const EventsExample1 = `
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"),
),
),
)
}
`
var OnEventBlurSnippet = `
h.Input(
h.OnEvent(
hx.BlurEvent,
js.SetValue("Input was blurred"),
)
)`
var EvalArbitraryJavascriptSnippet = fmt.Sprintf(`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(%s
if(Math.random() > 0.5) {
self.innerHTML = "Success!";
}%s
),
),
)
}`, "`", "`")
const CurrentHandlersSnippet = `
h.OnEvent
h.OnLoad
h.HxBeforeRequest
h.HxOnLoad
h.HxOnAfterSwap
h.OnClick
h.OnSubmit
h.HxBeforeSseMessage
h.HxAfterSseMessage
h.HxOnSseError
h.HxOnSseClose
h.HxOnSseConnecting
h.HxOnSseOpen
h.HxAfterRequest
h.HxOnMutationError
`

View file

@ -0,0 +1,110 @@
package interactivity
import . "htmgo-site/pages/docs"
import "htmgo-site/ui"
import "github.com/maddalax/htmgo/framework/h"
func Swapping(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Swapping"),
Text(`
Swapping is the process of swapping out the content of an element with another element.
This is the primary way htmgo allows you to add interactivity to your website through htmx.
`),
h.P(
h.Text("The swapping examples below utilize "),
Link("hx-swap-oob", "https://htmx.org/attributes/hx-swap-oob/"),
h.Text(" behind the scenes to swap out the content of an element."),
),
Text("Example: A simple counter"),
ui.GoCodeSnippet(SwapExample),
Text(`
In this example, when the form is submitted, an HTTP POST will be sent to the server and call <b>CounterPartial</b>.
CounterPartial will then update the count and return it back to the client via <b>h.SwapManyPartial</b>.
The h.SwapManyPartial function is a helper function that allows you to swap out multiple elements on the page.
`),
Text(`
All the routing is handled behind the scenes by htmgo, so you can reference partials directly by their function reference,
instead of having to wire up routes for each partial.
`),
Text(`
Sometimes you may need to pass additional information when calling the partial, such as an id of the current entity you are working with.
This can be done by like so:
`),
Text("Example: Getting the http path to the partial with extra qs parameters"),
ui.GoCodeSnippet(SwapGetPartialPathWithQsExample),
Text("Example: Posting to the partial path on blur"),
ui.GoCodeSnippet(SwapGetPartialPathExampleOnBlur),
h.P(
h.Text("Note: if your swapping is not working as expected, make sure the element you are swapping has an id and it matches. "),
h.Text("For further details on how oob works behind the scenes, see the "),
Link("hx-swap-oob", "https://htmx.org/attributes/hx-swap-oob/"),
h.Text(" docs."),
),
NextStep(
"mt-4",
PrevBlock("Loops / Dealing With Lists", DocPath("/control/loops")),
NextBlock("Events / Commands", DocPath("/interactivity/events")),
),
),
)
}
const SwapExample = `
func CounterPartial(ctx *h.RequestContext) *h.Partial {
count, _ := strconv.ParseInt(ctx.FormValue("count"), 10, 64)
count++
return h.SwapManyPartial(
ctx,
CounterForm(int(count)),
h.ElementIf(count > 10, SubmitButton("New record!")),
)
}
func CounterForm(count int) *h.Element {
return h.Form(
h.Id("counter-form"),
h.PostPartial(CounterPartial),
h.Input(
"text",
h.Class("hidden"),
h.Value(count),
h.Name("count"),
),
h.P(
h.Id("counter"),
h.Name("count"),
h.TextF("Count: %d", count),
),
h.Button(
h.Type("submit"),
h.Text("Increment"),
),
)
}
`
const SwapGetPartialPathWithQsExample = `
func MyComponent() *h.Element {
return h.Div(
h.GetPartialPathWithQs(
CounterPartial,
h.NewQs("count", count),
),
)
}
`
const SwapGetPartialPathExampleOnBlur = `
func MyComponent() *h.Element {
path := h.GetPartialPath(CounterPartial)
return h.Input(
h.Post(path, hx.BlurEvent),
)
}
`

View file

@ -0,0 +1,48 @@
package docs
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/ui"
)
const IntroSnippet = `func DocsPage(ctx *h.RequestContext) *h.Page {
pages := dirwalk.WalkPages("md/docs")
return h.NewPage(
h.Div(
h.Class("flex flex-col md:flex-row gap-4"),
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"),
MarkdownContent(ctx, page),
)
}),
),
),
}`
func Introduction(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-2"),
Title("Introduction"),
Text(`
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.
`),
ui.GoCodeSnippet(IntroSnippet),
Inline(
Link("The site you are reading now", "https://github.com/maddalax/htmgo/tree/master/htmgo-site"),
Text(" was written with htmgo!"),
),
NextStep(
"mt-4",
h.Div(),
NextBlock("Getting Started", DocPath("/installation")),
),
),
)
}

View file

@ -0,0 +1,63 @@
package misc
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func Formatter(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Formatter"),
Text(`
htmgo has a built-in formatter that can be used to format htmgo element blocks.
It is available through the 'htmgo' cli tool that is installed with htmgo.
`),
HelpText(`Note: if you have previously installed htmgo, you will need to run GOPROXY=direct go install github.com/maddalax/htmgo/cli/htmgo@latest to update the cli tool.`),
Text("Usage:"),
ui.SingleLineBashCodeSnippet(`htmgo format .`),
HelpText(`This will format all htmgo element blocks in your project recursively.`),
ui.SingleLineBashCodeSnippet(`htmgo format ./my-file.go`),
HelpText(`This will format the file specified.`),
Text("Before:"),
ui.GoCodeSnippet(formatBefore),
Text("After:"),
ui.GoCodeSnippet(formatAfter),
h.Div(
h.Class("hidden md:block w-[800px] h-[800px] rounded"),
Video(),
),
NextStep(
"mt-4",
PrevBlock("Tailwind Intellisense", DocPath("/misc/tailwind-intellisense")),
NextBlock("Configuration", DocPath("/config/htmgo-config")),
),
),
)
}
const formatBefore = `h.Div(
h.Class("flex gap-2"), h.Text("hello"), h.Text("world"),
)`
const formatAfter = `h.Div(
h.Class("flex gap-2"),
h.Text("hello"),
h.Text("world"),
)
`
func Video() *h.Element {
return h.Video(
h.Tag(
"source",
h.Src("/public/formatter.mp4"),
h.Type("video/mp4"),
),
h.Controls(),
h.Class("h-full w-full rounded"),
)
}

View file

@ -0,0 +1,44 @@
package misc
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/internal/urlhelper"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func TailwindIntellisense(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Tailwind Intellisense"),
Text(`
Tailwind's language server allows you to specify custom configuration on what it should match to start giving you tailwind intellisense.
`),
Text(`To make this work, you will need to update the tailwind lsp config with the config below:`),
Image("/public/tailwind-intellisense.png"),
Text(`To make this work, you will need to update your Tailwind LSP configuration with what is below:`),
SubTitle("Jetbrains IDE's"),
ui.CodeSnippetFromUrl(urlhelper.ToAbsoluteUrl(ctx, "/public/jetbrains-tailwind.json"), ui.CodeSnippetProps{
Lang: "json",
HideLineNumbers: true,
}),
Text(`
To find this configuration in GoLand you can go to Settings -> Languages & Frameworks -> Style Sheets -> Tailwind CSS and update the configuration there.
These changes are additive, add these options to your existing Tailwind LSP configuration, instead of replacing the entire file.
`),
SubTitle("Visual Studio Code"),
Text(`For VSCode, you should be able to update your settings.json with the following values:`),
ui.CodeSnippetFromUrl(urlhelper.ToAbsoluteUrl(ctx, "/public/vscode-tailwind.json"), ui.CodeSnippetProps{
Lang: "json",
HideLineNumbers: true,
}),
NextStep(
"mt-4",
PrevBlock("Mutation Error Extension", DocPath("/htmx-extensions/mutation-error")),
NextBlock("Formatting blocks", DocPath("/misc/formatter")),
),
),
)
}

View file

@ -0,0 +1,83 @@
package performance
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func CachingGlobally(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Caching Components Globally"),
Text(`
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.
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.
To cache a component in htmgo, we offer two ways, caching globally or caching per key, this section will focus on caching globally, you will learn more about caching per key in the next section:
`),
Text("<b>Methods for caching globally:</b>"),
ui.GoCodeSnippet(CachingMethods),
h.P(
h.Text("For caching components per unique identifier, see "),
Link("Caching Components Per Key", "/docs/performance/caching-per-key"),
h.Text("."),
),
Text(`<b>Usage:</b>`),
ui.GoCodeSnippet(CachedGloballyExample),
Text(`
We are using CachedT because the component takes one argument, the RequestContext.
If the component takes more arguments, use CachedT2, CachedT3, etc.
`),
Text(
`<b>Important Note:</b> When using h.CachedT and not <b>CachedPerKey</b>, 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.
The arguments passed into cached component <b>DO NOT</b> 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.
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.
`),
NextStep(
"mt-4",
PrevBlock("Events", DocPath("/interactivity/events")),
NextBlock("Caching Per Key", DocPath("/performance/caching-per-key")),
),
),
)
}
const CachingMethods = `
// 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)
`
const CachedGloballyExample = `
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(time.Minute*15, func(ctx *h.RequestContext) *h.Element {
return ExpensiveComponent(ctx)
})
func IndexPage(ctx *h.RequestContext) *h.Page {
return h.NewPage(
CachedComponent(ctx),
)
}
`

View file

@ -0,0 +1,98 @@
package performance
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func CachingPerKey(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Caching Components Per Key"),
Text(`
If you need to cache a component per unique identifier, you can use the <b>CachedPerKey</b> functions.
These functions allow you to cache a component by a specific key. This key can be any string that uniquely identifies the user.
Note: I'm using the term 'user' to simply mean a unique identifier. This could be a user ID, session ID, or any other unique identifier.
`),
Text("<b>Methods for caching per key:</b>"),
ui.GoCodeSnippet(CachingMethodsPerKey),
Text(`<b>Usage:</b>`),
ui.GoCodeSnippet(CachedPerKeyExample),
Text(`
We are using CachedPerKeyT because the component takes one argument, the RequestContext.
If the component takes more arguments, use CachedPerKeyT2, CachedPerKeyT3, etc.
`),
Text(
`
<b>Important Note:</b>
The cached value is stored globally in memory by key, it is shared across all requests. Ensure if you are storing request-specific data in a cached component, you are using a unique key for each user.
The arguments passed into cached component <b>DO NOT</b> affect the cache key. The only thing that affects the cache key is the key returned by the GetElementFuncWithKey function.
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.
`),
NextStep(
"mt-4",
PrevBlock("Caching Globally", DocPath("/performance/caching-globally")),
NextBlock("Pushing Data", DocPath("/pushing-data/sse")),
),
),
)
}
const CachingMethodsPerKey = `
// No arguments passed to the component, the component can be cached by a specific key
h.CachedPerKey(duration time.Duration, cb GetElementFuncWithKey)
// One argument passed to the component, the component can be cached by a specific key
h.CachedPerKeyT1(duration time.Duration, cb GetElementFuncWithKey)
// Two argument passed to the component, the component can be cached by a specific key
h.CachedPerKeyT2(duration time.Duration, cb GetElementFuncWithKey)
// Three arguments passed to the component, the component can be cached by a specific key
h.CachedPerKeyT3(duration time.Duration, cb GetElementFuncWithKey)
// Four arguments passed to the component, the component can be cached by a specific key
h.CachedPerKeyT4(duration time.Duration, cb GetElementFuncWithKey)
`
const CachedPerKeyExample = `
var CachedUserDocuments = h.CachedPerKeyT(time.Minute*15, func(ctx *h.RequestContext) (string, h.GetElementFunc) {
userId := getUserIdFromSession(ctx)
return userId, func() *h.Element {
return UserDocuments(ctx)
}
})
func UserDocuments(ctx *h.RequestContext) *h.Element {
docService := NewDocumentService(ctx)
// Expensive call
docs := docService.getDocuments()
return h.Div(
h.Class("grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"),
h.List(docs, func(doc Document, index int) *h.Element {
return h.Div(
h.Class("p-4 bg-white border border-gray-200 rounded-md"),
h.H3(doc.Title),
h.P(doc.Description),
)
}),
)
}
func MyPage(ctx *h.RequestContext) *h.Page {
// Note this is not a real way to create a context, just an example
user1 := &h.RequestContext{
Session: "user_1_session",
}
user2 := &h.RequestContext{
Session: "user_2_session",
}
// Different users will get different cached components
return h.NewPage(
CachedUserDocuments(user1),
CachedUserDocuments(user2),
)
}
`

View file

@ -0,0 +1,87 @@
package pushing_data
import (
"github.com/maddalax/htmgo/framework/h"
. "htmgo-site/pages/docs"
"htmgo-site/ui"
)
func ServerSentEvents(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Server Sent Events (SSE)"),
Text(`
htmgo supports server-sent events (SSE) out of the box.
This allows you to push data from the server to the client in real-time.
`),
h.P(
h.Text("Example of this can be found in the "),
Link("examples/chat", "examples/chat"),
h.Text(" project."),
),
SubTitle("How it works"),
Text(`1. The client sends a request to the server to establish a connection.
2. The server holds the connection open and sends data (in our case, most likely elements) to the client whenever there is new data to send.
3. The htmgo SSE extension uses hx-swap-oob to swap out the elements that the server sends.
`),
HelpText("Note: SSE is unidirectional (the server can only send data to the client). For the client to send data to the server, normal xhr behavior should be used (form submission, triggers, etc)."),
Text(`<b>Usage:</b>`),
Text("Add the SSE connection attribute and the path to the handler that will handle the connection."),
ui.GoCodeSnippet(SseConnectAttribute),
Text("The following <b>Event Handlers</b> can be used to react to SSE connections."),
ui.GoCodeSnippet(SseEventHandlers),
Text("Example: Adding an event listener handle SSE errors."),
ui.GoCodeSnippet(SseErrorHandlingExample),
Text("Example: Clearing the input field after sending a message."),
ui.GoCodeSnippet(SseClearInputExample),
NextStep(
"mt-4",
PrevBlock("Caching Per Key", DocPath("/performance/caching-per-key")),
NextBlock("HTMX extensions", DocPath("/htmx-extensions/overview")),
),
),
)
}
const SseConnectAttribute = `
h.Attribute("sse-connect", fmt.Sprintf("/chat/%s", roomId))
`
const SseEventHandlers = `
h.HxOnSseOpen
h.HxBeforeSseMessage
h.HxAfterSseMessage
h.HxOnSseError
h.HxOnSseClose
h.HxOnSseConnecting
`
const SseErrorHandlingExample = `
h.HxOnSseError(
js.EvalJs(fmt.Sprintf("
const reason = e.detail.event.data
if(['invalid room', 'no session', 'invalid user'].includes(reason)) {
window.location.href = '/?roomId=%s';
} else if(e.detail.event.code === 1011) {
window.location.reload()
} else if (e.detail.event.code === 1008 || e.detail.event.code === 1006) {
window.location.href = '/?roomId=%s';
} else {
console.error('Connection closed:', e.detail.event)
}
", roomId, roomId)),
),
`
const SseClearInputExample = `
func MessageInput() *h.Element {
return h.Input("text",
h.Id("message-input"),
h.Required(),
h.HxAfterSseMessage(
js.SetValue(""),
),
)
}`

View file

@ -0,0 +1,35 @@
package docs
import (
"github.com/maddalax/htmgo/framework/h"
)
func RelatedProjects(ctx *h.RequestContext) *h.Page {
return DocPage(
ctx,
h.Div(
h.Class("flex flex-col gap-3"),
Title("Other languages and related projects"),
Text(`
If you're not a Go user but are interested in the idea of what htmgo is, you might want to check out these other projects:
`),
h.Ul(
h.Class("font-bold"),
h.Text("Python:"),
h.Class("list-disc list-inside"),
h.Li(
h.P(
h.Class("font-normal"),
Link("fastht.ml", "https://fastht.ml"),
h.Text(" - Modern web applications in pure Python, Built on solid web foundations, not the latest fads - with FastHTML you can get started on anything from simple dashboards to scalable web applications in minutes."),
),
),
),
NextStep(
"mt-4",
PrevBlock("Tailwind Intellisense", "/docs/misc/tailwind-intellisense"),
NextBlock("Adding Interactivity", "/docs/interactivity/swapping"),
),
),
)
}

View file

@ -0,0 +1,126 @@
package docs
import (
"github.com/maddalax/htmgo/framework/h"
)
type Section struct {
Title string
Pages []*Page
}
type Page struct {
Title string
Path string
}
func DocPath(path string) string {
return "/docs" + path
}
var sections = []Section{
{
Title: "Getting Started",
Pages: []*Page{
{Title: "Introduction", Path: DocPath("/introduction")},
{Title: "Quick Start", Path: DocPath("/installation")},
{Title: "Related Projects", Path: DocPath("/related-projects")},
},
},
{
Title: "Core Concepts",
Pages: []*Page{
{Title: "Pages", Path: DocPath("/core-concepts/pages")},
{Title: "Partials", Path: DocPath("/core-concepts/partials")},
{Title: "Components", Path: DocPath("/core-concepts/components")},
{Title: "Tags and Attributes", Path: DocPath("/core-concepts/tags-and-attributes")},
{Title: "Raw HTML", Path: DocPath("/core-concepts/raw-html")},
},
},
{
Title: "Control",
Pages: []*Page{
{Title: "Conditionals", Path: DocPath("/control/if-else")},
{Title: "Rendering Lists", Path: DocPath("/control/loops")},
},
},
{
Title: "Interactivity",
Pages: []*Page{
{Title: "Swapping", Path: DocPath("/interactivity/swapping")},
{Title: "Events", Path: DocPath("/interactivity/events")},
{Title: "Evaluating Javascript", Path: DocPath("/interactivity/events")},
},
},
{
Title: "Performance",
Pages: []*Page{
{Title: "Caching Globally", Path: DocPath("/performance/caching-globally")},
{Title: "Caching Per Key", Path: DocPath("/performance/caching-per-key")},
},
},
{
Title: "Pushing Data",
Pages: []*Page{
{Title: "Server Sent Events", Path: DocPath("/pushing-data/sse")},
},
},
{
Title: "HTMX Extensions",
Pages: []*Page{
{Title: "Overview", Path: DocPath("/htmx-extensions/overview")},
{Title: "Trigger Children", Path: DocPath("/htmx-extensions/trigger-children")},
{Title: "Mutation Error", Path: DocPath("/htmx-extensions/mutation-error")},
},
},
{
Title: "Miscellaneous",
Pages: []*Page{
{Title: "Tailwind Intellisense", Path: DocPath("/misc/tailwind-intellisense")},
{Title: "Formatter", Path: DocPath("/misc/formatter")},
},
},
{
Title: "Configuration",
Pages: []*Page{
{Title: "Htmgo Config", Path: DocPath("/config/htmgo-config")},
},
},
}
func DocSidebar() *h.Element {
return h.Div(
h.Class("px-3 py-2 pr-6 md:min-h-screen pb-4 mb:pb-0 bg-neutral-50 border-r border-r-slate-300 overflow-y-auto"),
h.Div(
h.Div(
h.Class("mb-3"),
h.A(
h.Href("#quick-start-introduction"),
h.Text("Documentation"),
h.Class("md:mt-4 text-xl text-slate-900 font-bold"),
),
),
h.Div(
h.Class("flex flex-col gap-4"),
h.List(sections, func(entry Section, index int) *h.Element {
return h.Div(
h.P(
h.Text(entry.Title),
h.Class("text-slate-800 font-bold"),
),
h.Div(
h.Class("pl-4 flex flex-col"),
h.List(entry.Pages, func(page *Page, index int) *h.Element {
return h.A(
h.Href(page.Path),
h.Text(page.Title),
h.Class("text-slate-900 hover:text-rose-400"),
)
}),
),
)
}),
),
),
)
}

View file

@ -65,5 +65,5 @@ func renderCodeToString(snippet *Snippet) *h.Element {
source = out.String()
}
return ui.CodeSnippet(source, "border-radius: 0.5rem;")
return ui.GoCodeSnippet(source, "border-radius: 0.5rem;")
}

View file

@ -2,13 +2,13 @@ package examples
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/pages"
"htmgo-site/pages/base"
"htmgo-site/partials"
)
func Index(ctx *h.RequestContext) *h.Page {
snippet := GetSnippet(ctx)
return pages.RootPage(
return base.RootPage(
ctx,
h.Div(
h.Class("flex h-full"),

View file

@ -2,11 +2,12 @@ package pages
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/pages/base"
"htmgo-site/partials"
)
func HtmlToGoPage(ctx *h.RequestContext) *h.Page {
return PageWithNav(
return base.PageWithNav(
ctx,
h.Div(
h.Class("flex flex-col h-screen items-center justify-center w-full pt-6"),

View file

@ -2,10 +2,11 @@ package pages
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/pages/base"
)
func IndexPage(ctx *h.RequestContext) *h.Page {
return PageWithNav(
return base.PageWithNav(
ctx,
h.Div(
h.Class("flex items-center justify-center"),

View file

@ -3,10 +3,11 @@ package pages
import (
"fmt"
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/pages/base"
)
func TestFormatPage(ctx *h.RequestContext) *h.Page {
return RootPage(
return base.RootPage(
ctx,
h.Div(
h.P(
@ -30,7 +31,7 @@ func notPage() int {
}
func TestOtherPage(ctx *h.RequestContext) *h.Page {
return RootPage(
return base.RootPage(
ctx,
h.Div(
h.Id("test"),

View file

@ -2,11 +2,12 @@ package pages
import (
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/pages/base"
"htmgo-site/partials"
)
func CurrentTimePage(ctx *h.RequestContext) *h.Page {
return RootPage(
return base.RootPage(
ctx,
h.GetPartial(
partials.CurrentTimePartial,

View file

@ -1,94 +0,0 @@
package partials
import (
"github.com/maddalax/htmgo/framework/datastructure/orderedmap"
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/internal/dirwalk"
"strings"
)
func formatPart(part string) string {
if part[1] == '_' {
part = part[2:]
}
part = strings.ReplaceAll(part, "-", " ")
part = strings.ReplaceAll(part, "_", " ")
part = strings.Title(part)
return part
}
func CreateAnchor(parts []string) string {
return strings.Join(h.Map(parts, func(part string) string {
return strings.ReplaceAll(strings.ToLower(formatPart(part)), " ", "-")
}), "-")
}
func partsToName(parts []string) string {
builder := strings.Builder{}
for i, part := range parts {
if i == 0 {
continue
}
part = formatPart(part)
builder.WriteString(part)
builder.WriteString(" ")
}
return builder.String()
}
func groupByFirstPart(pages []*dirwalk.Page) *orderedmap.Map[string, []*dirwalk.Page] {
grouped := orderedmap.New[string, []*dirwalk.Page]()
for _, page := range pages {
if len(page.Parts) > 0 {
section := page.Parts[0]
existing, has := grouped.Get(section)
if !has {
existing = []*dirwalk.Page{}
grouped.Set(section, existing)
}
grouped.Set(section, append(existing, page))
}
}
return grouped
}
func DocSidebar(pages []*dirwalk.Page) *h.Element {
grouped := groupByFirstPart(pages)
return h.Div(
h.Class("px-3 py-2 pr-6 md:min-h-screen pb-4 mb:pb-0 bg-neutral-50 border-r border-r-slate-300 overflow-y-auto"),
h.Div(
h.Div(
h.Class("mb-3"),
h.A(
h.Href("#quick-start-introduction"),
h.Text("Documentation"),
h.Class("md:mt-4 text-xl text-slate-900 font-bold"),
),
),
h.Div(
h.Class("flex flex-col gap-4"),
h.List(grouped.Entries(), func(entry orderedmap.Entry[string, []*dirwalk.Page], index int) *h.Element {
return h.Div(
h.P(
h.Text(formatPart(entry.Key)),
h.Class("text-slate-800 font-bold"),
),
h.Div(
h.Class("pl-4 flex flex-col"),
h.List(entry.Value, func(page *dirwalk.Page, index int) *h.Element {
anchor := CreateAnchor(page.Parts)
return h.A(
h.Href("#"+anchor),
h.Text(partsToName(page.Parts)),
h.Class("text-slate-900 hover:text-rose-400"),
)
}),
),
)
}),
),
),
)
}

View file

@ -10,7 +10,11 @@ func ConvertHtmlToGo(ctx *h.RequestContext) *h.Partial {
value := ctx.FormValue("html-input")
parsed := string(htmltogo.Parse([]byte(value)))
formatted := ui.FormatCode(parsed, "height: 100%;")
formatted := ui.FormatCode(ui.CodeSnippetProps{
Code: parsed,
Lang: "go",
CustomStyles: []string{"height: 100%;"},
})
return h.SwapManyPartial(ctx,
GoOutput(formatted),
@ -53,7 +57,7 @@ func GoOutput(content string) *h.Element {
),
h.If(
content != "",
ui.CopyButton("#go-output-raw"),
ui.AbsoluteCopyButton("#go-output-raw"),
),
),
)

View file

@ -9,7 +9,12 @@ 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)),
h.Class("flex gap-1 items-center"),
h.Pf("The current time is "),
h.Span(
h.Text(now.Format(time.RFC3339)),
h.Class("font-bold"),
),
),
)
}

View file

@ -6,9 +6,10 @@ import (
"github.com/maddalax/htmgo/framework/js"
)
func CopyButton(selector string) *h.Element {
func CopyButton(selector string, classes ...string) *h.Element {
classes = append(classes, "flex p-2 bg-slate-800 text-white cursor-pointer items-center")
return h.Div(
h.Class("absolute top-0 right-0 p-2 bg-slate-800 text-white rounded-bl-md cursor-pointer"),
h.Class(classes...),
h.Text("Copy"),
h.OnClick(
// language=JavaScript
@ -26,3 +27,7 @@ func CopyButton(selector string) *h.Element {
),
)
}
func AbsoluteCopyButton(selector string) *h.Element {
return CopyButton(selector, "absolute top-0 right-0 rounded-bl-md")
}

View file

@ -9,20 +9,26 @@ import (
"github.com/alecthomas/chroma/v2/styles"
"github.com/google/uuid"
"github.com/maddalax/htmgo/framework/h"
"io"
"net/http"
"strings"
)
func FormatCode(code string, customStyles ...string) string {
func FormatCode(props CodeSnippetProps) string {
if props.SingleLine {
props.CustomStyles = append(props.CustomStyles, "height: 50px; width: 100%;")
}
var buf bytes.Buffer
lexer := lexers.Get("go")
lexer := lexers.Get(props.Lang)
style := styles.Get("github")
formatter := html.New(
html.WrapLongLines(true),
html.WithLineNumbers(true),
html.WithLineNumbers(!props.SingleLine && !props.HideLineNumbers),
html.WithCustomCSS(map[chroma.TokenType]string{
chroma.PreWrapper: fmt.Sprintf("font-size: 14px; padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important; %s", strings.Join(customStyles, ";")),
chroma.PreWrapper: fmt.Sprintf("border-radius: 0.2rem; line-height: 24px; font-size: 14px; padding: 12px; overflow: auto; background-color: rgb(245, 245, 245) !important; %s", strings.Join(props.CustomStyles, ";")),
}))
iterator, err := lexer.Tokenise(nil, code)
iterator, err := lexer.Tokenise(nil, props.Code)
if err != nil {
return ""
}
@ -30,18 +36,94 @@ func FormatCode(code string, customStyles ...string) string {
return buf.String()
}
func CodeSnippet(code string, customStyles ...string) *h.Element {
type CodeSnippetProps struct {
Code string
Lang string
CustomStyles []string
HideLineNumbers bool
SingleLine bool
}
func CodeSnippet(props CodeSnippetProps) *h.Element {
id := fmt.Sprintf("code-snippet-%s", uuid.NewString())
props.Code = strings.TrimPrefix(props.Code, "\n")
props.Code = strings.TrimSuffix(props.Code, "\n")
if props.SingleLine {
return h.Div(
h.Class("flex items-center w-full"),
h.Div(
h.UnsafeRaw(props.Code),
h.Class("hidden"),
h.Id(id),
),
h.UnsafeRaw(
FormatCode(props),
),
CopyButton("#"+id, "h-[50px] rounded-sm"),
)
}
return h.Div(
h.Class("relative"),
h.Div(
h.UnsafeRaw(code),
h.UnsafeRaw(props.Code),
h.Class("hidden"),
h.Id(id),
),
CopyButton("#"+id),
AbsoluteCopyButton("#"+id),
h.UnsafeRaw(
FormatCode(code, customStyles...),
FormatCode(props),
),
)
}
func BashCodeSnippet(code string, customStyles ...string) *h.Element {
return CodeSnippet(CodeSnippetProps{
Code: code,
Lang: "bash",
CustomStyles: customStyles,
})
}
func SingleLineBashCodeSnippet(code string, customStyles ...string) *h.Element {
return CodeSnippet(CodeSnippetProps{
Code: code,
Lang: "bash",
CustomStyles: customStyles,
SingleLine: true,
})
}
func GoCodeSnippet(code string, customStyles ...string) *h.Element {
return CodeSnippet(CodeSnippetProps{
Code: code,
Lang: "go",
CustomStyles: customStyles,
})
}
func GoCodeSnippetSingleLine(code string, customStyles ...string) *h.Element {
return CodeSnippet(CodeSnippetProps{
Code: code,
Lang: "go",
CustomStyles: customStyles,
SingleLine: true,
})
}
func CodeSnippetFromUrl(url string, props CodeSnippetProps) *h.Element {
data, err := http.Get(url)
if err != nil {
fmt.Printf("error: %s\n", err.Error())
return h.Empty()
}
defer data.Body.Close()
b, err := io.ReadAll(data.Body)
if err != nil {
return h.Empty()
}
props.Code = string(b)
return CodeSnippet(props)
}