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:
parent
df9c7f9cf7
commit
35877a1b2e
68 changed files with 1948 additions and 1268 deletions
|
|
@ -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...))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
BIN
htmgo-site/assets/public/formatter.mp4
Normal file
BIN
htmgo-site/assets/public/formatter.mp4
Normal file
Binary file not shown.
14
htmgo-site/assets/public/jetbrains-tailwind.json
Normal file
14
htmgo-site/assets/public/jetbrains-tailwind.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"includeLanguages": {
|
||||
"go": "html"
|
||||
},
|
||||
"experimental": {
|
||||
"configFile": null,
|
||||
"classRegex": [
|
||||
["Class\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
|
||||
["ClassX\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
|
||||
["ClassIf\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
|
||||
["Classes\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"]
|
||||
]
|
||||
}
|
||||
}
|
||||
11
htmgo-site/assets/public/vscode-tailwind.json
Normal file
11
htmgo-site/assets/public/vscode-tailwind.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"tailwindCSS.includeLanguages": {
|
||||
"go": "html"
|
||||
},
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["Class\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
|
||||
["ClassX\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
|
||||
["ClassIf\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`]"],
|
||||
["Classes\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`"]
|
||||
]
|
||||
}
|
||||
|
|
@ -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"]
|
||||
|
|
|
|||
28
htmgo-site/internal/urlhelper/resolve.go
Normal file
28
htmgo-site/internal/urlhelper/resolve.go
Normal 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()
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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")),
|
||||
),
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
```
|
||||
|
||||
|
|
@ -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),
|
||||
)
|
||||
})
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
@ -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!";
|
||||
}
|
||||
`,
|
||||
),
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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(""),
|
||||
),
|
||||
)
|
||||
}
|
||||
```
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
||||

|
||||
|
||||
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\\(([^)]*)\\)", "[\"`]([^\"`]*)[\"`"]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
## Troubleshooting:
|
||||
|
||||
**command not found: htmgo**
|
||||
ensure you installed htmgo above and ensure GOPATH is set in your shell
|
||||
|
|
@ -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,
|
||||
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
165
htmgo-site/pages/docs/base.go
Normal file
165
htmgo-site/pages/docs/base.go
Normal 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...),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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: []
|
||||
```
|
||||
`
|
||||
73
htmgo-site/pages/docs/control/if-else.go
Normal file
73
htmgo-site/pages/docs/control/if-else.go
Normal 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"),
|
||||
)
|
||||
`
|
||||
55
htmgo-site/pages/docs/control/loops.go
Normal file
55
htmgo-site/pages/docs/control/loops.go
Normal 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),
|
||||
)
|
||||
})
|
||||
`
|
||||
41
htmgo-site/pages/docs/core-concepts/components.go
Normal file
41
htmgo-site/pages/docs/core-concepts/components.go
Normal 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.
|
||||
`),
|
||||
)
|
||||
}
|
||||
113
htmgo-site/pages/docs/core-concepts/pages.go
Normal file
113
htmgo-site/pages/docs/core-concepts/pages.go
Normal 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."),
|
||||
)
|
||||
}
|
||||
84
htmgo-site/pages/docs/core-concepts/partials.go
Normal file
84
htmgo-site/pages/docs/core-concepts/partials.go
Normal 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"),
|
||||
),
|
||||
)
|
||||
}
|
||||
45
htmgo-site/pages/docs/core-concepts/raw-html.go
Normal file
45
htmgo-site/pages/docs/core-concepts/raw-html.go
Normal 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')")`
|
||||
84
htmgo-site/pages/docs/core-concepts/tags-and-attributes.go
Normal file
84
htmgo-site/pages/docs/core-concepts/tags-and-attributes.go
Normal 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"),
|
||||
)
|
||||
),
|
||||
)`
|
||||
42
htmgo-site/pages/docs/htmx-extensions/mutation-error.go
Normal file
42
htmgo-site/pages/docs/htmx-extensions/mutation-error.go
Normal 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"),
|
||||
),
|
||||
)
|
||||
`
|
||||
80
htmgo-site/pages/docs/htmx-extensions/overview.go
Normal file
80
htmgo-site/pages/docs/htmx-extensions/overview.go
Normal 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
|
||||
`
|
||||
37
htmgo-site/pages/docs/htmx-extensions/trigger-children.go
Normal file
37
htmgo-site/pages/docs/htmx-extensions/trigger-children.go
Normal 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"),
|
||||
),
|
||||
)
|
||||
}
|
||||
`
|
||||
8
htmgo-site/pages/docs/index.go
Normal file
8
htmgo-site/pages/docs/index.go
Normal 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()
|
||||
}
|
||||
48
htmgo-site/pages/docs/installation.go
Normal file
48
htmgo-site/pages/docs/installation.go
Normal 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")),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
126
htmgo-site/pages/docs/interactivity/events.go
Normal file
126
htmgo-site/pages/docs/interactivity/events.go
Normal 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
|
||||
`
|
||||
110
htmgo-site/pages/docs/interactivity/swapping.go
Normal file
110
htmgo-site/pages/docs/interactivity/swapping.go
Normal 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),
|
||||
)
|
||||
}
|
||||
`
|
||||
48
htmgo-site/pages/docs/introduction.go
Normal file
48
htmgo-site/pages/docs/introduction.go
Normal 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")),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
63
htmgo-site/pages/docs/misc/formatter.go
Normal file
63
htmgo-site/pages/docs/misc/formatter.go
Normal 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"),
|
||||
)
|
||||
}
|
||||
44
htmgo-site/pages/docs/misc/tailwind-intellisense.go
Normal file
44
htmgo-site/pages/docs/misc/tailwind-intellisense.go
Normal 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")),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
83
htmgo-site/pages/docs/performance/caching-globally.go
Normal file
83
htmgo-site/pages/docs/performance/caching-globally.go
Normal 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),
|
||||
)
|
||||
}
|
||||
`
|
||||
98
htmgo-site/pages/docs/performance/caching-per-key.go
Normal file
98
htmgo-site/pages/docs/performance/caching-per-key.go
Normal 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),
|
||||
)
|
||||
}
|
||||
`
|
||||
87
htmgo-site/pages/docs/pushing-data/sse.go
Normal file
87
htmgo-site/pages/docs/pushing-data/sse.go
Normal 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(""),
|
||||
),
|
||||
)
|
||||
}`
|
||||
35
htmgo-site/pages/docs/related-projects.go
Normal file
35
htmgo-site/pages/docs/related-projects.go
Normal 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"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
126
htmgo-site/pages/docs/sidebar.go
Normal file
126
htmgo-site/pages/docs/sidebar.go
Normal 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"),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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;")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue