htmgo - custom formatter (#47)

* format htmgo elements on save

* formatter updates

* ensure we maintain comments
This commit is contained in:
maddalax 2024-10-25 10:33:48 -05:00 committed by GitHub
parent 3f8ab7d905
commit 8736c00fd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 763 additions and 306 deletions

View file

@ -9,6 +9,7 @@ import (
"github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets" "github.com/maddalax/htmgo/cli/htmgo/tasks/copyassets"
"github.com/maddalax/htmgo/cli/htmgo/tasks/css" "github.com/maddalax/htmgo/cli/htmgo/tasks/css"
"github.com/maddalax/htmgo/cli/htmgo/tasks/downloadtemplate" "github.com/maddalax/htmgo/cli/htmgo/tasks/downloadtemplate"
"github.com/maddalax/htmgo/cli/htmgo/tasks/formatter"
"github.com/maddalax/htmgo/cli/htmgo/tasks/process" "github.com/maddalax/htmgo/cli/htmgo/tasks/process"
"github.com/maddalax/htmgo/cli/htmgo/tasks/reloader" "github.com/maddalax/htmgo/cli/htmgo/tasks/reloader"
"github.com/maddalax/htmgo/cli/htmgo/tasks/run" "github.com/maddalax/htmgo/cli/htmgo/tasks/run"
@ -19,10 +20,10 @@ import (
) )
func main() { func main() {
done := RegisterSignals() needsSignals := true
commandMap := make(map[string]*flag.FlagSet) commandMap := make(map[string]*flag.FlagSet)
commands := []string{"template", "run", "watch", "build", "setup", "css", "schema", "generate"} commands := []string{"template", "run", "watch", "build", "setup", "css", "schema", "generate", "format"}
for _, command := range commands { for _, command := range commands {
commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError) commandMap[command] = flag.NewFlagSet(command, flag.ExitOnError)
@ -56,6 +57,15 @@ func main() {
slog.Debug("Running task:", slog.String("task", taskName)) slog.Debug("Running task:", slog.String("task", taskName))
slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir())) slog.Debug("working dir:", slog.String("dir", process.GetWorkingDir()))
if taskName == "format" {
needsSignals = false
}
done := make(chan bool, 1)
if needsSignals {
done = RegisterSignals()
}
if taskName == "watch" { if taskName == "watch" {
fmt.Printf("Running in watch mode\n") fmt.Printf("Running in watch mode\n")
os.Setenv("ENV", "development") os.Setenv("ENV", "development")
@ -90,7 +100,18 @@ func main() {
}() }()
startWatcher(reloader.OnFileChange) startWatcher(reloader.OnFileChange)
} else { } else {
if taskName == "schema" { if taskName == "format" {
if len(os.Args) < 3 {
fmt.Println(fmt.Sprintf("Usage: htmgo format <file>"))
os.Exit(1)
}
file := os.Args[2]
if file == "." {
formatter.FormatDir(process.GetWorkingDir())
} else {
formatter.FormatFile(os.Args[2])
}
} else if taskName == "schema" {
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter entity name:") fmt.Print("Enter entity name:")
text, _ := reader.ReadString('\n') text, _ := reader.ReadString('\n')

View file

@ -0,0 +1,50 @@
package formatter
import (
"fmt"
"github.com/maddalax/htmgo/tools/html-to-htmgo/htmltogo"
"os"
"path/filepath"
"strings"
)
func FormatDir(dir string) {
files, err := os.ReadDir(dir)
if err != nil {
fmt.Printf("error reading dir: %s\n", err.Error())
return
}
for _, file := range files {
if file.IsDir() {
FormatDir(filepath.Join(dir, file.Name()))
} else {
FormatFile(filepath.Join(dir, file.Name()))
}
}
}
func FormatFile(file string) {
if !strings.HasSuffix(file, ".go") {
return
}
fmt.Printf("formatting file: %s\n", file)
source, err := os.ReadFile(file)
if err != nil {
fmt.Printf("error reading file: %s\n", err.Error())
return
}
str := string(source)
if !strings.Contains(str, "github.com/maddalax/htmgo/framework/h") {
return
}
parsed := htmltogo.Indent(str)
os.WriteFile(file, []byte(parsed), 0644)
return
}

View file

@ -115,7 +115,7 @@ func OnShutdown() {
} }
} }
// give it a second // give it a second
time.Sleep(time.Second * 2) time.Sleep(time.Second * 1)
// force kill // force kill
KillAll() KillAll()
} }

View file

@ -11,18 +11,25 @@ import (
func MessageRow(message *Message) *h.Element { func MessageRow(message *Message) *h.Element {
return h.Div( return h.Div(
h.Attribute("hx-swap-oob", "beforeend"), h.Attribute("hx-swap-oob", "beforeend"),
h.Class("flex flex-col gap-4 w-full break-words whitespace-normal"), // Ensure container breaks long words h.Class("flex flex-col gap-4 w-full break-words whitespace-normal"),
// Ensure container breaks long words
h.Id("messages"), h.Id("messages"),
h.Div( h.Div(
h.Class("flex flex-col gap-1"), h.Class("flex flex-col gap-1"),
h.Div( h.Div(
h.Class("flex gap-2 items-center"), h.Class("flex gap-2 items-center"),
h.Pf(message.UserName, h.Class("font-bold")), h.Pf(
message.UserName,
h.Class("font-bold"),
),
h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")), h.Pf(message.CreatedAt.In(time.Local).Format("01/02 03:04 PM")),
), ),
h.Article( h.Article(
h.Class("break-words whitespace-normal"), // Ensure message text wraps correctly h.Class("break-words whitespace-normal"),
h.P(h.Text(message.Message)), // Ensure message text wraps correctly
h.P(
h.Text(message.Message),
),
), ),
), ),
) )

View file

@ -6,6 +6,9 @@ func FormError(error string) *h.Element {
return h.Div( return h.Div(
h.Id("form-error"), h.Id("form-error"),
h.Text(error), h.Text(error),
h.If(error != "", h.Class("p-4 bg-rose-400 text-white rounded")), h.If(
error != "",
h.Class("p-4 bg-rose-400 text-white rounded"),
),
) )
} }

View file

@ -19,11 +19,14 @@ type InputProps struct {
} }
func Input(props InputProps) *h.Element { func Input(props InputProps) *h.Element {
validation := h.If(props.ValidationPath != "", h.Children( validation := h.If(
props.ValidationPath != "",
h.Children(
h.Post(props.ValidationPath, hx.BlurEvent), h.Post(props.ValidationPath, hx.BlurEvent),
h.Attribute("hx-swap", "innerHTML transition:true"), h.Attribute("hx-swap", "innerHTML transition:true"),
h.Attribute("hx-target", "next div"), h.Attribute("hx-target", "next div"),
)) ),
)
if props.Type == "" { if props.Type == "" {
props.Type = "text" props.Type = "text"
@ -32,18 +35,41 @@ func Input(props InputProps) *h.Element {
input := h.Input( input := h.Input(
props.Type, props.Type,
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"), h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
h.If(props.Name != "", h.Name(props.Name)), h.If(
h.If(props.Children != nil, h.Children(props.Children...)), props.Name != "",
h.If(props.Required, h.Required()), h.Name(props.Name),
h.If(props.Placeholder != "", h.Placeholder(props.Placeholder)), ),
h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)), h.If(
props.Children != nil,
h.Children(props.Children...),
),
h.If(
props.Required,
h.Required(),
),
h.If(
props.Placeholder != "",
h.Placeholder(props.Placeholder),
),
h.If(
props.DefaultValue != "",
h.Attribute("value", props.DefaultValue),
),
validation, validation,
) )
wrapped := h.Div( wrapped := h.Div(
h.If(props.Id != "", h.Id(props.Id)), h.If(
props.Id != "",
h.Id(props.Id),
),
h.Class("flex flex-col gap-1"), h.Class("flex flex-col gap-1"),
h.If(props.Label != "", h.Label(h.Text(props.Label))), h.If(
props.Label != "",
h.Label(
h.Text(props.Label),
),
),
input, input,
h.Div( h.Div(
h.Id(props.Id+"-error"), h.Id(props.Id+"-error"),

View file

@ -17,13 +17,10 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
RootPage( RootPage(
h.Div( h.Div(
h.TriggerChildren(), h.TriggerChildren(),
h.Attribute("sse-connect", fmt.Sprintf("/sse/chat/%s", roomId)), h.Attribute("sse-connect", fmt.Sprintf("/sse/chat/%s", roomId)),
h.HxOnSseOpen( h.HxOnSseOpen(
js.ConsoleLog("Connected to chat room"), js.ConsoleLog("Connected to chat room"),
), ),
h.HxOnSseError( h.HxOnSseError(
js.EvalJs(fmt.Sprintf(` js.EvalJs(fmt.Sprintf(`
const reason = e.detail.event.data const reason = e.detail.event.data
@ -38,35 +35,27 @@ func ChatRoom(ctx *h.RequestContext) *h.Page {
} }
`, roomId, roomId)), `, roomId, roomId)),
), ),
// Adjusted flex properties for responsive layout // Adjusted flex properties for responsive layout
h.Class("flex flex-row h-screen bg-neutral-100 overflow-x-hidden"), h.Class("flex flex-row h-screen bg-neutral-100 overflow-x-hidden"),
// Collapse Button for mobile // Collapse Button for mobile
CollapseButton(), CollapseButton(),
// Sidebar for connected users // Sidebar for connected users
UserSidebar(), UserSidebar(),
h.Div( h.Div(
// Adjusted to fill height and width // Adjusted to fill height and width
h.Class("flex flex-col h-full w-full bg-white p-4 overflow-hidden"), h.Class("flex flex-col h-full w-full bg-white p-4 overflow-hidden"),
// Room name at the top, fixed // Room name at the top, fixed
CachedRoomHeader(ctx), CachedRoomHeader(ctx),
h.HxAfterSseMessage( h.HxAfterSseMessage(
js.EvalJsOnSibling("#messages", js.EvalJsOnSibling("#messages",
`element.scrollTop = element.scrollHeight;`), `element.scrollTop = element.scrollHeight;`),
), ),
// Chat Messages // Chat Messages
h.Div( h.Div(
h.Id("messages"), h.Id("messages"),
// Adjusted flex properties and removed max-width // Adjusted flex properties and removed max-width
h.Class("flex flex-col gap-4 mb-4 overflow-auto flex-grow w-full pt-[50px]"), h.Class("flex flex-col gap-4 mb-4 overflow-auto flex-grow w-full pt-[50px]"),
), ),
// Chat Input at the bottom // Chat Input at the bottom
Form(), Form(),
), ),
@ -91,7 +80,10 @@ func roomNameHeader(ctx *h.RequestContext) *h.Element {
} }
return h.Div( return h.Div(
h.Class("bg-neutral-700 text-white p-3 shadow-sm w-full fixed top-0 left-0 flex justify-center z-10"), h.Class("bg-neutral-700 text-white p-3 shadow-sm w-full fixed top-0 left-0 flex justify-center z-10"),
h.H2F(room.Name, h.Class("text-lg font-bold")), h.H2F(
room.Name,
h.Class("text-lg font-bold"),
),
h.Div( h.Div(
h.Class("absolute right-5 top-3 cursor-pointer"), h.Class("absolute right-5 top-3 cursor-pointer"),
h.Text("Share"), h.Text("Share"),
@ -108,7 +100,10 @@ func UserSidebar() *h.Element {
return h.Div( return h.Div(
h.Class("sidebar h-full pt-[67px] min-w-48 w-48 bg-neutral-200 p-4 flex-col justify-between gap-3 rounded-l-lg hidden md:flex"), h.Class("sidebar h-full pt-[67px] min-w-48 w-48 bg-neutral-200 p-4 flex-col justify-between gap-3 rounded-l-lg hidden md:flex"),
h.Div( h.Div(
h.H3F("Connected Users", h.Class("text-lg font-bold")), h.H3F(
"Connected Users",
h.Class("text-lg font-bold"),
),
chat.ConnectedUsers(make([]db.User, 0), ""), chat.ConnectedUsers(make([]db.User, 0), ""),
), ),
h.A( h.A(
@ -121,9 +116,11 @@ func UserSidebar() *h.Element {
func CollapseButton() *h.Element { func CollapseButton() *h.Element {
return h.Div( return h.Div(
h.Class("fixed top-0 left-4 md:hidden z-50"), // Always visible on mobile h.Class("fixed top-0 left-4 md:hidden z-50"),
// Always visible on mobile
h.Button( h.Button(
h.Class("p-2 text-2xl bg-neutral-700 text-white rounded-md"), // Styling the button h.Class("p-2 text-2xl bg-neutral-700 text-white rounded-md"),
// Styling the button
h.OnClick( h.OnClick(
js.EvalJs(` js.EvalJs(`
const sidebar = document.querySelector('.sidebar'); const sidebar = document.querySelector('.sidebar');
@ -131,13 +128,15 @@ func CollapseButton() *h.Element {
sidebar.classList.toggle('flex'); sidebar.classList.toggle('flex');
`), `),
), ),
h.UnsafeRaw("&#9776;"), // The icon for collapsing the sidebar h.UnsafeRaw("&#9776;"),
// The icon for collapsing the sidebar
), ),
) )
} }
func MessageInput() *h.Element { func MessageInput() *h.Element {
return h.Input("text", return h.Input(
"text",
h.Id("message-input"), h.Id("message-input"),
h.Required(), h.Required(),
h.Class("p-4 rounded-md border border-slate-200 w-full focus:outline-none focus:ring focus:ring-slate-200"), h.Class("p-4 rounded-md border border-slate-200 w-full focus:outline-none focus:ring focus:ring-slate-200"),

View file

@ -13,12 +13,14 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"), h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"),
h.Div( h.Div(
h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"), h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"),
h.H2F("htmgo chat", h.Class("text-3xl font-bold text-center mb-6")), h.H2F(
"htmgo chat",
h.Class("text-3xl font-bold text-center mb-6"),
),
h.Form( h.Form(
h.Attribute("hx-swap", "none"), h.Attribute("hx-swap", "none"),
h.PostPartial(partials.CreateOrJoinRoom), h.PostPartial(partials.CreateOrJoinRoom),
h.Class("flex flex-col gap-6"), h.Class("flex flex-col gap-6"),
// Username input at the top // Username input at the top
components.Input(components.InputProps{ components.Input(components.InputProps{
Id: "username", Id: "username",
@ -30,11 +32,9 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
h.MaxLength(15), h.MaxLength(15),
}, },
}), }),
// Single box for Create or Join a Chat Room // Single box for Create or Join a Chat Room
h.Div( h.Div(
h.Class("p-4 border border-gray-300 rounded-md flex flex-col gap-6"), h.Class("p-4 border border-gray-300 rounded-md flex flex-col gap-6"),
// Create New Chat Room input // Create New Chat Room input
components.Input(components.InputProps{ components.Input(components.InputProps{
Name: "new-chat-room", Name: "new-chat-room",
@ -45,15 +45,20 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
h.MaxLength(20), h.MaxLength(20),
}, },
}), }),
// OR divider // OR divider
h.Div( h.Div(
h.Class("flex items-center justify-center gap-4"), h.Class("flex items-center justify-center gap-4"),
h.Div(h.Class("border-t border-gray-300 flex-grow")), h.Div(
h.P(h.Text("OR"), h.Class("text-gray-500")), h.Class("border-t border-gray-300 flex-grow"),
h.Div(h.Class("border-t border-gray-300 flex-grow")), ),
h.P(
h.Text("OR"),
h.Class("text-gray-500"),
),
h.Div(
h.Class("border-t border-gray-300 flex-grow"),
),
), ),
// Join Chat Room input // Join Chat Room input
components.Input(components.InputProps{ components.Input(components.InputProps{
Id: "join-chat-room", Id: "join-chat-room",
@ -67,10 +72,8 @@ func ChatAppFirstScreen(ctx *h.RequestContext) *h.Page {
}, },
}), }),
), ),
// Error message // Error message
components.FormError(""), components.FormError(""),
// Submit button at the bottom // Submit button at the bottom
components.PrimaryButton(components.ButtonProps{ components.PrimaryButton(components.ButtonProps{
Type: "submit", Type: "submit",

View file

@ -13,7 +13,12 @@ import (
func StoryComments(ctx *h.RequestContext) *h.Partial { func StoryComments(ctx *h.RequestContext) *h.Partial {
return h.NewPartial( return h.NewPartial(
h.Fragment( h.Fragment(
h.OobSwap(ctx, h.Div(h.Id("comments-loader"))), h.OobSwap(
ctx,
h.Div(
h.Id("comments-loader"),
),
),
h.Div( h.Div(
h.Class("flex flex-col gap-3 prose max-w-none"), h.Class("flex flex-col gap-3 prose max-w-none"),
CachedStoryComments(news.MustItemId(ctx)), CachedStoryComments(news.MustItemId(ctx)),
@ -57,9 +62,15 @@ func Comment(item news.Comment, nesting int) *h.Element {
"border-b border-gray-200": nesting == 0, "border-b border-gray-200": nesting == 0,
"border-l border-gray-200": nesting > 0, "border-l border-gray-200": nesting > 0,
}), }),
h.If(nesting > 0, h.Attribute("style", fmt.Sprintf("margin-left: %dpx", (nesting-1)*15))), h.If(
nesting > 0,
h.Attribute("style", fmt.Sprintf("margin-left: %dpx", (nesting-1)*15)),
),
h.Div( h.Div(
h.If(nesting > 0, h.Class("pl-4")), h.If(
nesting > 0,
h.Class("pl-4"),
),
h.Div( h.Div(
h.Class("flex gap-1 items-center"), h.Class("flex gap-1 items-center"),
h.Div( h.Div(
@ -77,12 +88,15 @@ func Comment(item news.Comment, nesting int) *h.Element {
h.UnsafeRaw(strings.TrimSpace(item.Text)), h.UnsafeRaw(strings.TrimSpace(item.Text)),
), ),
), ),
h.If(len(children) > 0, h.List( h.If(
len(children) > 0,
h.List(
children, func(child news.Comment, index int) *h.Element { children, func(child news.Comment, index int) *h.Element {
return h.Div( return h.Div(
Comment(child, nesting+1), Comment(child, nesting+1),
) )
}, },
)), ),
),
) )
} }

View file

@ -57,13 +57,18 @@ func StorySidebar(ctx *h.RequestContext) *h.Partial {
page := parse.MustParseInt(pageRaw, 0) page := parse.MustParseInt(pageRaw, 0)
fetchMorePath := h.GetPartialPathWithQs(StorySidebar, h.NewQs("mode", "infinite", "page", fmt.Sprintf("%d", page+1), "category", category)) fetchMorePath := h.GetPartialPathWithQs(
StorySidebar,
h.NewQs("mode", "infinite", "page", fmt.Sprintf("%d", page+1), "category", category),
)
list := CachedStoryList(category, page, 50, fetchMorePath) list := CachedStoryList(category, page, 50, fetchMorePath)
body := h.Aside( body := h.Aside(
h.Id("story-sidebar"), h.Id("story-sidebar"),
h.JoinExtensions(h.TriggerChildren()), h.JoinExtensions(
h.TriggerChildren(),
),
h.Class("sticky top-0 h-screen p-1 bg-gray-100 overflow-y-auto max-w-80 min-w-80"), h.Class("sticky top-0 h-screen p-1 bg-gray-100 overflow-y-auto max-w-80 min-w-80"),
h.Div( h.Div(
h.Class("flex flex-col gap-1"), h.Class("flex flex-col gap-1"),
@ -99,7 +104,9 @@ func SidebarTitle(defaultCategory string) *h.Element {
h.Text("Hacker News"), h.Text("Hacker News"),
), ),
h.Div( h.Div(
h.OnLoad(h.EvalJs(ScrollJs)), h.OnLoad(
h.EvalJs(ScrollJs),
),
h.Class("scroll-container mt-2 flex gap-1 no-scrollbar overflow-y-hidden whitespace-nowrap overflow-x-auto"), h.Class("scroll-container mt-2 flex gap-1 no-scrollbar overflow-y-hidden whitespace-nowrap overflow-x-auto"),
h.List(news.Categories, func(item news.Category, index int) *h.Element { h.List(news.Categories, func(item news.Category, index int) *h.Element {
return CategoryBadge(defaultCategory, item) return CategoryBadge(defaultCategory, item)
@ -114,7 +121,13 @@ func CategoryBadge(defaultCategory string, category news.Category) *h.Element {
category.Name, category.Name,
selected, selected,
h.Attribute("hx-swap", "none"), h.Attribute("hx-swap", "none"),
h.If(!selected, h.PostPartialOnClickQs(StorySidebar, h.NewQs("category", category.Path))), h.If(
!selected,
h.PostPartialOnClickQs(
StorySidebar,
h.NewQs("category", category.Path),
),
),
) )
} }

View file

@ -24,14 +24,16 @@ func UserProfilePage(u db.User) *h.Element {
return h.Div( return h.Div(
h.Class("flex flex-col gap-6 items-center pt-10 min-h-screen bg-neutral-100"), h.Class("flex flex-col gap-6 items-center pt-10 min-h-screen bg-neutral-100"),
h.H3F("User Profile", h.Class("text-2xl font-bold")), h.H3F(
"User Profile",
h.Class("text-2xl font-bold"),
),
h.Pf("Welcome, %s!", u.Email), h.Pf("Welcome, %s!", u.Email),
h.Form( h.Form(
h.Attribute("hx-swap", "none"), h.Attribute("hx-swap", "none"),
h.PostPartial(partials.UpdateProfile), h.PostPartial(partials.UpdateProfile),
h.TriggerChildren(), h.TriggerChildren(),
h.Class("flex flex-col gap-4 w-full max-w-md p-6 bg-white rounded-md shadow-md"), h.Class("flex flex-col gap-4 w-full max-w-md p-6 bg-white rounded-md shadow-md"),
ui.Input(ui.InputProps{ ui.Input(ui.InputProps{
Id: "email", Id: "email",
Name: "email", Name: "email",
@ -42,26 +44,22 @@ func UserProfilePage(u db.User) *h.Element {
h.Disabled(), h.Disabled(),
}, },
}), }),
ui.Input(ui.InputProps{ ui.Input(ui.InputProps{
Name: "birth-date", Name: "birth-date",
Label: "Birth Date", Label: "Birth Date",
DefaultValue: user.GetMetaKey(meta, "birthDate"), DefaultValue: user.GetMetaKey(meta, "birthDate"),
Type: "date", Type: "date",
}), }),
ui.Input(ui.InputProps{ ui.Input(ui.InputProps{
Name: "favorite-color", Name: "favorite-color",
Label: "Favorite Color", Label: "Favorite Color",
DefaultValue: user.GetMetaKey(meta, "favoriteColor"), DefaultValue: user.GetMetaKey(meta, "favoriteColor"),
}), }),
ui.Input(ui.InputProps{ ui.Input(ui.InputProps{
Name: "occupation", Name: "occupation",
Label: "Occupation", Label: "Occupation",
DefaultValue: user.GetMetaKey(meta, "occupation"), DefaultValue: user.GetMetaKey(meta, "occupation"),
}), }),
ui.FormError(""), ui.FormError(""),
ui.SubmitButton("Save Changes"), ui.SubmitButton("Save Changes"),
), ),

View file

@ -6,7 +6,10 @@ func FormError(error string) *h.Element {
return h.Div( return h.Div(
h.Id("form-error"), h.Id("form-error"),
h.Text(error), h.Text(error),
h.If(error != "", h.Class("p-4 bg-rose-400 text-white rounded")), h.If(
error != "",
h.Class("p-4 bg-rose-400 text-white rounded"),
),
) )
} }

View file

@ -19,11 +19,14 @@ type InputProps struct {
} }
func Input(props InputProps) *h.Element { func Input(props InputProps) *h.Element {
validation := h.If(props.ValidationPath != "", h.Children( validation := h.If(
props.ValidationPath != "",
h.Children(
h.Post(props.ValidationPath, hx.BlurEvent), h.Post(props.ValidationPath, hx.BlurEvent),
h.Attribute("hx-swap", "innerHTML transition:true"), h.Attribute("hx-swap", "innerHTML transition:true"),
h.Attribute("hx-target", "next div"), h.Attribute("hx-target", "next div"),
)) ),
)
if props.Type == "" { if props.Type == "" {
props.Type = "text" props.Type = "text"
@ -32,18 +35,41 @@ func Input(props InputProps) *h.Element {
input := h.Input( input := h.Input(
props.Type, props.Type,
h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"), h.Class("border p-2 rounded focus:outline-none focus:ring focus:ring-slate-800"),
h.If(props.Name != "", h.Name(props.Name)), h.If(
h.If(props.Children != nil, h.Children(props.Children...)), props.Name != "",
h.If(props.Required, h.Required()), h.Name(props.Name),
h.If(props.Placeholder != "", h.Placeholder(props.Placeholder)), ),
h.If(props.DefaultValue != "", h.Attribute("value", props.DefaultValue)), h.If(
props.Children != nil,
h.Children(props.Children...),
),
h.If(
props.Required,
h.Required(),
),
h.If(
props.Placeholder != "",
h.Placeholder(props.Placeholder),
),
h.If(
props.DefaultValue != "",
h.Attribute("value", props.DefaultValue),
),
validation, validation,
) )
wrapped := h.Div( wrapped := h.Div(
h.If(props.Id != "", h.Id(props.Id)), h.If(
props.Id != "",
h.Id(props.Id),
),
h.Class("flex flex-col gap-1"), h.Class("flex flex-col gap-1"),
h.If(props.Label != "", h.Label(h.Text(props.Label))), h.If(
props.Label != "",
h.Label(
h.Text(props.Label),
),
),
input, input,
h.Div( h.Div(
h.Id(props.Id+"-error"), h.Id(props.Id+"-error"),

View file

@ -16,7 +16,10 @@ func CenteredForm(props CenteredFormProps) *h.Element {
h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"), h.Class("flex flex-col items-center justify-center min-h-screen bg-neutral-100"),
h.Div( h.Div(
h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"), h.Class("bg-white p-8 rounded-lg shadow-lg w-full max-w-md"),
h.H2F(props.Title, h.Class("text-3xl font-bold text-center mb-6")), h.H2F(
props.Title,
h.Class("text-3xl font-bold text-center mb-6"),
),
h.Form( h.Form(
h.TriggerChildren(), h.TriggerChildren(),
h.Post(props.PostUrl), h.Post(props.PostUrl),

View file

@ -10,7 +10,10 @@ import (
func TaskListPage(ctx *h.RequestContext) *h.Page { func TaskListPage(ctx *h.RequestContext) *h.Page {
title := h.Div( title := h.Div(
h.H1(h.Class("text-7xl font-extralight text-rose-500 tracking-wide"), h.Text("todos")), h.H1(
h.Class("text-7xl font-extralight text-rose-500 tracking-wide"),
h.Text("todos"),
),
) )
return h.NewPage(base.RootPage( return h.NewPage(base.RootPage(
@ -21,7 +24,9 @@ func TaskListPage(ctx *h.RequestContext) *h.Page {
title, title,
task.Card(ctx), task.Card(ctx),
h.Children( h.Children(
h.Div(h.Text("Double-click to edit a todo")), h.Div(
h.Text("Double-click to edit a todo"),
),
), ),
), ),
), ),

View file

@ -58,7 +58,9 @@ func Input(list []*ent.Task) *h.Element {
h.Name("name"), h.Name("name"),
h.Class("pl-12 text-xl p-4 w-full outline-none focus:outline-2 focus:outline-rose-400"), h.Class("pl-12 text-xl p-4 w-full outline-none focus:outline-2 focus:outline-rose-400"),
h.Placeholder("What needs to be done?"), h.Placeholder("What needs to be done?"),
h.Post(h.GetPartialPath(Create)), h.Post(
h.GetPartialPath(Create),
),
h.HxTrigger(hx.OnEvent(hx.TriggerKeyUpEnter)), h.HxTrigger(hx.OnEvent(hx.TriggerKeyUpEnter)),
), ),
CompleteAllIcon(list), CompleteAllIcon(list),
@ -66,23 +68,34 @@ func Input(list []*ent.Task) *h.Element {
} }
func CompleteAllIcon(list []*ent.Task) *h.Element { func CompleteAllIcon(list []*ent.Task) *h.Element {
notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool { notCompletedCount := len(
h.Filter(list, func(item *ent.Task) bool {
return item.CompletedAt == nil return item.CompletedAt == nil
})) }),
)
return h.Div( return h.Div(
h.ClassX("absolute top-1 left-5 p-2 rotate-90 text-3xl cursor-pointer", map[string]bool{ h.ClassX("absolute top-1 left-5 p-2 rotate-90 text-3xl cursor-pointer", map[string]bool{
"text-slate-400": notCompletedCount > 0, "text-slate-400": notCompletedCount > 0,
}), h.UnsafeRaw("&#x203A;"), }),
h.PostPartialWithQs(CompleteAll, h.NewQs("complete", h.Ternary(notCompletedCount > 0, "true", "false"))), h.UnsafeRaw("&#x203A;"),
h.PostPartialWithQs(
CompleteAll,
h.NewQs(
"complete",
h.Ternary(notCompletedCount > 0, "true", "false"),
),
),
) )
} }
func Footer(list []*ent.Task, activeTab Tab) *h.Element { func Footer(list []*ent.Task, activeTab Tab) *h.Element {
notCompletedCount := len(h.Filter(list, func(item *ent.Task) bool { notCompletedCount := len(
h.Filter(list, func(item *ent.Task) bool {
return item.CompletedAt == nil return item.CompletedAt == nil
})) }),
)
tabs := []Tab{TabAll, TabActive, TabComplete} tabs := []Tab{TabAll, TabActive, TabComplete}
@ -96,7 +109,12 @@ func Footer(list []*ent.Task, activeTab Tab) *h.Element {
h.Class("flex items-center gap-4"), h.Class("flex items-center gap-4"),
h.List(tabs, func(tab Tab, index int) *h.Element { h.List(tabs, func(tab Tab, index int) *h.Element {
return h.P( return h.P(
h.PostOnClick(h.GetPartialPathWithQs(ChangeTab, h.NewQs("tab", tab))), h.PostOnClick(
h.GetPartialPathWithQs(
ChangeTab,
h.NewQs("tab", tab),
),
),
h.ClassX("cursor-pointer px-2 py-1 rounded", map[string]bool{ h.ClassX("cursor-pointer px-2 py-1 rounded", map[string]bool{
"border border-rose-600": activeTab == tab, "border border-rose-600": activeTab == tab,
}), }),
@ -139,12 +157,14 @@ func Task(task *ent.Task, editing bool) *h.Element {
"border border-b-slate-100": !editing, "border border-b-slate-100": !editing,
}), }),
CompleteIcon(task), CompleteIcon(task),
h.IfElse(editing, h.IfElse(
editing,
h.Div( h.Div(
h.Class("flex-1 h-full"), h.Class("flex-1 h-full"),
h.Form( h.Form(
h.Class("h-full"), h.Class("h-full"),
h.Input("text", h.Input(
"text",
h.Name("task"), h.Name("task"),
h.Value(task.ID.String()), h.Value(task.ID.String()),
h.Class("hidden"), h.Class("hidden"),
@ -168,30 +188,43 @@ func Task(task *ent.Task, editing bool) *h.Element {
), ),
), ),
h.P( h.P(
h.GetPartialWithQs(EditNameForm, h.NewQs("id", task.ID.String()), hx.TriggerDblClick), h.GetPartialWithQs(
EditNameForm,
h.NewQs("id", task.ID.String()),
hx.TriggerDblClick,
),
h.ClassX("text-xl break-all text-wrap truncate", map[string]bool{ h.ClassX("text-xl break-all text-wrap truncate", map[string]bool{
"line-through text-slate-400": task.CompletedAt != nil, "line-through text-slate-400": task.CompletedAt != nil,
}), }),
h.Text(task.Name), h.Text(task.Name),
)), ),
),
) )
} }
func CompleteIcon(task *ent.Task) *h.Element { func CompleteIcon(task *ent.Task) *h.Element {
return h.Div( return h.Div(
h.HxTrigger(hx.OnClick()), h.HxTrigger(hx.OnClick()),
h.Post(h.GetPartialPathWithQs(ToggleCompleted, h.NewQs("id", task.ID.String()))), h.Post(
h.GetPartialPathWithQs(
ToggleCompleted,
h.NewQs("id", task.ID.String()),
),
),
h.Class("flex items-center justify-center cursor-pointer"), h.Class("flex items-center justify-center cursor-pointer"),
h.Div( h.Div(
h.ClassX("w-10 h-10 border rounded-full flex items-center justify-center", map[string]bool{ h.ClassX("w-10 h-10 border rounded-full flex items-center justify-center", map[string]bool{
"border-green-500": task.CompletedAt != nil, "border-green-500": task.CompletedAt != nil,
"border-slate-400": task.CompletedAt == nil, "border-slate-400": task.CompletedAt == nil,
}), }),
h.If(task.CompletedAt != nil, h.UnsafeRaw(` h.If(
task.CompletedAt != nil,
h.UnsafeRaw(`
<svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <svg class="w-6 h-6 text-green-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path> <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"></path>
</svg> </svg>
`)), `),
),
), ),
) )
} }
@ -199,46 +232,75 @@ func CompleteIcon(task *ent.Task) *h.Element {
func UpdateName(ctx *h.RequestContext) *h.Partial { func UpdateName(ctx *h.RequestContext) *h.Partial {
id, err := uuid.Parse(ctx.FormValue("task")) id, err := uuid.Parse(ctx.FormValue("task"))
if err != nil { if err != nil {
return h.NewPartial(h.Div(h.Text("invalid id"))) return h.NewPartial(
h.Div(
h.Text("invalid id"),
),
)
} }
name := ctx.FormValue("name") name := ctx.FormValue("name")
if name == "" { if name == "" {
return h.NewPartial(h.Div(h.Text("name is required"))) return h.NewPartial(
h.Div(
h.Text("name is required"),
),
)
} }
if len(name) > 150 { if len(name) > 150 {
return h.NewPartial(h.Div(h.Text("task must be less than 150 characters"))) return h.NewPartial(
h.Div(
h.Text("task must be less than 150 characters"),
),
)
} }
service := tasks.NewService(ctx) service := tasks.NewService(ctx)
task, err := service.Get(id) task, err := service.Get(id)
if task == nil { if task == nil {
return h.NewPartial(h.Div(h.Text("task not found"))) return h.NewPartial(
h.Div(
h.Text("task not found"),
),
)
} }
task, err = service.SetName(task.ID, name) task, err = service.SetName(task.ID, name)
if err != nil { if err != nil {
return h.NewPartial(h.Div(h.Text("failed to update"))) return h.NewPartial(
h.Div(
h.Text("failed to update"),
),
)
} }
return h.NewPartial( return h.NewPartial(
h.OobSwap(ctx, Task(task, false))) h.OobSwap(ctx, Task(task, false)),
)
} }
func EditNameForm(ctx *h.RequestContext) *h.Partial { func EditNameForm(ctx *h.RequestContext) *h.Partial {
id, err := uuid.Parse(ctx.QueryParam("id")) id, err := uuid.Parse(ctx.QueryParam("id"))
if err != nil { if err != nil {
return h.NewPartial(h.Div(h.Text("invalid id"))) return h.NewPartial(
h.Div(
h.Text("invalid id"),
),
)
} }
service := tasks.NewService(ctx) service := tasks.NewService(ctx)
task, err := service.Get(id) task, err := service.Get(id)
if task == nil { if task == nil {
return h.NewPartial(h.Div(h.Text("task not found"))) return h.NewPartial(
h.Div(
h.Text("task not found"),
),
)
} }
return h.NewPartial( return h.NewPartial(
@ -249,21 +311,36 @@ func EditNameForm(ctx *h.RequestContext) *h.Partial {
func ToggleCompleted(ctx *h.RequestContext) *h.Partial { func ToggleCompleted(ctx *h.RequestContext) *h.Partial {
id, err := uuid.Parse(ctx.QueryParam("id")) id, err := uuid.Parse(ctx.QueryParam("id"))
if err != nil { if err != nil {
return h.NewPartial(h.Div(h.Text("invalid id"))) return h.NewPartial(
h.Div(
h.Text("invalid id"),
),
)
} }
service := tasks.NewService(ctx) service := tasks.NewService(ctx)
task, err := service.Get(id) task, err := service.Get(id)
if task == nil { if task == nil {
return h.NewPartial(h.Div(h.Text("task not found"))) return h.NewPartial(
h.Div(
h.Text("task not found"),
),
)
} }
task, err = service.SetCompleted(task.ID, h. task, err = service.SetCompleted(
Ternary(task.CompletedAt == nil, true, false)) task.ID,
h.
Ternary(task.CompletedAt == nil, true, false),
)
if err != nil { if err != nil {
return h.NewPartial(h.Div(h.Text("failed to update"))) return h.NewPartial(
h.Div(
h.Text("failed to update"),
),
)
} }
list, _ := service.List() list, _ := service.List()
@ -282,7 +359,9 @@ func CompleteAll(ctx *h.RequestContext) *h.Partial {
list, _ := service.List() list, _ := service.List()
return h.NewPartial(h.OobSwap(ctx, CardBody(list, getActiveTab(ctx)))) return h.NewPartial(
h.OobSwap(ctx, CardBody(list, getActiveTab(ctx))),
)
} }
func ClearCompleted(ctx *h.RequestContext) *h.Partial { func ClearCompleted(ctx *h.RequestContext) *h.Partial {
@ -291,7 +370,9 @@ func ClearCompleted(ctx *h.RequestContext) *h.Partial {
list, _ := service.List() list, _ := service.List()
return h.NewPartial(h.OobSwap(ctx, CardBody(list, getActiveTab(ctx)))) return h.NewPartial(
h.OobSwap(ctx, CardBody(list, getActiveTab(ctx))),
)
} }
func Create(ctx *h.RequestContext) *h.Partial { func Create(ctx *h.RequestContext) *h.Partial {
@ -300,7 +381,9 @@ func Create(ctx *h.RequestContext) *h.Partial {
if len(name) > 150 { if len(name) > 150 {
return h.NewPartial( return h.NewPartial(
h.Div( h.Div(
h.HxOnLoad(js.Alert("Task must be less than 150 characters")), h.HxOnLoad(
js.Alert("Task must be less than 150 characters"),
),
), ),
) )
} }
@ -312,7 +395,9 @@ func Create(ctx *h.RequestContext) *h.Partial {
if list != nil && len(list) >= 100 { if list != nil && len(list) >= 100 {
return h.NewPartial( return h.NewPartial(
h.Div( h.Div(
h.HxOnLoad(js.Alert("There are too many tasks, please complete and clear some.")), h.HxOnLoad(
js.Alert("There are too many tasks, please complete and clear some."),
),
), ),
) )
} }
@ -322,7 +407,11 @@ func Create(ctx *h.RequestContext) *h.Partial {
}) })
if err != nil { if err != nil {
return h.NewPartial(h.Div(h.Text("failed to create"))) return h.NewPartial(
h.Div(
h.Text("failed to create"),
),
)
} }
list, err = service.List() list, err = service.List()
@ -338,8 +427,12 @@ func ChangeTab(ctx *h.RequestContext) *h.Partial {
tab := ctx.QueryParam("tab") tab := ctx.QueryParam("tab")
return h.SwapManyPartialWithHeaders(ctx, return h.SwapManyPartialWithHeaders(
h.PushQsHeader(ctx, h.NewQs("tab", tab)), ctx,
h.PushQsHeader(
ctx,
h.NewQs("tab", tab),
),
List(list, tab), List(list, tab),
Footer(list, tab), Footer(list, tab),
) )

View file

@ -13,7 +13,9 @@ func RootPage(ctx *h.RequestContext, children ...h.Ren) *h.Element {
description := "build simple and scalable systems with go + htmx" description := "build simple and scalable systems with go + htmx"
return h.Html( return h.Html(
h.HxExtension(h.BaseExtensions()), h.HxExtension(
h.BaseExtensions(),
),
h.Head( h.Head(
h.Meta("viewport", "width=device-width, initial-scale=1"), h.Meta("viewport", "width=device-width, initial-scale=1"),
h.Meta("title", title), h.Meta("title", title),
@ -54,7 +56,8 @@ func RootPage(ctx *h.RequestContext, children ...h.Ren) *h.Element {
} }
func PageWithNav(ctx *h.RequestContext, children ...h.Ren) *h.Element { func PageWithNav(ctx *h.RequestContext, children ...h.Ren) *h.Element {
return RootPage(ctx, return RootPage(
ctx,
h.Fragment( h.Fragment(
partials.NavBar(ctx, partials.NavBarProps{ partials.NavBar(ctx, partials.NavBarProps{
Expanded: false, Expanded: false,

View file

@ -43,7 +43,8 @@ func DocsPage(ctx *h.RequestContext) *h.Page {
MarkdownContent(ctx, page.FilePath, anchor), MarkdownContent(ctx, page.FilePath, anchor),
h.Div( h.Div(
h.Class("ml-4 pl-1 mt-2 bg-rose-200"), h.Class("ml-4 pl-1 mt-2 bg-rose-200"),
h.If(anchor == "core-concepts-partials", h.If(
anchor == "core-concepts-partials",
h.GetPartial(partials.CurrentTimePartial, "load, every 1s"), h.GetPartial(partials.CurrentTimePartial, "load, every 1s"),
), ),
), ),

View file

@ -57,7 +57,9 @@ var examples = []Example{
func ExamplesPage(ctx *h.RequestContext) *h.Page { func ExamplesPage(ctx *h.RequestContext) *h.Page {
return h.NewPage( return h.NewPage(
base.PageWithNav(ctx, h.Div( base.PageWithNav(
ctx,
h.Div(
h.Class("flex items-center justify-center"), h.Class("flex items-center justify-center"),
h.Div( h.Div(
h.Class("w-full px-4 flex flex-col prose max-w-[95vw] md:max-w-3xl mt-6"), h.Class("w-full px-4 flex flex-col prose max-w-[95vw] md:max-w-3xl mt-6"),
@ -81,7 +83,8 @@ func ExamplesPage(ctx *h.RequestContext) *h.Page {
ExampleCards(), ExampleCards(),
), ),
), ),
)), ),
),
), ),
) )
} }
@ -91,14 +94,16 @@ func ExampleCards() *h.Element {
h.Class("prose-h2:my-1 prose-img:my-1 grid grid-cols-1 gap-6 text-center pb-8"), h.Class("prose-h2:my-1 prose-img:my-1 grid grid-cols-1 gap-6 text-center pb-8"),
h.List(examples, func(example Example, index int) *h.Element { h.List(examples, func(example Example, index int) *h.Element {
return h.Div( return h.Div(
h.Class("border border-gray-200 shadow-sm rounded-md px-4 pb-4 bg-neutral-100"), // Removed specific width, handled by grid h.Class("border border-gray-200 shadow-sm rounded-md px-4 pb-4 bg-neutral-100"),
h.Div( h.Div(
h.Class("flex flex-col gap-1 mt-4"), h.Class("flex flex-col gap-1 mt-4"),
h.H2( h.H2(
h.Class("text-lg text-center mb-1"), // Reduced margin at the bottom of the title h.Class("text-lg text-center mb-1"),
h.Text(example.Title), h.Text(example.Title),
), ),
h.If(example.Image != "", h.Div( h.If(
example.Image != "",
h.Div(
h.A( h.A(
h.Href(example.Demo), h.Href(example.Demo),
h.Class("not-prose"), h.Class("not-prose"),
@ -106,11 +111,15 @@ func ExampleCards() *h.Element {
h.Src(example.Image), h.Src(example.Image),
h.Class("w-[75%] rounded-md mx-auto"), h.Class("w-[75%] rounded-md mx-auto"),
), ),
), // Ensures image is centered within the card ),
)), ),
h.If(example.Description != "", h.Div( ),
h.If(
example.Description != "",
h.Div(
h.Pf(example.Description), h.Pf(example.Description),
)), ),
),
h.Div( h.Div(
h.Div( h.Div(
h.Class("flex gap-2 justify-center mt-2"), h.Class("flex gap-2 justify-center mt-2"),

View file

@ -9,21 +9,29 @@ import (
) )
func Form(ctx *h.RequestContext) *h.Page { func Form(ctx *h.RequestContext) *h.Page {
return h.NewPage(base.RootPage(ctx, return h.NewPage(base.RootPage(
ctx,
h.Div( h.Div(
h.Class("flex flex-col items-center justify-center p-4 gap-6"), h.Class("flex flex-col items-center justify-center p-4 gap-6"),
h.H2F("Form submission with loading state example", h.Class("text-2xl font-bold")), h.H2F(
"Form submission with loading state example",
h.Class("text-2xl font-bold"),
),
h.Form( h.Form(
h.TriggerChildren(), h.TriggerChildren(),
h.PostPartial(partials.SubmitForm), h.PostPartial(partials.SubmitForm),
h.Class("flex flex-col gap-2"), h.Class("flex flex-col gap-2"),
h.LabelFor("name", "Your Name"), h.LabelFor("name", "Your Name"),
h.Input("text", h.Input(
"text",
h.Required(), h.Required(),
h.Class("p-4 rounded-md border border-slate-200"), h.Class("p-4 rounded-md border border-slate-200"),
h.Name("name"), h.Name("name"),
h.Placeholder("Name"), h.Placeholder("Name"),
h.OnEvent(hx.KeyDownEvent, js.SubmitFormOnEnter()), h.OnEvent(
hx.KeyDownEvent,
js.SubmitFormOnEnter(),
),
), ),
SubmitButton(), SubmitButton(),
), ),

View file

@ -8,7 +8,8 @@ import (
func HtmlToGoPage(ctx *h.RequestContext) *h.Page { func HtmlToGoPage(ctx *h.RequestContext) *h.Page {
return h.NewPage( return h.NewPage(
base.PageWithNav(ctx, base.PageWithNav(
ctx,
h.Div( h.Div(
h.Class("flex flex-col h-screen items-center justify-center w-full pt-6"), h.Class("flex flex-col h-screen items-center justify-center w-full pt-6"),
h.H3( h.H3(

View file

@ -7,14 +7,19 @@ import (
func IndexPage(ctx *h.RequestContext) *h.Page { func IndexPage(ctx *h.RequestContext) *h.Page {
return h.NewPage( return h.NewPage(
base.PageWithNav(ctx, h.Div( base.PageWithNav(
ctx,
h.Div(
h.Class("flex items-center justify-center"), h.Class("flex items-center justify-center"),
h.Div( h.Div(
h.Class("w-full px-4 flex flex-col prose md:max-w-3xl mt-6 mx-auto"), h.Class("w-full px-4 flex flex-col prose md:max-w-3xl mt-6 mx-auto"),
h.Div( h.Div(
h.Class("flex flex-col mb-6 md:mb-0 md:flex-row justify-between items-center"), h.Class("flex flex-col mb-6 md:mb-0 md:flex-row justify-between items-center"),
h.Div( h.Div(
h.H1F("htmgo", h.Class("text-center md:text-left")), h.H1F(
"htmgo",
h.Class("text-center md:text-left"),
),
h.H3F( h.H3F(
"build simple and scalable systems with %s", "build simple and scalable systems with %s",
"go + htmx", "go + htmx",
@ -37,7 +42,8 @@ func IndexPage(ctx *h.RequestContext) *h.Page {
MarkdownPage(ctx, "md/index.md", ""), MarkdownPage(ctx, "md/index.md", ""),
), ),
), ),
)), ),
),
), ),
) )
} }

View file

@ -20,7 +20,10 @@ func MarkdownContent(ctx *h.RequestContext, path string, id string) *h.Element {
embeddedMd := ctx.Get("embeddedMarkdown").(fs.FS) embeddedMd := ctx.Get("embeddedMarkdown").(fs.FS)
renderer := service.Get[markdown.Renderer](ctx.ServiceLocator()) renderer := service.Get[markdown.Renderer](ctx.ServiceLocator())
return h.Div( return h.Div(
h.If(id != "", h.Id(id)), h.If(
id != "",
h.Id(id),
),
h.Div( h.Div(
h.Class("w-full flex flex-col prose max-w-md md:max-w-xl lg:max-w-3xl prose-code:text-black prose-p:my-1 prose:p-0 prose-li:m-0 prose-ul:m-0 prose-ol:m-0"), h.Class("w-full flex flex-col prose max-w-md md:max-w-xl lg:max-w-3xl prose-code:text-black prose-p:my-1 prose:p-0 prose-li:m-0 prose-ul:m-0 prose-ol:m-0"),
h.UnsafeRaw(renderer.RenderFile(path, embeddedMd)), h.UnsafeRaw(renderer.RenderFile(path, embeddedMd)),

59
htmgo-site/pages/test.go Normal file
View file

@ -0,0 +1,59 @@
package pages
import (
"fmt"
"github.com/maddalax/htmgo/framework/h"
"htmgo-site/pages/base"
)
func TestFormatPage(ctx *h.RequestContext) *h.Page {
return h.NewPage(
base.RootPage(
ctx,
h.Div(
h.P(
h.Class("hello"),
h.Details(
h.Summary(
h.Text("Summary"),
),
h.Text("Details"),
),
h.Id("hi"),
),
),
),
)
}
func notPage() int {
test := 1
fmt.Printf("test: %d\n", test)
return test
}
func TestOtherPage(ctx *h.RequestContext) *h.Page {
return h.NewPage(
base.RootPage(
ctx,
h.Div(
h.Id("test"),
h.Details(
h.Summary(
h.Text("Summary"),
),
h.Text("Details"),
),
h.Class("flex flex-col gap-2 bg-white h-full"),
h.Id("test"),
h.Details(
h.Summary(
h.Text("Summary"),
),
h.Text("Details"),
),
),
),
)
}

View file

@ -8,6 +8,8 @@ import (
func SubmitForm(ctx *h.RequestContext) *h.Partial { func SubmitForm(ctx *h.RequestContext) *h.Partial {
time.Sleep(time.Second * 3) time.Sleep(time.Second * 3)
return h.NewPartial( return h.NewPartial(
h.Div(h.Text("Form submitted")), h.Div(
h.Text("Form submitted"),
),
) )
} }

View file

@ -41,12 +41,13 @@ func GoOutput(content string) *h.Element {
h.Id("go-output-content"), h.Id("go-output-content"),
h.UnsafeRaw(content), h.UnsafeRaw(content),
), ),
h.If(content != "", h.Div( h.If(
content != "",
h.Div(
h.Class("absolute top-0 right-0 p-2 bg-slate-800 text-white rounded-bl-md cursor-pointer"), h.Class("absolute top-0 right-0 p-2 bg-slate-800 text-white rounded-bl-md cursor-pointer"),
h.Text("Copy"), h.Text("Copy"),
// language=JavaScript h.OnClick(
h.OnClick(js.EvalJs(` js.EvalJs(`
if(!navigator.clipboard) { if(!navigator.clipboard) {
alert("Clipboard API not supported"); alert("Clipboard API not supported");
return; return;
@ -57,8 +58,10 @@ func GoOutput(content string) *h.Element {
setTimeout(() => { setTimeout(() => {
self.innerText = "Copy"; self.innerText = "Copy";
}, 1000); }, 1000);
`)), `),
)), ),
),
),
), ),
) )
} }

View file

@ -58,29 +58,26 @@ func Star(ctx *h.RequestContext) *h.Element {
h.Class("w-4 h-4 -mt-0.5 mr-0.5 stroke-current text-white"), h.Class("w-4 h-4 -mt-0.5 mr-0.5 stroke-current text-white"),
h.Attribute("xmlns", "http://www.w3.org/2000/svg"), h.Attribute("xmlns", "http://www.w3.org/2000/svg"),
h.Attribute("viewBox", "0 0 24 24"), h.Attribute("viewBox", "0 0 24 24"),
h.Attribute("fill", "none"), // No fill h.Attribute("fill", "none"),
h.Attribute("stroke", "currentColor"), // Apply stroke h.Attribute("stroke", "currentColor"),
h.Attribute("stroke-width", "2"), // Stroke width h.Attribute("stroke-width", "2"),
h.Path( h.Path(
h.D("M12 17.27l5.18 3.05-1.64-5.68 4.46-3.87-5.88-.5L12 3.5l-2.12 6.77-5.88.5 4.46 3.87-1.64 5.68L12 17.27z"), h.D("M12 17.27l5.18 3.05-1.64-5.68 4.46-3.87-5.88-.5L12 3.5l-2.12 6.77-5.88.5 4.46 3.87-1.64 5.68L12 17.27z"),
), ),
), ),
h.Text("Star"), h.Text("Star"),
), ),
h.If(count > 0, h.Div( h.If(
count > 0,
h.Div(
h.Class("flex items-center px-3 py-1 bg-black text-white text-sm font-semibold"), h.Class("flex items-center px-3 py-1 bg-black text-white text-sm font-semibold"),
h.Pf("%d", count), h.Pf("%d", count),
)), ),
),
) )
} }
func NavBar(ctx *h.RequestContext, props NavBarProps) *h.Element { func NavBar(ctx *h.RequestContext, props NavBarProps) *h.Element {
//prelease := h.If(props.ShowPreRelease, h.A(
// h.Class("bg-blue-200 text-blue-700 text-center p-2 flex items-center justify-center"),
// h.Href("https://github.com/maddalax/htmgo/issues"),
// h.Attribute("target", "_blank"),
// h.Text("htmgo."),
//))
desktopNav := h.Nav( desktopNav := h.Nav(
h.Class("hidden sm:block bg-neutral-100 border border-b-slate-300 p-4 md:p-3 max-h-[100vh - 9rem] overflow-y-auto"), h.Class("hidden sm:block bg-neutral-100 border border-b-slate-300 p-4 md:p-3 max-h-[100vh - 9rem] overflow-y-auto"),
@ -94,7 +91,8 @@ func NavBar(ctx *h.RequestContext, props NavBarProps) *h.Element {
h.Class("text-2xl"), h.Class("text-2xl"),
h.Href("/"), h.Href("/"),
h.Text("htmgo"), h.Text("htmgo"),
)), ),
),
h.Div( h.Div(
h.Id("search-container"), h.Id("search-container"),
), ),
@ -118,7 +116,6 @@ func NavBar(ctx *h.RequestContext, props NavBarProps) *h.Element {
return h.Div( return h.Div(
h.Id("navbar"), h.Id("navbar"),
//prelease,
MobileNav(ctx, props.Expanded), MobileNav(ctx, props.Expanded),
desktopNav, desktopNav,
) )
@ -139,31 +136,41 @@ func MobileNav(ctx *h.RequestContext, expanded bool) *h.Element {
h.Class("text-2xl"), h.Class("text-2xl"),
h.Href("/"), h.Href("/"),
h.Text("htmgo"), h.Text("htmgo"),
)), ),
),
h.Div( h.Div(
h.Class("flex items-center gap-3"), h.Class("flex items-center gap-3"),
h.Div(h.Class("mt-1"), CachedStar(ctx)), h.Div(
h.Class("mt-1"),
CachedStar(ctx),
),
h.Button( h.Button(
h.Boost(), h.Boost(),
h.GetPartialWithQs( h.GetPartialWithQs(
ToggleNavbar, ToggleNavbar,
h.NewQs("expanded", h.Ternary(expanded, "false", "true"), "test", "true"), h.NewQs(
"expanded",
h.Ternary(expanded, "false", "true"),
"test",
"true",
),
"click", "click",
), ),
h.AttributePairs( h.AttributePairs(
"class", "text-2xl", "class",
"aria-expanded", h.Ternary(expanded, "true", "false"), "text-2xl",
"aria-expanded",
h.Ternary(expanded, "true", "false"),
), ),
h.Class("text-2xl"), h.Class("text-2xl"),
h.UnsafeRaw("&#9776;"), h.UnsafeRaw("&#9776;"),
), ),
), ),
), ),
), ),
h.If(expanded, h.Div( h.If(
expanded,
h.Div(
h.Class("mt-2 ml-2 flex flex-col gap-2"), h.Class("mt-2 ml-2 flex flex-col gap-2"),
h.List(navItems, func(item NavItem, index int) *h.Element { h.List(navItems, func(item NavItem, index int) *h.Element {
return h.Div( return h.Div(
@ -176,6 +183,7 @@ func MobileNav(ctx *h.RequestContext, expanded bool) *h.Element {
), ),
) )
}), }),
)), ),
),
) )
} }

View file

@ -71,7 +71,10 @@ func DocSidebar(pages []*dirwalk.Page) *h.Element {
h.Class("flex flex-col gap-4"), h.Class("flex flex-col gap-4"),
h.List(grouped.Entries(), func(entry datastructures.Entry[string, []*dirwalk.Page], index int) *h.Element { h.List(grouped.Entries(), func(entry datastructures.Entry[string, []*dirwalk.Page], index int) *h.Element {
return h.Div( return h.Div(
h.P(h.Text(formatPart(entry.Key)), h.Class("text-slate-800 font-bold")), h.P(
h.Text(formatPart(entry.Key)),
h.Class("text-slate-800 font-bold"),
),
h.Div( h.Div(
h.Class("pl-4 flex flex-col"), h.Class("pl-4 flex flex-col"),
h.List(entry.Value, func(page *dirwalk.Page, index int) *h.Element { h.List(entry.Value, func(page *dirwalk.Page, index int) *h.Element {

View file

@ -9,7 +9,11 @@ func IndexPage(ctx *h.RequestContext) *h.Page {
return RootPage( return RootPage(
h.Div( h.Div(
h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"), h.Class("flex flex-col gap-4 items-center pt-24 min-h-screen bg-neutral-100"),
h.H3(h.Id("intro-text"), h.Text("hello htmgo"), h.Class("text-5xl")), h.H3(
h.Id("intro-text"),
h.Text("hello htmgo"),
h.Class("text-5xl"),
),
h.Div( h.Div(
h.Class("mt-3"), h.Class("mt-3"),
partials.CounterForm(0), partials.CounterForm(0),

View file

@ -7,7 +7,9 @@ import (
func RootPage(children ...h.Ren) *h.Page { func RootPage(children ...h.Ren) *h.Page {
return h.NewPage( return h.NewPage(
h.Html( h.Html(
h.HxExtensions(h.BaseExtensions()), h.HxExtensions(
h.BaseExtensions(),
),
h.Head( h.Head(
h.Meta("viewport", "width=device-width, initial-scale=1"), h.Meta("viewport", "width=device-width, initial-scale=1"),
h.Link("/public/favicon.ico", "icon"), h.Link("/public/favicon.ico", "icon"),

View file

@ -26,7 +26,8 @@ func CounterForm(count int) *h.Element {
h.Class("flex flex-col gap-3 items-center"), h.Class("flex flex-col gap-3 items-center"),
h.Id("counter-form"), h.Id("counter-form"),
h.PostPartial(CounterPartial), h.PostPartial(CounterPartial),
h.Input("text", h.Input(
"text",
h.Class("hidden"), h.Class("hidden"),
h.Value(count), h.Value(count),
h.Name("count"), h.Name("count"),

View file

@ -0,0 +1,139 @@
package htmltogo
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/printer"
"go/token"
"golang.org/x/tools/go/ast/astutil"
"slices"
"strings"
)
func Indent(input string) string {
fset := token.NewFileSet()
// Parse the code string into an AST
f, err := parser.ParseFile(fset, "", input, parser.ParseComments)
if err != nil {
return input
}
htmgoComponentTypes := []string{
"h.Element",
"h.Page",
"h.Partial",
"h.Ren",
}
for _, decl := range f.Decls {
switch c := decl.(type) {
case *ast.FuncDecl:
if c.Type.Results == nil || len(c.Type.Results.List) == 0 {
continue
}
returnType := c.Type.Results.List[0].Type
isHtmgoComponent := false
if v, ok := returnType.(*ast.StarExpr); ok {
if x, ok := v.X.(*ast.SelectorExpr); ok {
name := x.X.(*ast.Ident).Name
str := name + "." + x.Sel.Name
isHtmgoComponent = slices.Contains(htmgoComponentTypes, str)
}
}
if !isHtmgoComponent {
continue
}
var isHTag = func(n ast.Expr) bool {
switch argc := n.(type) {
// If the first argument is another node, add an indent
case *ast.CallExpr:
if v, ok := argc.Fun.(*ast.SelectorExpr); ok {
if v2, ok := v.X.(*ast.Ident); ok {
if v2.Name == "h" || v2.Name == "js" {
return true
}
}
}
}
return false
}
var indent = func(children []ast.Expr) []ast.Expr {
children = append(children, ast.NewIdent("INDENTME"))
return children
}
astutil.Apply(c.Body, nil, func(cursor *astutil.Cursor) bool {
switch n := cursor.Node().(type) {
case *ast.CallExpr:
newChildren := make([]ast.Expr, 0)
hasAnyHElements := false
for _, arg := range n.Args {
if isHTag(arg) {
hasAnyHElements = true
break
}
}
for i, arg := range n.Args {
if len(n.Args) == 1 && isHTag(arg) {
newChildren = indent(newChildren)
newChildren = append(newChildren, arg)
newChildren = indent(newChildren)
continue
}
if !hasAnyHElements {
newChildren = append(newChildren, arg)
continue
}
if len(n.Args) > 1 {
if i == 0 {
newChildren = indent(newChildren)
}
}
newChildren = append(newChildren, arg)
if len(n.Args) > 1 {
newChildren = indent(newChildren)
}
}
n.Args = newChildren
return true
}
return true
})
}
}
// Convert the AST node to a string
var buf bytes.Buffer
if err := printer.Fprint(&buf, fset, f); err != nil {
fmt.Println("Error printing AST:", err)
return input
}
// Output the formatted code
indented := strings.ReplaceAll(buf.String(), "INDENTME,", "\n\t\t")
indented = strings.ReplaceAll(indented, ", INDENTME", ", \n\t\t")
formatted, err := format.Source([]byte(indented))
if err != nil {
return input
}
return string(formatted)
}

View file

@ -18,5 +18,5 @@ func Parse(input []byte) []byte {
return nil return nil
} }
return []byte(formatter.Format(parsed)) return []byte(Indent(formatter.Format(parsed)))
} }

View file

@ -16,8 +16,7 @@ import (
func MyComponent() *h.Element { func MyComponent() *h.Element {
return ` + node.String() + ` return ` + node.String() + `
}`) }`)
indented := Indent(string(b)) dist, err := format.Source(b)
dist, err := format.Source([]byte(indented))
if err != nil { if err != nil {
return string(b) return string(b)
} }

View file

@ -1,58 +0,0 @@
package formatter
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/token"
"golang.org/x/tools/go/ast/astutil"
"strings"
)
func Indent(input string) string {
fset := token.NewFileSet()
// Parse the code string into an AST
f, err := parser.ParseFile(fset, "", input, 0)
if err != nil {
return input
}
component := f.Decls[1].(*ast.FuncDecl)
astutil.Apply(component.Body, nil, func(cursor *astutil.Cursor) bool {
switch n := cursor.Node().(type) {
case *ast.CallExpr:
newChildren := make([]ast.Expr, 0)
for i, arg := range n.Args {
if i == 0 {
switch arg.(type) {
// If the first argument is another node, add an indent
case *ast.CallExpr:
newChildren = append(newChildren, ast.NewIdent("INDENTME"))
}
}
newChildren = append(newChildren, arg)
newChildren = append(newChildren, ast.NewIdent("INDENTME"))
}
n.Args = newChildren
return true
}
return true
})
// Convert the AST node to a string
var buf bytes.Buffer
if err := printer.Fprint(&buf, fset, component); err != nil {
fmt.Println("Error printing AST:", err)
return input
}
// Output the formatted code
indented := strings.ReplaceAll(buf.String(), "INDENTME,", "\n\t\t")
indented = strings.ReplaceAll(indented, ", INDENTME", ", \n\t\t")
return indented
}