diff --git a/.gitignore b/.gitignore index c3c5ddc..f5550a5 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,6 @@ bench/*.pprof examples/simple/simple examples/htmx/htmx examples/todo/todo -examples/blog/blog examples/htmx-render-target/htmx-render-target-example examples/lint-misuse/lint-misuse diff --git a/examples/README.md b/examples/README.md index 21d67f8..64ffe1e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,19 +14,13 @@ Each example has its own `go.mod`. From the example directory: ```shell -# Generate templ files first (required) -templ generate -include-version=false +# Generate the .x.go files from .gsx sources first (required) +gsx generate . # Run the server (defaults to :8080) go run . ``` -Or use templ's watch mode for live-reloading during development: - -```shell -templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." -``` - You'll need: - Go 1.24+ -- `templ` CLI: `go install github.com/a-h/templ/cmd/templ@latest` (not required for `html-template/`, which uses only the standard library) +- `gsx` CLI: `go install github.com/gsxhq/gsx/cmd/gsx@latest` (not required for `html-template/`, which uses only the standard library) diff --git a/examples/blog/README.md b/examples/blog/README.md index 49f4b4e..1abfc2c 100644 --- a/examples/blog/README.md +++ b/examples/blog/README.md @@ -11,7 +11,7 @@ to Go + templ. ## Run ```sh -templ generate -include-version=false +gsx generate ./admin ./blog ./ui/components ./ui/layout go run . # open http://localhost:8080 — admin login: admin / admin ``` diff --git a/examples/blog/admin/components.gsx b/examples/blog/admin/components.gsx new file mode 100644 index 0000000..91a5a16 --- /dev/null +++ b/examples/blog/admin/components.gsx @@ -0,0 +1,145 @@ +// Admin-local gsx functions. StatsGrid, RecentPostsCard and PostsTable are +// standalone function components so the dashboard's Props+RenderTarget switch +// can refresh each widget independently with target.Is(StatsGrid) / +// target.Is(RecentPostsCard). +package admin + +import ( + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" +) + +// StatCell — capitalized (gsx components must be Capitalized; the templ name +// was lowercase `statCell`). +component StatCell(label string, value int) { +
+
{ value }
+
+ { label } +
+
+} + +component StatsGrid(stats store.Stats) { +
id} class="grid grid-cols-2 gap-3 md:grid-cols-4"> + + + + +
+} + +component RecentPostsCard(posts []store.Post) { +
id}> + + + +
+} + +component PostsTable(posts []store.Post) { +
id} + class="overflow-hidden rounded-lg border bg-white shadow-sm" + > + + + + + + + + + + + { for _, p := range posts { + + + + + + + } } + { if len(posts) == 0 { + + + + } } + +
TitleStatusCreated
+ url("id", p.ID)} + > + { p.Title } + + + { if p.Published { + + published + + } else { + + draft + + } } + + { p.CreatedAt.Format("Jan 2, 2006") } + +
url("id", p.ID)} + hx-post={postDeleteHandler{} |> url("id", p.ID)} + hx-target={PostsTable |> target} + hx-swap="outerHTML" + hx-confirm="Delete this post?" + class="inline" + > + +
+
+ No posts yet. +
+
+} diff --git a/examples/blog/admin/components.templ b/examples/blog/admin/components.templ deleted file mode 100644 index 4100748..0000000 --- a/examples/blog/admin/components.templ +++ /dev/null @@ -1,98 +0,0 @@ -// Admin-local templ functions. StatsGrid and RecentPostsCard are standalone -// function components so the dashboard's Props+RenderTarget switch can refresh -// each widget independently with target.Is(StatsGrid) / target.Is(RecentPostsCard). -package admin - -import ( - "github.com/jackielii/structpages" - "github.com/jackielii/structpages/examples/blog/store" - "github.com/jackielii/structpages/examples/blog/ui/components" -) - -templ statCell(label string, value int) { -
-
{ value }
-
{ label }
-
-} - -templ StatsGrid(stats store.Stats) { -
- @statCell("Posts", stats.Posts) - @statCell("Drafts", stats.Drafts) - @statCell("Comments", stats.Comments) - @statCell("Categories", stats.Categories) -
-} - -templ RecentPostsCard(posts []store.Post) { -
- @components.Card("Recent posts") { - - } -
-} - -templ PostsTable(posts []store.Post) { -
- - - - - - - - - - - for _, p := range posts { - - - - - - - } - if len(posts) == 0 { - - } - -
TitleStatusCreated
- { p.Title } - - if p.Published { - published - } else { - draft - } - { p.CreatedAt.Format("Jan 2, 2006") } -
- -
-
No posts yet.
-
-} diff --git a/examples/blog/admin/components.x.go b/examples/blog/admin/components.x.go new file mode 100644 index 0000000..c8d0c70 --- /dev/null +++ b/examples/blog/admin/components.x.go @@ -0,0 +1,276 @@ +// Code generated by gsx; DO NOT EDIT. + +package admin + +import ( + "context" + "io" + "strconv" + + "github.com/gsxhq/gsx" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" +) + +// StatCell — capitalized (gsx components must be Capitalized; the templ name +// was lowercase `statCell`). + +type StatCellProps struct { + Label string + Value int + Attrs gsx.Attrs +} + +//line components.gsx:14:1 +func StatCell(_gsxp StatCellProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + label := _gsxp.Label + value := _gsxp.Value + _gsxgw := gsx.W(_gsxw) +//line components.gsx:15:2 + _gsxgw.S("
") +//line components.gsx:16:3 + _gsxgw.S("
") +//line components.gsx:16:54 + _gsxgw.Text(strconv.FormatInt(int64(value), 10)) + _gsxgw.S("
") +//line components.gsx:17:3 + _gsxgw.S("
") +//line components.gsx:18:4 + _gsxgw.Text(string(label)) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +type StatsGridProps struct { + Stats store.Stats + Attrs gsx.Attrs +} + +//line components.gsx:23:1 +func StatsGrid(_gsxp StatsGridProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + stats := _gsxp.Stats + _gsxgw := gsx.W(_gsxw) +//line components.gsx:24:2 + _gsxgw.S("") +//line components.gsx:25:3 + _gsxgw.Node(ctx, StatCell(StatCellProps{Label: "Posts", Value: stats.Posts})) +//line components.gsx:26:3 + _gsxgw.Node(ctx, StatCell(StatCellProps{Label: "Drafts", Value: stats.Drafts})) +//line components.gsx:27:3 + _gsxgw.Node(ctx, StatCell(StatCellProps{Label: "Comments", Value: stats.Comments})) +//line components.gsx:28:3 + _gsxgw.Node(ctx, StatCell(StatCellProps{Label: "Categories", Value: stats.Categories})) + _gsxgw.S("") + return _gsxgw.Err() + }) +} + +type RecentPostsCardProps struct { + Posts []store.Post + Attrs gsx.Attrs +} + +//line components.gsx:32:1 +func RecentPostsCard(_gsxp RecentPostsCardProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + posts := _gsxp.Posts + _gsxgw := gsx.W(_gsxw) +//line components.gsx:33:2 + _gsxgw.S("") +//line components.gsx:34:3 + _gsxgw.Node(ctx, components.Card(components.CardProps{Title: "Recent posts", Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line components.gsx:35:4 + _gsxgw.S("") + return _gsxgw.Err() + })})) + _gsxgw.S("") + return _gsxgw.Err() + }) +} + +type PostsTableProps struct { + Posts []store.Post + Attrs gsx.Attrs +} + +//line components.gsx:67:1 +func PostsTable(_gsxp PostsTableProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + posts := _gsxp.Posts + _gsxgw := gsx.W(_gsxw) +//line components.gsx:68:2 + _gsxgw.S("") +//line components.gsx:72:3 + _gsxgw.S("") +//line components.gsx:73:4 + _gsxgw.S("") +//line components.gsx:76:5 + _gsxgw.S("") +//line components.gsx:77:6 + _gsxgw.S("") +//line components.gsx:78:6 + _gsxgw.S("") +//line components.gsx:79:6 + _gsxgw.S("") +//line components.gsx:80:6 + _gsxgw.S("") +//line components.gsx:83:4 + _gsxgw.S("") +//line components.gsx:84:5 + for _, p := range posts { +//line components.gsx:85:6 + _gsxgw.S("") +//line components.gsx:86:7 + _gsxgw.S("") +//line components.gsx:94:7 + _gsxgw.S("") +//line components.gsx:109:7 + _gsxgw.S("") +//line components.gsx:112:7 + _gsxgw.S("") + } +//line components.gsx:132:5 + if len(posts) == 0 { +//line components.gsx:133:6 + _gsxgw.S("") +//line components.gsx:134:7 + _gsxgw.S("") + } + _gsxgw.S("
TitleStatusCreated
") +//line components.gsx:87:8 + _gsxgw.S("") +//line components.gsx:91:9 + _gsxgw.Text(string(p.Title)) + _gsxgw.S("") +//line components.gsx:95:8 + if p.Published { +//line components.gsx:96:9 + _gsxgw.S("published") + } else { +//line components.gsx:102:9 + _gsxgw.S("draft") + } + _gsxgw.S("") +//line components.gsx:110:8 + _gsxgw.Text(string(p.CreatedAt.Format("Jan 2, 2006"))) + _gsxgw.S("") +//line components.gsx:113:8 + _gsxgw.S("
") +//line components.gsx:122:9 + _gsxgw.S("
No posts yet.
") + return _gsxgw.Err() + }) +} diff --git a/examples/blog/admin/dashboard.gsx b/examples/blog/admin/dashboard.gsx new file mode 100644 index 0000000..a2c123a --- /dev/null +++ b/examples/blog/admin/dashboard.gsx @@ -0,0 +1,88 @@ +package admin + +import ( + "net/http" + + "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/auth" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type dashboardPage struct{} + +type dashboardProps struct { + User store.User + Stats store.Stats + RecentPosts []store.Post +} + +// Props demonstrates the Props + RenderTarget pattern. Each widget is a +// standalone gsx function (StatsGrid, RecentPostsCard) so HTMX refresh +// requests with HX-Target: #stats-grid or #recent-posts-card resolve here +// and only the touched data is loaded — no full page work. +// +// Returns dashboardProps directly — gsx now emits method components as +// func (p dashboardPage) Page(props dashboardProps) gsx.Node without a wrapper struct. +func (p dashboardPage) Props(r *http.Request, s *store.Store, target structpages.RenderTarget) (dashboardProps, error) { + switch { + case target.Is(StatsGrid): + return dashboardProps{}, structpages.RenderComponent(StatsGrid(StatsGridProps{Stats: s.Stats()})) + + case target.Is(RecentPostsCard): + posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 5}) + return dashboardProps{}, structpages.RenderComponent(RecentPostsCard(RecentPostsCardProps{Posts: posts})) + } + + user, _ := auth.UserFromContext(r.Context()) + posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 5}) + return dashboardProps{ + User: user, + Stats: s.Stats(), + RecentPosts: posts, + }, nil +} + +component (p dashboardPage) Page(props dashboardProps) { + +
+

Dashboard

+
+ + +
+
+
+ + + +
    +
  • + Click ↻ Stats — only the StatsGrid widget refreshes (check Network tab). +
  • +
  • + Click ↻ Recent posts — only that card reloads, with its own DB query. +
  • +
  • + Hard refresh — the full document re-renders via Page(). +
  • +
+
+
+
+} diff --git a/examples/blog/admin/dashboard.templ b/examples/blog/admin/dashboard.templ deleted file mode 100644 index 4d692dc..0000000 --- a/examples/blog/admin/dashboard.templ +++ /dev/null @@ -1,79 +0,0 @@ -package admin - -import ( - "net/http" - - "github.com/jackielii/structpages" - "github.com/jackielii/structpages/examples/blog/auth" - "github.com/jackielii/structpages/examples/blog/store" - "github.com/jackielii/structpages/examples/blog/ui/components" - "github.com/jackielii/structpages/examples/blog/ui/layout" -) - -type dashboardPage struct{} - -type dashboardProps struct { - User store.User - Stats store.Stats - RecentPosts []store.Post -} - -// Props demonstrates the Props + RenderTarget pattern. Each widget is a -// standalone templ function (StatsGrid, RecentPostsCard) so HTMX refresh -// requests with HX-Target: #stats-grid or #recent-posts-card resolve here -// and only the touched data is loaded — no full page work. -func (p dashboardPage) Props(r *http.Request, s *store.Store, target structpages.RenderTarget) (dashboardProps, error) { - switch { - case target.Is(StatsGrid): - return dashboardProps{}, structpages.RenderComponent(StatsGrid(s.Stats())) - - case target.Is(RecentPostsCard): - posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 5}) - return dashboardProps{}, structpages.RenderComponent(RecentPostsCard(posts)) - } - - user, _ := auth.UserFromContext(r.Context()) - posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 5}) - return dashboardProps{ - User: user, - Stats: s.Stats(), - RecentPosts: posts, - }, nil -} - -templ (p dashboardPage) Page(props dashboardProps) { - @layout.AdminShell("Dashboard", props.User) { - @p.Content(props) - } -} - -templ (dashboardPage) Content(props dashboardProps) { -
-

Dashboard

-
- - -
-
-
- @StatsGrid(props.Stats) - @RecentPostsCard(props.RecentPosts) - @components.Card("Try it") { -
    -
  • Click ↻ Stats — only the StatsGrid widget refreshes (check Network tab).
  • -
  • Click ↻ Recent posts — only that card reloads, with its own DB query.
  • -
  • Hard refresh — the full document re-renders via Page().
  • -
- } -
-} diff --git a/examples/blog/admin/dashboard.x.go b/examples/blog/admin/dashboard.x.go new file mode 100644 index 0000000..6f4a914 --- /dev/null +++ b/examples/blog/admin/dashboard.x.go @@ -0,0 +1,122 @@ +// Code generated by gsx; DO NOT EDIT. + +package admin + +import ( + "context" + "io" + "net/http" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/auth" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type dashboardPage struct{} + +type dashboardProps struct { + User store.User + Stats store.Stats + RecentPosts []store.Post +} + +// Props demonstrates the Props + RenderTarget pattern. Each widget is a +// standalone gsx function (StatsGrid, RecentPostsCard) so HTMX refresh +// requests with HX-Target: #stats-grid or #recent-posts-card resolve here +// and only the touched data is loaded — no full page work. +// +// Returns dashboardProps directly — gsx now emits method components as +// func (p dashboardPage) Page(props dashboardProps) gsx.Node without a wrapper struct. +func (p dashboardPage) Props(r *http.Request, s *store.Store, target structpages.RenderTarget) (dashboardProps, error) { + switch { + case target.Is(StatsGrid): + return dashboardProps{}, structpages.RenderComponent(StatsGrid(StatsGridProps{Stats: s.Stats()})) + + case target.Is(RecentPostsCard): + posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 5}) + return dashboardProps{}, structpages.RenderComponent(RecentPostsCard(RecentPostsCardProps{Posts: posts})) + } + + user, _ := auth.UserFromContext(r.Context()) + posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 5}) + return dashboardProps{ + User: user, + Stats: s.Stats(), + RecentPosts: posts, + }, nil +} + +//line dashboard.gsx:47:1 +func (p dashboardPage) Page(props dashboardProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line dashboard.gsx:48:2 + _gsxgw.Node(ctx, layout.AdminShell(layout.AdminShellProps{Title: "Dashboard", Current: props.User, Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line dashboard.gsx:49:3 + _gsxgw.S("
") +//line dashboard.gsx:50:4 + _gsxgw.S("

Dashboard

") +//line dashboard.gsx:51:4 + _gsxgw.S("
") +//line dashboard.gsx:52:5 + _gsxgw.S("") +//line dashboard.gsx:60:5 + _gsxgw.S("
") +//line dashboard.gsx:70:3 + _gsxgw.S("
") +//line dashboard.gsx:71:4 + _gsxgw.Node(ctx, StatsGrid(StatsGridProps{Stats: props.Stats})) +//line dashboard.gsx:72:4 + _gsxgw.Node(ctx, RecentPostsCard(RecentPostsCardProps{Posts: props.RecentPosts})) +//line dashboard.gsx:73:4 + _gsxgw.Node(ctx, components.Card(components.CardProps{Title: "Try it", Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line dashboard.gsx:74:5 + _gsxgw.S("
    ") +//line dashboard.gsx:75:6 + _gsxgw.S("
  • Click ↻ Stats — only the StatsGrid widget refreshes (check Network tab).
  • ") +//line dashboard.gsx:78:6 + _gsxgw.S("
  • Click ↻ Recent posts — only that card reloads, with its own DB query.
  • ") +//line dashboard.gsx:81:6 + _gsxgw.S("
  • Hard refresh — the full document re-renders via Page().
") + return _gsxgw.Err() + })})) + _gsxgw.S("
") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} diff --git a/examples/blog/admin/login.templ b/examples/blog/admin/login.gsx similarity index 59% rename from examples/blog/admin/login.templ rename to examples/blog/admin/login.gsx index 5bbdfb7..b65a597 100644 --- a/examples/blog/admin/login.templ +++ b/examples/blog/admin/login.gsx @@ -2,7 +2,10 @@ package admin import "github.com/jackielii/structpages/examples/blog/ui/components" -templ loginShell(username, errMsg string) { +// LoginShell is invoked from login.go (LoginPage.ServeHTTP). gsx requires +// component names to be Capitalized (lowercase = HTML element), so the templ +// `loginShell` becomes `LoginShell`. +component LoginShell(username, errMsg string) { @@ -12,21 +15,28 @@ templ loginShell(username, errMsg string) {
- @components.Card("Sign in") { +
- @components.Alert(components.AlertError, errMsg) +
diff --git a/examples/blog/admin/login.x.go b/examples/blog/admin/login.x.go new file mode 100644 index 0000000..ce7480c --- /dev/null +++ b/examples/blog/admin/login.x.go @@ -0,0 +1,82 @@ +// Code generated by gsx; DO NOT EDIT. + +package admin + +import ( + "context" + "io" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages/examples/blog/ui/components" +) + +// LoginShell is invoked from login.go (LoginPage.ServeHTTP). gsx requires +// component names to be Capitalized (lowercase = HTML element), so the templ +// `loginShell` becomes `LoginShell`. + +type LoginShellProps struct { + Username string + ErrMsg string +} + +//line login.gsx:8:1 +func LoginShell(_gsxp LoginShellProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + username := _gsxp.Username + errMsg := _gsxp.ErrMsg + _gsxgw := gsx.W(_gsxw) + _gsxgw.S("") +//line login.gsx:10:2 + _gsxgw.S("") +//line login.gsx:11:3 + _gsxgw.S("") +//line login.gsx:12:4 + _gsxgw.S("") +//line login.gsx:13:4 + _gsxgw.S("Sign in — blog admin") +//line login.gsx:14:4 + _gsxgw.S("") +//line login.gsx:16:3 + _gsxgw.S("") +//line login.gsx:17:4 + _gsxgw.S("
") +//line login.gsx:18:5 + _gsxgw.Node(ctx, components.Card(components.CardProps{Title: "Sign in", Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line login.gsx:19:6 + _gsxgw.S("
") +//line login.gsx:20:7 + _gsxgw.Node(ctx, components.Alert(components.AlertProps{Kind: components.AlertError, Msg: errMsg})) +//line login.gsx:24:7 + _gsxgw.S("") +//line login.gsx:36:7 + _gsxgw.S("") +//line login.gsx:47:7 + _gsxgw.S("") +//line login.gsx:53:7 + _gsxgw.S("

Demo credentials: ") +//line login.gsx:54:26 + _gsxgw.S("admin / ") +//line login.gsx:54:47 + _gsxgw.S("admin

") + return _gsxgw.Err() + })})) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} diff --git a/examples/blog/admin/posts.go b/examples/blog/admin/posts.go index c238dd0..74adc45 100644 --- a/examples/blog/admin/posts.go +++ b/examples/blog/admin/posts.go @@ -118,7 +118,7 @@ func parsePostForm(r *http.Request) (store.Post, string) { // renderPostForm re-renders the form on validation failure, preserving inputs. func renderPostForm(ctx context.Context, w http.ResponseWriter, user store.User, title string, p store.Post, cats []store.Category, errMsg string) error { body := PostForm(PostFormProps{P: p, Cats: cats, ErrMsg: errMsg}) - return AdminShellWith(AdminShellWithProps{Title: title, User: user, Body: body}).Render(ctx, w) + return AdminShellWith(AdminShellWithProps{Title: title, User: user, Children: body}).Render(ctx, w) } // postFormAction returns the POST URL for the form: create when ID==0, diff --git a/examples/blog/admin/posts.gsx b/examples/blog/admin/posts.gsx new file mode 100644 index 0000000..c2f96a8 --- /dev/null +++ b/examples/blog/admin/posts.gsx @@ -0,0 +1,142 @@ +package admin + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages/examples/blog/auth" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +// AdminShellWith is a tiny gsx wrapper used by handlers that need to render a +// custom body inside AdminShell from Go code. The body is passed as the +// implicit Children prop — from Go: AdminShellWith(AdminShellWithProps{Title: …, +// User: …, Children: body}). +component AdminShellWith(title string, user store.User) { + + { children } + +} + +// --- List --- + +type postListPage struct{} + +type postListProps struct { + User store.User + Posts []store.Post +} + +func (postListPage) Props(r *http.Request, s *store.Store) (postListProps, error) { + user, _ := auth.UserFromContext(r.Context()) + posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 50}) + return postListProps{User: user, Posts: posts}, nil +} + +component (p postListPage) Page(props postListProps) { + +
+

All posts

+ url} + > + New post + +
+ +
+} + +// --- New --- + +type postNewPage struct{} + +type postFormViewProps struct { + User store.User + Categories []store.Category + Post store.Post +} + +func (postNewPage) Props(r *http.Request, s *store.Store) (postFormViewProps, error) { + user, _ := auth.UserFromContext(r.Context()) + return postFormViewProps{User: user, Categories: s.ListCategories()}, nil +} + +component (p postNewPage) Page(props postFormViewProps) { + +

New post

+ +
+} + +// --- Edit --- + +type postEditPage struct{} + +func (postEditPage) Props(r *http.Request, s *store.Store) (postFormViewProps, error) { + user, _ := auth.UserFromContext(r.Context()) + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + return postFormViewProps{}, fmt.Errorf("invalid post id: %w", err) + } + p, err := s.GetPost(id) + if err != nil { + return postFormViewProps{}, err + } + return postFormViewProps{User: user, Categories: s.ListCategories(), Post: p}, nil +} + +component (p postEditPage) Page(props postFormViewProps) { + +

Edit post

+ +
+} + +// --- Shared form --- + +component PostForm(p store.Post, cats []store.Category, errMsg string) { +
+ + + + + + + + +} diff --git a/examples/blog/admin/posts.templ b/examples/blog/admin/posts.templ deleted file mode 100644 index caf897f..0000000 --- a/examples/blog/admin/posts.templ +++ /dev/null @@ -1,123 +0,0 @@ -package admin - -import ( - "fmt" - "net/http" - "strconv" - - "github.com/jackielii/structpages/examples/blog/auth" - "github.com/jackielii/structpages/examples/blog/store" - "github.com/jackielii/structpages/examples/blog/ui/components" - "github.com/jackielii/structpages/examples/blog/ui/layout" -) - -// adminShellWith is a tiny templ wrapper used by handlers that need to -// render a custom body inside AdminShell from Go code. -templ adminShellWith(title string, user store.User, body templ.Component) { - @layout.AdminShell(title, user) { - @body - } -} - -// --- List --- - -type postListPage struct{} - -type postListProps struct { - User store.User - Posts []store.Post -} - -func (postListPage) Props(r *http.Request, s *store.Store) (postListProps, error) { - user, _ := auth.UserFromContext(r.Context()) - posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 50}) - return postListProps{User: user, Posts: posts}, nil -} - -templ (p postListPage) Page(props postListProps) { - @layout.AdminShell("Posts", props.User) { - @p.Content(props) - } -} - -templ (postListPage) Content(props postListProps) { -
-

All posts

- New post -
- @PostsTable(props.Posts) -} - -// --- New --- - -type postNewPage struct{} - -type postFormViewProps struct { - User store.User - Categories []store.Category - Post store.Post -} - -func (postNewPage) Props(r *http.Request, s *store.Store) (postFormViewProps, error) { - user, _ := auth.UserFromContext(r.Context()) - return postFormViewProps{User: user, Categories: s.ListCategories()}, nil -} - -templ (postNewPage) Page(props postFormViewProps) { - @layout.AdminShell("New post", props.User) { -

New post

- @postForm(props.Post, props.Categories, "") - } -} - -// --- Edit --- - -type postEditPage struct{} - -func (postEditPage) Props(r *http.Request, s *store.Store) (postFormViewProps, error) { - user, _ := auth.UserFromContext(r.Context()) - id, err := strconv.Atoi(r.PathValue("id")) - if err != nil { - return postFormViewProps{}, fmt.Errorf("invalid post id: %w", err) - } - p, err := s.GetPost(id) - if err != nil { - return postFormViewProps{}, err - } - return postFormViewProps{User: user, Categories: s.ListCategories(), Post: p}, nil -} - -templ (postEditPage) Page(props postFormViewProps) { - @layout.AdminShell("Edit post", props.User) { -

Edit post

- @postForm(props.Post, props.Categories, "") - } -} - -// --- Shared form --- - -templ postForm(p store.Post, cats []store.Category, errMsg string) { -
- @components.Alert(components.AlertError, errMsg) - @components.Input("title", "Title", p.Title, "") - @components.Input("slug", "Slug (auto if blank)", p.Slug, "") - - @components.Textarea("body", "Body", p.Body, "") - -
- @components.Button("Save", templ.Attributes{"type": "submit"}) - Cancel -
-
-} diff --git a/examples/blog/admin/posts.x.go b/examples/blog/admin/posts.x.go new file mode 100644 index 0000000..0eecf25 --- /dev/null +++ b/examples/blog/admin/posts.x.go @@ -0,0 +1,240 @@ +// Code generated by gsx; DO NOT EDIT. + +package admin + +import ( + "context" + "fmt" + "io" + "net/http" + "strconv" + + "github.com/gsxhq/gsx" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/auth" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +// AdminShellWith is a tiny gsx wrapper used by handlers that need to render a +// custom body inside AdminShell from Go code. The body is passed as the +// implicit Children prop — from Go: AdminShellWith(AdminShellWithProps{Title: …, +// User: …, Children: body}). + +type AdminShellWithProps struct { + Title string + User store.User + Children gsx.Node +} + +//line posts.gsx:19:1 +func AdminShellWith(_gsxp AdminShellWithProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + title := _gsxp.Title + user := _gsxp.User + children := _gsxp.Children + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:20:2 + _gsxgw.Node(ctx, layout.AdminShell(layout.AdminShellProps{Title: title, Current: user, Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:21:3 + _gsxgw.Node(ctx, children) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +// --- List --- + +type postListPage struct{} + +type postListProps struct { + User store.User + Posts []store.Post +} + +func (postListPage) Props(r *http.Request, s *store.Store) (postListProps, error) { + user, _ := auth.UserFromContext(r.Context()) + posts, _ := s.ListPosts(store.PostFilter{IncludeDraft: true, PageSize: 50}) + return postListProps{User: user, Posts: posts}, nil +} + +//line posts.gsx:40:1 +func (p postListPage) Page(props postListProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:41:2 + _gsxgw.Node(ctx, layout.AdminShell(layout.AdminShellProps{Title: "Posts", Current: props.User, Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:42:3 + _gsxgw.S("
") +//line posts.gsx:43:4 + _gsxgw.S("

All posts

") +//line posts.gsx:44:4 + _gsxgw.S("New post
") +//line posts.gsx:51:3 + _gsxgw.Node(ctx, PostsTable(PostsTableProps{Posts: props.Posts})) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +// --- New --- + +type postNewPage struct{} + +type postFormViewProps struct { + User store.User + Categories []store.Category + Post store.Post +} + +func (postNewPage) Props(r *http.Request, s *store.Store) (postFormViewProps, error) { + user, _ := auth.UserFromContext(r.Context()) + return postFormViewProps{User: user, Categories: s.ListCategories()}, nil +} + +//line posts.gsx:70:1 +func (p postNewPage) Page(props postFormViewProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:71:2 + _gsxgw.Node(ctx, layout.AdminShell(layout.AdminShellProps{Title: "New post", Current: props.User, Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:72:3 + _gsxgw.S("

New post

") +//line posts.gsx:73:3 + _gsxgw.Node(ctx, PostForm(PostFormProps{P: props.Post, Cats: props.Categories, ErrMsg: ""})) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +// --- Edit --- + +type postEditPage struct{} + +func (postEditPage) Props(r *http.Request, s *store.Store) (postFormViewProps, error) { + user, _ := auth.UserFromContext(r.Context()) + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + return postFormViewProps{}, fmt.Errorf("invalid post id: %w", err) + } + p, err := s.GetPost(id) + if err != nil { + return postFormViewProps{}, err + } + return postFormViewProps{User: user, Categories: s.ListCategories(), Post: p}, nil +} + +//line posts.gsx:94:1 +func (p postEditPage) Page(props postFormViewProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:95:2 + _gsxgw.Node(ctx, layout.AdminShell(layout.AdminShellProps{Title: "Edit post", Current: props.User, Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:96:3 + _gsxgw.S("

Edit post

") +//line posts.gsx:97:3 + _gsxgw.Node(ctx, PostForm(PostFormProps{P: props.Post, Cats: props.Categories, ErrMsg: ""})) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +// --- Shared form --- + +type PostFormProps struct { + P store.Post + Cats []store.Category + ErrMsg string + Attrs gsx.Attrs +} + +//line posts.gsx:103:1 +func PostForm(_gsxp PostFormProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + p := _gsxp.P + cats := _gsxp.Cats + errMsg := _gsxp.ErrMsg + _gsxgw := gsx.W(_gsxw) +//line posts.gsx:104:2 + _gsxgw.S("") +//line posts.gsx:105:3 + _gsxgw.Node(ctx, components.Alert(components.AlertProps{Kind: components.AlertError, Msg: errMsg})) +//line posts.gsx:106:3 + _gsxgw.Node(ctx, components.Input(components.InputProps{Name: "title", Label: "Title", Value: p.Title, ErrMsg: ""})) +//line posts.gsx:107:3 + _gsxgw.Node(ctx, components.Input(components.InputProps{Name: "slug", Label: "Slug (auto if blank)", Value: p.Slug, ErrMsg: ""})) +//line posts.gsx:113:3 + _gsxgw.S("") +//line posts.gsx:127:3 + _gsxgw.Node(ctx, components.Textarea(components.TextareaProps{Name: "body", Label: "Body", Value: p.Body, ErrMsg: ""})) +//line posts.gsx:128:3 + _gsxgw.S("") +//line posts.gsx:132:3 + _gsxgw.S("
") +//line posts.gsx:133:4 + _gsxgw.Node(ctx, components.Button(components.ButtonProps{Label: "Save", Attrs: gsx.Attrs{}.Merge(gsx.Attrs{"type": "submit"})})) +//line posts.gsx:134:4 + _gsxgw.S("Cancel
") + return _gsxgw.Err() + }) +} diff --git a/examples/blog/admin/users.gsx b/examples/blog/admin/users.gsx new file mode 100644 index 0000000..87ef00c --- /dev/null +++ b/examples/blog/admin/users.gsx @@ -0,0 +1,94 @@ +package admin + +import ( + "net/http" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages/examples/blog/auth" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type userListPage struct{} + +type userListProps struct { + User store.User + Users []store.User +} + +func (userListPage) Props(r *http.Request, s *store.Store) (userListProps, error) { + user, _ := auth.UserFromContext(r.Context()) + return userListProps{User: user, Users: s.ListUsers()}, nil +} + +component (p userListPage) Page(props userListProps) { + +

Users

+
+ +
    + { for _, u := range props.Users { +
  • + + { u.Username } + { if u.IsAdmin { + + admin + + } } + +
    url("id", u.ID)} + class="m-0" + > + +
    +
  • + } } +
+
+ +
url} + class="space-y-3" + > + + + + + +
+
+
+} diff --git a/examples/blog/admin/users.templ b/examples/blog/admin/users.templ deleted file mode 100644 index b6faa0c..0000000 --- a/examples/blog/admin/users.templ +++ /dev/null @@ -1,74 +0,0 @@ -package admin - -import ( - "net/http" - - "github.com/jackielii/structpages/examples/blog/auth" - "github.com/jackielii/structpages/examples/blog/store" - "github.com/jackielii/structpages/examples/blog/ui/components" - "github.com/jackielii/structpages/examples/blog/ui/layout" -) - -type userListPage struct{} - -type userListProps struct { - User store.User - Users []store.User -} - -func (userListPage) Props(r *http.Request, s *store.Store) (userListProps, error) { - user, _ := auth.UserFromContext(r.Context()) - return userListProps{User: user, Users: s.ListUsers()}, nil -} - -templ (p userListPage) Page(props userListProps) { - @layout.AdminShell("Users", props.User) { - @p.Content(props) - } -} - -templ (userListPage) Content(props userListProps) { -

Users

-
- @components.Card("Existing users") { -
    - for _, u := range props.Users { -
  • - - { u.Username } - if u.IsAdmin { - admin - } - -
    - -
    -
  • - } -
- } - @components.Card("Create user") { -
- @components.Input("username", "Username", "", "") - - - @components.Button("Create", templ.Attributes{"type": "submit"}) -
- } -
-} diff --git a/examples/blog/admin/users.x.go b/examples/blog/admin/users.x.go new file mode 100644 index 0000000..a591514 --- /dev/null +++ b/examples/blog/admin/users.x.go @@ -0,0 +1,111 @@ +// Code generated by gsx; DO NOT EDIT. + +package admin + +import ( + "context" + "io" + "net/http" + + "github.com/gsxhq/gsx" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/auth" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type userListPage struct{} + +type userListProps struct { + User store.User + Users []store.User +} + +func (userListPage) Props(r *http.Request, s *store.Store) (userListProps, error) { + user, _ := auth.UserFromContext(r.Context()) + return userListProps{User: user, Users: s.ListUsers()}, nil +} + +//line users.gsx:25:1 +func (p userListPage) Page(props userListProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line users.gsx:26:2 + _gsxgw.Node(ctx, layout.AdminShell(layout.AdminShellProps{Title: "Users", Current: props.User, Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line users.gsx:27:3 + _gsxgw.S("

Users

") +//line users.gsx:28:3 + _gsxgw.S("
") +//line users.gsx:29:4 + _gsxgw.Node(ctx, components.Card(components.CardProps{Title: "Existing users", Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line users.gsx:30:5 + _gsxgw.S("
    ") +//line users.gsx:31:6 + for _, u := range props.Users { +//line users.gsx:32:7 + _gsxgw.S("
  • ") +//line users.gsx:33:8 + _gsxgw.S("") +//line users.gsx:34:9 + _gsxgw.Text(string(u.Username)) +//line users.gsx:35:9 + if u.IsAdmin { +//line users.gsx:36:10 + _gsxgw.S("admin") + } + _gsxgw.S("") +//line users.gsx:43:8 + _gsxgw.S("
    ") +//line users.gsx:48:9 + _gsxgw.S("
  • ") + } + _gsxgw.S("
") + return _gsxgw.Err() + })})) +//line users.gsx:59:4 + _gsxgw.Node(ctx, components.Card(components.CardProps{Title: "Create user", Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line users.gsx:60:5 + _gsxgw.S("
") +//line users.gsx:65:6 + _gsxgw.Node(ctx, components.Input(components.InputProps{Name: "username", Label: "Username", Value: "", ErrMsg: ""})) +//line users.gsx:71:6 + _gsxgw.S("") +//line users.gsx:82:6 + _gsxgw.S("") +//line users.gsx:86:6 + _gsxgw.Node(ctx, components.Button(components.ButtonProps{Label: "Create", Attrs: gsx.Attrs{}.Merge(gsx.Attrs{"type": "submit"})})) + _gsxgw.S("
") + return _gsxgw.Err() + })})) + _gsxgw.S("
") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} diff --git a/examples/blog/blog/category.templ b/examples/blog/blog/category.gsx similarity index 55% rename from examples/blog/blog/category.templ rename to examples/blog/blog/category.gsx index 61e717e..4527eb6 100644 --- a/examples/blog/blog/category.templ +++ b/examples/blog/blog/category.gsx @@ -4,7 +4,6 @@ import ( "net/http" "strconv" - "github.com/jackielii/structpages" "github.com/jackielii/structpages/examples/blog/store" "github.com/jackielii/structpages/examples/blog/ui/components" "github.com/jackielii/structpages/examples/blog/ui/layout" @@ -15,7 +14,7 @@ type categoryPage struct{} type categoryProps struct { Category store.Category Posts []store.Post - Pagination components.PaginationProps + Pagination components.PageNav } func (categoryPage) Props(r *http.Request, s *store.Store) (categoryProps, error) { @@ -34,18 +33,16 @@ func (categoryPage) Props(r *http.Request, s *store.Store) (categoryProps, error PageSize: store.DefaultPageSize, }) - // URL closure captures the request context so links re-use the current - // route params (slug auto-fills from r.PathValue). ctx := r.Context() return categoryProps{ Category: cat, Posts: posts, - Pagination: components.PaginationProps{ + Pagination: components.PageNav{ Page: page, PageSize: store.DefaultPageSize, Total: total, URL: func(target int) (string, error) { - return structpages.URLFor(ctx, + return components.URL(ctx, []any{categoryPage{}, "?page={page}"}, "page", target, ) @@ -54,24 +51,20 @@ func (categoryPage) Props(r *http.Request, s *store.Store) (categoryProps, error }, nil } -templ (p categoryPage) Page(props categoryProps) { - @layout.PublicShell(props.Category.Name) { - @p.Content(props) - } -} - -templ (categoryPage) Content(props categoryProps) { -

{ props.Category.Name }

-

Posts filed under this category.

-
- if len(props.Posts) == 0 { -

Nothing here yet.

- } - for _, post := range props.Posts { - @PostCard(post) - } -
-
- @components.Pagination(props.Pagination) -
+component (p categoryPage) Page(props categoryProps) { + +

{ props.Category.Name }

+

+ Posts filed under this category. +

+
+ { if len(props.Posts) == 0 { +

Nothing here yet.

+ } } + { for _, post := range props.Posts { + + } } +
+
{ components.Pagination(props.Pagination) }
+
} diff --git a/examples/blog/blog/category.x.go b/examples/blog/blog/category.x.go new file mode 100644 index 0000000..d72660f --- /dev/null +++ b/examples/blog/blog/category.x.go @@ -0,0 +1,95 @@ +// Code generated by gsx; DO NOT EDIT. + +package blog + +import ( + "context" + "io" + "net/http" + "strconv" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type categoryPage struct{} + +type categoryProps struct { + Category store.Category + Posts []store.Post + Pagination components.PageNav +} + +func (categoryPage) Props(r *http.Request, s *store.Store) (categoryProps, error) { + slug := r.PathValue("slug") + cat, err := s.GetCategoryBySlug(slug) + if err != nil { + return categoryProps{}, err + } + page, _ := strconv.Atoi(r.URL.Query().Get("page")) + if page < 1 { + page = 1 + } + posts, total := s.ListPosts(store.PostFilter{ + CategoryID: cat.ID, + Page: page, + PageSize: store.DefaultPageSize, + }) + + ctx := r.Context() + return categoryProps{ + Category: cat, + Posts: posts, + Pagination: components.PageNav{ + Page: page, + PageSize: store.DefaultPageSize, + Total: total, + URL: func(target int) (string, error) { + return components.URL(ctx, + []any{categoryPage{}, "?page={page}"}, + "page", target, + ) + }, + }, + }, nil +} + +//line category.gsx:54:1 +func (p categoryPage) Page(props categoryProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line category.gsx:55:2 + _gsxgw.Node(ctx, layout.PublicShell(layout.PublicShellProps{Title: props.Category.Name, Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line category.gsx:56:3 + _gsxgw.S("

") +//line category.gsx:56:43 + _gsxgw.Text(string(props.Category.Name)) + _gsxgw.S("

") +//line category.gsx:57:3 + _gsxgw.S("

Posts filed under this category.

") +//line category.gsx:60:3 + _gsxgw.S("
") +//line category.gsx:61:4 + if len(props.Posts) == 0 { +//line category.gsx:62:5 + _gsxgw.S("

Nothing here yet.

") + } +//line category.gsx:64:4 + for _, post := range props.Posts { +//line category.gsx:65:5 + _gsxgw.Node(ctx, PostCard(PostCardProps{P: post})) + } + _gsxgw.S("
") +//line category.gsx:68:3 + _gsxgw.S("
") +//line category.gsx:68:21 + _gsxgw.Node(ctx, components.Pagination(props.Pagination)) + _gsxgw.S("
") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} diff --git a/examples/blog/blog/components.templ b/examples/blog/blog/components.gsx similarity index 63% rename from examples/blog/blog/components.templ rename to examples/blog/blog/components.gsx index 500cb42..16b6e9c 100644 --- a/examples/blog/blog/components.templ +++ b/examples/blog/blog/components.gsx @@ -1,27 +1,28 @@ -// Feature-local templ components for the public blog. These are templ +// Feature-local gsx components for the public blog. These are standalone // functions (not page methods) so other handlers in this package can target // them via RenderComponent — most notably the comment handler, which // re-renders CommentsList after a successful POST. package blog import ( - "github.com/jackielii/structpages" "github.com/jackielii/structpages/examples/blog/store" - "github.com/jackielii/structpages/examples/blog/ui/components" ) -templ PostMeta(p store.Post) { +component PostMeta(p store.Post) {

Posted { p.CreatedAt.Format("Jan 2, 2006") }

} -templ PostCard(p store.Post) { +component PostCard(p store.Post) { } @@ -29,19 +30,21 @@ templ PostCard(p store.Post) { // CommentsList is a standalone function component so the commentHandler // can re-render it from ServeHTTP via RenderComponent(CommentsList(...)). // It owns its own wrapper id, which doubles as the HTMX hx-target. -templ CommentsList(comments []store.Comment) { -
- if len(comments) == 0 { -

No comments yet — be the first.

- } - for _, c := range comments { +component CommentsList(comments []store.Comment) { +
id} class="space-y-3"> + { if len(comments) == 0 { +

+ No comments yet — be the first. +

+ } } + { for _, c := range comments {
{ c.Author } · { c.CreatedAt.Format("15:04 Jan 2") }

{ c.Body }

- } + } }

Total: { len(comments) }

} diff --git a/examples/blog/blog/components.x.go b/examples/blog/blog/components.x.go new file mode 100644 index 0000000..039cabd --- /dev/null +++ b/examples/blog/blog/components.x.go @@ -0,0 +1,140 @@ +// Code generated by gsx; DO NOT EDIT. + +package blog + +import ( + "context" + "io" + "strconv" + + "github.com/gsxhq/gsx" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/store" +) + +type PostMetaProps struct { + P store.Post + Attrs gsx.Attrs +} + +//line components.gsx:11:1 +func PostMeta(_gsxp PostMetaProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + p := _gsxp.P + _gsxgw := gsx.W(_gsxw) +//line components.gsx:12:2 + _gsxgw.S("

Posted ") +//line components.gsx:13:10 + _gsxgw.Text(string(p.CreatedAt.Format("Jan 2, 2006"))) + _gsxgw.S("

") + return _gsxgw.Err() + }) +} + +type PostCardProps struct { + P store.Post + Attrs gsx.Attrs +} + +//line components.gsx:17:1 +func PostCard(_gsxp PostCardProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + p := _gsxp.P + _gsxgw := gsx.W(_gsxw) +//line components.gsx:18:2 + _gsxgw.S("") + return _gsxgw.Err() + }) +} + +// CommentsList is a standalone function component so the commentHandler +// can re-render it from ServeHTTP via RenderComponent(CommentsList(...)). +// It owns its own wrapper id, which doubles as the HTMX hx-target. + +type CommentsListProps struct { + Comments []store.Comment + Attrs gsx.Attrs +} + +//line components.gsx:33:1 +func CommentsList(_gsxp CommentsListProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + comments := _gsxp.Comments + _gsxgw := gsx.W(_gsxw) +//line components.gsx:34:2 + _gsxgw.S("") +//line components.gsx:35:3 + if len(comments) == 0 { +//line components.gsx:36:4 + _gsxgw.S("

No comments yet — be the first.

") + } +//line components.gsx:40:3 + for _, c := range comments { +//line components.gsx:41:4 + _gsxgw.S("
") +//line components.gsx:42:5 + _gsxgw.S("
") +//line components.gsx:43:6 + _gsxgw.Text(string(c.Author)) + _gsxgw.S(" · ") +//line components.gsx:43:22 + _gsxgw.Text(string(c.CreatedAt.Format("15:04 Jan 2"))) + _gsxgw.S("
") +//line components.gsx:45:5 + _gsxgw.S("

") +//line components.gsx:45:44 + _gsxgw.Text(string(c.Body)) + _gsxgw.S("

") + } +//line components.gsx:48:3 + _gsxgw.S("

Total: ") +//line components.gsx:48:44 + _gsxgw.Text(strconv.FormatInt(int64(len(comments)), 10)) + _gsxgw.S("

") + return _gsxgw.Err() + }) +} diff --git a/examples/blog/blog/home.gsx b/examples/blog/blog/home.gsx new file mode 100644 index 0000000..4fc5d6e --- /dev/null +++ b/examples/blog/blog/home.gsx @@ -0,0 +1,52 @@ +package blog + +import ( + "net/http" + + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type homePage struct{} + +type homeProps struct { + Posts []store.Post + Categories []store.Category +} + +// Props loads page data. Returns homeProps directly — gsx now emits method +// components as func (p homePage) Page(props homeProps) gsx.Node without a wrapper struct. +func (homePage) Props(_ *http.Request, s *store.Store) (homeProps, error) { + posts, _ := s.ListPosts(store.PostFilter{PageSize: 5}) + return homeProps{Posts: posts, Categories: s.ListCategories()}, nil +} + +// Page renders the full document. The former Content() method is inlined here: +// gsx's generated-props wrapping makes a separate Content(props) method +// undispatchable by structpages alongside Page(props) (both would need the same +// single Props-return type). See GAP notes. +component (p homePage) Page(props homeProps) { + +

Recent Posts

+
+ { for _, post := range props.Posts { + + } } +
+ + + +
+} diff --git a/examples/blog/blog/home.templ b/examples/blog/blog/home.templ deleted file mode 100644 index d8be302..0000000 --- a/examples/blog/blog/home.templ +++ /dev/null @@ -1,52 +0,0 @@ -package blog - -import ( - "net/http" - - "github.com/jackielii/structpages/examples/blog/store" - "github.com/jackielii/structpages/examples/blog/ui/components" - "github.com/jackielii/structpages/examples/blog/ui/layout" -) - -type homePage struct{} - -type homeProps struct { - Posts []store.Post - Categories []store.Category -} - -// Props demonstrates the simplest pattern: load data using a DI-injected -// *store.Store. The framework passes *http.Request and *store.Store by -// matching parameter types against the registry built in main. -func (homePage) Props(_ *http.Request, s *store.Store) (homeProps, error) { - posts, _ := s.ListPosts(store.PostFilter{PageSize: 5}) - return homeProps{Posts: posts, Categories: s.ListCategories()}, nil -} - -// Page wraps the layout for full-document loads. HTMX nav links target -// #content, which causes structpages to dispatch to Content() instead. -templ (p homePage) Page(props homeProps) { - @layout.PublicShell("Home") { - @p.Content(props) - } -} - -templ (homePage) Content(props homeProps) { -

Recent Posts

-
- for _, post := range props.Posts { - @PostCard(post) - } -
- @components.Card("Browse by category") { - - } -} diff --git a/examples/blog/blog/home.x.go b/examples/blog/blog/home.x.go new file mode 100644 index 0000000..a5def35 --- /dev/null +++ b/examples/blog/blog/home.x.go @@ -0,0 +1,82 @@ +// Code generated by gsx; DO NOT EDIT. + +package blog + +import ( + "context" + "io" + "net/http" + + "github.com/gsxhq/gsx" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type homePage struct{} + +type homeProps struct { + Posts []store.Post + Categories []store.Category +} + +// Props loads page data. Returns homeProps directly — gsx now emits method +// components as func (p homePage) Page(props homeProps) gsx.Node without a wrapper struct. +func (homePage) Props(_ *http.Request, s *store.Store) (homeProps, error) { + posts, _ := s.ListPosts(store.PostFilter{PageSize: 5}) + return homeProps{Posts: posts, Categories: s.ListCategories()}, nil +} + +// Page renders the full document. The former Content() method is inlined here: +// gsx's generated-props wrapping makes a separate Content(props) method +// undispatchable by structpages alongside Page(props) (both would need the same +// single Props-return type). See GAP notes. + +//line home.gsx:29:1 +func (p homePage) Page(props homeProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line home.gsx:30:2 + _gsxgw.Node(ctx, layout.PublicShell(layout.PublicShellProps{Title: "Home", Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line home.gsx:31:3 + _gsxgw.S("

Recent Posts

") +//line home.gsx:32:3 + _gsxgw.S("
") +//line home.gsx:33:4 + for _, post := range props.Posts { +//line home.gsx:34:5 + _gsxgw.Node(ctx, PostCard(PostCardProps{P: post})) + } + _gsxgw.S("
") +//line home.gsx:37:3 + _gsxgw.Node(ctx, components.Card(components.CardProps{Title: "Browse by category", Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line home.gsx:38:4 + _gsxgw.S("") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} diff --git a/examples/blog/blog/post.gsx b/examples/blog/blog/post.gsx new file mode 100644 index 0000000..cef8761 --- /dev/null +++ b/examples/blog/blog/post.gsx @@ -0,0 +1,88 @@ +package blog + +import ( + "net/http" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type postPage struct{} + +type postProps struct { + Post store.Post + Author store.User + Category store.Category + Comments []store.Comment +} + +func (postPage) Props(r *http.Request, s *store.Store) (postProps, error) { + slug := r.PathValue("slug") + p, err := s.GetPostBySlug(slug) + if err != nil { + return postProps{}, err + } + author, _ := s.GetUser(p.AuthorID) + category, _ := s.GetCategory(p.CategoryID) + return postProps{ + Post: p, + Author: author, + Category: category, + Comments: s.ListComments(p.ID), + }, nil +} + +component (p postPage) Page(props postProps) { + + +
+

Comments

+ +
url("slug", props.Post.Slug)} + hx-target={CommentsList |> target} + hx-swap="outerHTML" + hx-on:htmx:after-request="this.reset()" + > +

Add a comment

+ + + + +
+
+} diff --git a/examples/blog/blog/post.templ b/examples/blog/blog/post.templ deleted file mode 100644 index c78f3ab..0000000 --- a/examples/blog/blog/post.templ +++ /dev/null @@ -1,72 +0,0 @@ -package blog - -import ( - "net/http" - - "github.com/jackielii/structpages" - "github.com/jackielii/structpages/examples/blog/store" - "github.com/jackielii/structpages/examples/blog/ui/components" - "github.com/jackielii/structpages/examples/blog/ui/layout" -) - -type postPage struct{} - -type postProps struct { - Post store.Post - Author store.User - Category store.Category - Comments []store.Comment -} - -func (postPage) Props(r *http.Request, s *store.Store) (postProps, error) { - slug := r.PathValue("slug") - p, err := s.GetPostBySlug(slug) - if err != nil { - return postProps{}, err - } - author, _ := s.GetUser(p.AuthorID) - category, _ := s.GetCategory(p.CategoryID) - return postProps{ - Post: p, - Author: author, - Category: category, - Comments: s.ListComments(p.ID), - }, nil -} - -templ (p postPage) Page(props postProps) { - @layout.PublicShell(props.Post.Title) { - @p.Content(props) - } -} - -templ (postPage) Content(props postProps) { -
-

{ props.Post.Title }

-

- by { props.Author.Username } - if props.Category.Slug != "" { - · { props.Category.Name } - } -

-
-

{ props.Post.Body }

-
-
-
-

Comments

- @CommentsList(props.Comments) -
-

Add a comment

- @components.Input("author", "Name", "", "") - @components.Textarea("body", "Comment", "", "") - @components.Button("Post comment", templ.Attributes{"type": "submit"}) -
-
-} diff --git a/examples/blog/blog/post.x.go b/examples/blog/blog/post.x.go new file mode 100644 index 0000000..7dafc62 --- /dev/null +++ b/examples/blog/blog/post.x.go @@ -0,0 +1,119 @@ +// Code generated by gsx; DO NOT EDIT. + +package blog + +import ( + "context" + "io" + "net/http" + + "github.com/gsxhq/gsx" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/components" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type postPage struct{} + +type postProps struct { + Post store.Post + Author store.User + Category store.Category + Comments []store.Comment +} + +func (postPage) Props(r *http.Request, s *store.Store) (postProps, error) { + slug := r.PathValue("slug") + p, err := s.GetPostBySlug(slug) + if err != nil { + return postProps{}, err + } + author, _ := s.GetUser(p.AuthorID) + category, _ := s.GetCategory(p.CategoryID) + return postProps{ + Post: p, + Author: author, + Category: category, + Comments: s.ListComments(p.ID), + }, nil +} + +//line post.gsx:37:1 +func (p postPage) Page(props postProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line post.gsx:38:2 + _gsxgw.Node(ctx, layout.PublicShell(layout.PublicShellProps{Title: props.Post.Title, Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line post.gsx:39:3 + _gsxgw.S("
") +//line post.gsx:40:4 + _gsxgw.S("

") +//line post.gsx:40:39 + _gsxgw.Text(string(props.Post.Title)) + _gsxgw.S("

") +//line post.gsx:41:4 + _gsxgw.S("

by ") +//line post.gsx:42:8 + _gsxgw.Text(string(props.Author.Username)) +//line post.gsx:43:5 + if props.Category.Slug != "" { + _gsxgw.S("· ") +//line post.gsx:44:9 + _gsxgw.S("") +//line post.gsx:50:7 + _gsxgw.Text(string(props.Category.Name)) + _gsxgw.S("") + } + _gsxgw.S("

") +//line post.gsx:54:4 + _gsxgw.S("
") +//line post.gsx:55:5 + _gsxgw.S("

") +//line post.gsx:55:8 + _gsxgw.Text(string(props.Post.Body)) + _gsxgw.S("

") +//line post.gsx:58:3 + _gsxgw.S("
") +//line post.gsx:59:4 + _gsxgw.S("

Comments

") +//line post.gsx:60:4 + _gsxgw.Node(ctx, CommentsList(CommentsListProps{Comments: props.Comments})) +//line post.gsx:61:4 + _gsxgw.S("
") +//line post.gsx:68:5 + _gsxgw.S("

Add a comment

") +//line post.gsx:69:5 + _gsxgw.Node(ctx, components.Input(components.InputProps{Name: "author", Label: "Name", Value: "", ErrMsg: ""})) +//line post.gsx:75:5 + _gsxgw.Node(ctx, components.Textarea(components.TextareaProps{Name: "body", Label: "Comment", Value: "", ErrMsg: ""})) +//line post.gsx:81:5 + _gsxgw.Node(ctx, components.Button(components.ButtonProps{Label: "Post comment", Attrs: gsx.Attrs{}.Merge(gsx.Attrs{"type": "submit"})})) + _gsxgw.S("
") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} diff --git a/examples/blog/blog/search.gsx b/examples/blog/blog/search.gsx new file mode 100644 index 0000000..afa51fe --- /dev/null +++ b/examples/blog/blog/search.gsx @@ -0,0 +1,75 @@ +package blog + +import ( + "net/http" + + "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type searchPage struct{} + +type searchProps struct { + Query string + Posts []store.Post +} + +// Props uses RenderTarget to load less when HTMX only wants the results +// fragment. On the initial page load it returns the full searchProps; when the +// Results fragment is targeted it renders just that component via RenderComponent. +func (p searchPage) Props(r *http.Request, s *store.Store, target structpages.RenderTarget) (searchProps, error) { + q := r.URL.Query().Get("q") + sp := searchProps{Query: q} + if q != "" { + sp.Posts, _ = s.ListPosts(store.PostFilter{Search: q}) + } + if target.Is(p.Results) { + return searchProps{}, structpages.RenderComponent(p.Results(sp)) + } + return sp, nil +} + +component (p searchPage) Page(props searchProps) { + +

Search

+
url} + hx-target={p.Results |> target} + hx-swap="outerHTML" + hx-trigger="input changed delay:250ms from:input, submit" + hx-push-url="true" + > + +
+ +
+} + +component (p searchPage) Results(props searchProps) { +
id} class="space-y-4"> + { if props.Query == "" { +

Type a query to search.

+ } else if len(props.Posts) == 0 { +

+ No results for " + { props.Query } + ". +

+ } else { +

+ { resultsCount(len(props.Posts)) } +

+ { for _, post := range props.Posts { + + } } + } } +
+} diff --git a/examples/blog/blog/search.templ b/examples/blog/blog/search.templ deleted file mode 100644 index a7d15a5..0000000 --- a/examples/blog/blog/search.templ +++ /dev/null @@ -1,73 +0,0 @@ -package blog - -import ( - "net/http" - - "github.com/jackielii/structpages" - "github.com/jackielii/structpages/examples/blog/store" - "github.com/jackielii/structpages/examples/blog/ui/layout" -) - -type searchPage struct{} - -type searchProps struct { - Query string - Posts []store.Post -} - -// Props uses RenderTarget to load less when HTMX only wants the results -// fragment. On the initial page load we render the full layout; on -// subsequent keystrokes we only return the Results partial. -func (p searchPage) Props(r *http.Request, s *store.Store, target structpages.RenderTarget) (searchProps, error) { - q := r.URL.Query().Get("q") - props := searchProps{Query: q} - if q != "" { - props.Posts, _ = s.ListPosts(store.PostFilter{Search: q}) - } - if target.Is(p.Results) { - return searchProps{}, structpages.RenderComponent(p.Results, props) - } - return props, nil -} - -templ (p searchPage) Page(props searchProps) { - @layout.PublicShell("Search") { - @p.Content(props) - } -} - -templ (p searchPage) Content(props searchProps) { -

Search

-
- -
- @p.Results(props) -} - -templ (searchPage) Results(props searchProps) { -
- if props.Query == "" { -

Type a query to search.

- } else if len(props.Posts) == 0 { -

No results for "{ props.Query }".

- } else { -

{ resultsCount(len(props.Posts)) }

- for _, post := range props.Posts { - @PostCard(post) - } - } -
-} diff --git a/examples/blog/blog/search.x.go b/examples/blog/blog/search.x.go new file mode 100644 index 0000000..94c6513 --- /dev/null +++ b/examples/blog/blog/search.x.go @@ -0,0 +1,119 @@ +// Code generated by gsx; DO NOT EDIT. + +package blog + +import ( + "context" + "io" + "net/http" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/store" + "github.com/jackielii/structpages/examples/blog/ui/layout" +) + +type searchPage struct{} + +type searchProps struct { + Query string + Posts []store.Post +} + +// Props uses RenderTarget to load less when HTMX only wants the results +// fragment. On the initial page load it returns the full searchProps; when the +// Results fragment is targeted it renders just that component via RenderComponent. +func (p searchPage) Props(r *http.Request, s *store.Store, target structpages.RenderTarget) (searchProps, error) { + q := r.URL.Query().Get("q") + sp := searchProps{Query: q} + if q != "" { + sp.Posts, _ = s.ListPosts(store.PostFilter{Search: q}) + } + if target.Is(p.Results) { + return searchProps{}, structpages.RenderComponent(p.Results(sp)) + } + return sp, nil +} + +//line search.gsx:33:1 +func (p searchPage) Page(props searchProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line search.gsx:34:2 + _gsxgw.Node(ctx, layout.PublicShell(layout.PublicShellProps{Title: "Search", Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line search.gsx:35:3 + _gsxgw.S("

Search

") +//line search.gsx:36:3 + _gsxgw.S("
") +//line search.gsx:44:4 + _gsxgw.S("
") +//line search.gsx:52:3 + _gsxgw.Node(ctx, p.Results(props)) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line search.gsx:56:1 +func (p searchPage) Results(props searchProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line search.gsx:57:2 + _gsxgw.S("") +//line search.gsx:58:3 + if props.Query == "" { +//line search.gsx:59:4 + _gsxgw.S("

Type a query to search.

") + } else { +//line search.gsx:60:10 + if len(props.Posts) == 0 { +//line search.gsx:61:4 + _gsxgw.S("

No results for \"") +//line search.gsx:63:5 + _gsxgw.Text(string(props.Query)) + _gsxgw.S("\".

") + } else { +//line search.gsx:67:4 + _gsxgw.S("

") +//line search.gsx:68:5 + _gsxgw.Text(string(resultsCount(len(props.Posts)))) + _gsxgw.S("

") +//line search.gsx:70:4 + for _, post := range props.Posts { +//line search.gsx:71:5 + _gsxgw.Node(ctx, PostCard(PostCardProps{P: post})) + } + } + } + _gsxgw.S("") + return _gsxgw.Err() + }) +} diff --git a/examples/blog/go.mod b/examples/blog/go.mod index 863813a..81d1739 100644 --- a/examples/blog/go.mod +++ b/examples/blog/go.mod @@ -4,29 +4,11 @@ go 1.26.1 require github.com/jackielii/structpages v0.0.0-00010101000000-000000000000 -require github.com/a-h/templ v0.3.1020 // indirect - require ( - github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cli/browser v1.3.0 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gsxhq/gsx v0.0.0 github.com/jackielii/ctxkey v1.0.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/natefinch/atomic v1.0.1 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/tools v0.46.0 // indirect ) replace github.com/jackielii/structpages => ../.. -tool github.com/a-h/templ/cmd/templ - replace github.com/gsxhq/gsx => /Users/jackieli/personal/gsxhq/gsx diff --git a/examples/blog/go.sum b/examples/blog/go.sum index df0b1bd..2108c87 100644 --- a/examples/blog/go.sum +++ b/examples/blog/go.sum @@ -1,45 +1,4 @@ -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= -github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= -github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= -github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jackielii/ctxkey v1.0.1 h1:CcgbR+fQbrzZJWxI/7Ec4EhzUbmTU1sfI1gV7MAgjIg= github.com/jackielii/ctxkey v1.0.1/go.mod h1:fo4HOwrvSnc3n8o5qZ5L+FVcSyQn+d67CCnlEbH24uc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/blog/ui/components/components.templ b/examples/blog/ui/components/components.gsx similarity index 60% rename from examples/blog/ui/components/components.templ rename to examples/blog/ui/components/components.gsx index 626ed6c..423096b 100644 --- a/examples/blog/ui/components/components.templ +++ b/examples/blog/ui/components/components.gsx @@ -2,7 +2,7 @@ // every feature package: buttons, form fields, alerts, cards, pagination, // and the styled error component used by the global error handler. // -// Each function is a standalone templ component (not a method on a page +// Each function is a standalone gsx component (not a method on a page // struct). That means they can be addressed by HTMX as targets via their // kebab-cased name — e.g. hx-target="#pagination" matches Pagination. package components @@ -26,90 +26,107 @@ func alertClasses(kind AlertKind) string { } } -templ Alert(kind AlertKind, msg string) { - if msg != "" { -
{ msg }
- } +component Alert(kind AlertKind, msg string) { + { if msg != "" { +
+ { msg } +
+ } } } -templ Card(title string) { +component Card(title string) {
- if title != "" { -

{ title }

- } - { children... } + { if title != "" { +

+ { title } +

+ } } + { children }
} -templ Button(label string, attrs templ.Attributes) { +// Button takes only its label; any extra attributes (type, hx-*, etc.) fall +// through to the root } -templ Input(name, label, value, errMsg string) { +component Input(name, label, value, errMsg string) { } -templ Textarea(name, label, value, errMsg string) { +component Textarea(name, label, value, errMsg string) { } // Pagination is rendered as a standalone function component so HTMX requests // with HX-Target: #pagination resolve here regardless of which page hosts it. -templ Pagination(p PaginationProps) { +component Pagination(p PageNav) { } // ErrorPage is the full document rendered by main.errorHandler for non-HTMX // errors. ErrorBlock is the partial used for HTMX requests. -templ ErrorPage(status int, msg string) { +component ErrorPage(status int, msg string) { @@ -119,16 +136,18 @@ templ ErrorPage(status int, msg string) {
- @ErrorBlock(status, msg) +
} -templ ErrorBlock(status int, msg string) { +component ErrorBlock(status int, msg string) {
{ status }

{ msg }

- Back to home + + Back to home +
} diff --git a/examples/blog/ui/components/components.x.go b/examples/blog/ui/components/components.x.go new file mode 100644 index 0000000..cf1f0f4 --- /dev/null +++ b/examples/blog/ui/components/components.x.go @@ -0,0 +1,348 @@ +// Code generated by gsx; DO NOT EDIT. + +package components + +import ( + "context" + "io" + "strconv" + + "github.com/gsxhq/gsx" +) + +type AlertKind string + +const ( + AlertInfo AlertKind = "info" + AlertError AlertKind = "error" + AlertSuccess AlertKind = "success" +) + +func alertClasses(kind AlertKind) string { + switch kind { + case AlertError: + return "border-red-200 bg-red-50 text-red-800" + case AlertSuccess: + return "border-emerald-200 bg-emerald-50 text-emerald-800" + default: + return "border-slate-200 bg-slate-50 text-slate-800" + } +} + +type AlertProps struct { + Kind AlertKind + Msg string +} + +//line components.gsx:29:1 +func Alert(_gsxp AlertProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + kind := _gsxp.Kind + msg := _gsxp.Msg + _gsxgw := gsx.W(_gsxw) +//line components.gsx:30:2 + if msg != "" { +//line components.gsx:31:3 + _gsxgw.S("
") +//line components.gsx:32:4 + _gsxgw.Text(string(msg)) + _gsxgw.S("
") + } + return _gsxgw.Err() + }) +} + +type CardProps struct { + Title string + Children gsx.Node + Attrs gsx.Attrs +} + +//line components.gsx:37:1 +func Card(_gsxp CardProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + title := _gsxp.Title + children := _gsxp.Children + _gsxgw := gsx.W(_gsxw) +//line components.gsx:38:2 + _gsxgw.S("
") +//line components.gsx:39:3 + if title != "" { +//line components.gsx:40:4 + _gsxgw.S("

") +//line components.gsx:41:5 + _gsxgw.Text(string(title)) + _gsxgw.S("

") + } +//line components.gsx:44:3 + _gsxgw.Node(ctx, children) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +// Button takes only its label; any extra attributes (type, hx-*, etc.) fall +// through to the root ") + return _gsxgw.Err() + }) +} + +type InputProps struct { + Name string + Label string + Value string + ErrMsg string + Attrs gsx.Attrs +} + +//line components.gsx:58:1 +func Input(_gsxp InputProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + name := _gsxp.Name + label := _gsxp.Label + value := _gsxp.Value + errMsg := _gsxp.ErrMsg + _gsxgw := gsx.W(_gsxw) +//line components.gsx:59:2 + _gsxgw.S("") + return _gsxgw.Err() + }) +} + +type TextareaProps struct { + Name string + Label string + Value string + ErrMsg string + Attrs gsx.Attrs +} + +//line components.gsx:74:1 +func Textarea(_gsxp TextareaProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + name := _gsxp.Name + label := _gsxp.Label + value := _gsxp.Value + errMsg := _gsxp.ErrMsg + _gsxgw := gsx.W(_gsxw) +//line components.gsx:75:2 + _gsxgw.S("") + return _gsxgw.Err() + }) +} + +// Pagination is rendered as a standalone function component so HTMX requests +// with HX-Target: #pagination resolve here regardless of which page hosts it. + +//line components.gsx:92:1 +func Pagination(p PageNav) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line components.gsx:93:2 + _gsxgw.S("") + return _gsxgw.Err() + }) +} + +// ErrorPage is the full document rendered by main.errorHandler for non-HTMX +// errors. ErrorBlock is the partial used for HTMX requests. + +type ErrorPageProps struct { + Status int + Msg string +} + +//line components.gsx:129:1 +func ErrorPage(_gsxp ErrorPageProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + status := _gsxp.Status + msg := _gsxp.Msg + _gsxgw := gsx.W(_gsxw) + _gsxgw.S("") +//line components.gsx:131:2 + _gsxgw.S("") +//line components.gsx:132:3 + _gsxgw.S("") +//line components.gsx:133:4 + _gsxgw.S("") +//line components.gsx:134:4 + _gsxgw.S("Error ") +//line components.gsx:134:17 + _gsxgw.Text(strconv.FormatInt(int64(status), 10)) + _gsxgw.S("") +//line components.gsx:135:4 + _gsxgw.S("") +//line components.gsx:137:3 + _gsxgw.S("") +//line components.gsx:138:4 + _gsxgw.S("
") +//line components.gsx:139:5 + _gsxgw.Node(ctx, ErrorBlock(ErrorBlockProps{Status: status, Msg: msg})) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +type ErrorBlockProps struct { + Status int + Msg string + Attrs gsx.Attrs +} + +//line components.gsx:145:1 +func ErrorBlock(_gsxp ErrorBlockProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + status := _gsxp.Status + msg := _gsxp.Msg + _gsxgw := gsx.W(_gsxw) +//line components.gsx:146:2 + _gsxgw.S("
") +//line components.gsx:147:3 + _gsxgw.S("
") +//line components.gsx:147:52 + _gsxgw.Text(strconv.FormatInt(int64(status), 10)) + _gsxgw.S("
") +//line components.gsx:148:3 + _gsxgw.S("

") +//line components.gsx:148:32 + _gsxgw.Text(string(msg)) + _gsxgw.S("

") +//line components.gsx:149:3 + _gsxgw.S("Back to home
") + return _gsxgw.Err() + }) +} diff --git a/examples/blog/ui/layout/layout.gsx b/examples/blog/ui/layout/layout.gsx new file mode 100644 index 0000000..14c91a2 --- /dev/null +++ b/examples/blog/ui/layout/layout.gsx @@ -0,0 +1,138 @@ +// Package layout exports the shared HTML shells used by every page. +// Feature packages call PublicShell or AdminShell with {children} +// instead of writing their own document. +package layout + +import ( + "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/store" +) + +// PublicShell wraps reader-facing pages. Cross-feature links (e.g. the admin +// link) use structpages.Ref so this package never imports admin or blog — +// keeping the dependency graph one-way (features → ui). +component PublicShell(title string) { + + + + + + { title } — structpages blog + + + + +
+ +
+
+ { children } +
+ + +} + +// AdminShell wraps the authenticated admin app. +component AdminShell(title string, current store.User) { + + + + + + Admin — { title } + + + + +
+
+ url} + > + + blog admin + + +
+
+
+ { children } +
+ + +} diff --git a/examples/blog/ui/layout/layout.templ b/examples/blog/ui/layout/layout.templ deleted file mode 100644 index 4675a1f..0000000 --- a/examples/blog/ui/layout/layout.templ +++ /dev/null @@ -1,77 +0,0 @@ -// Package layout exports the shared HTML shells used by every page. -// Feature packages call PublicShell or AdminShell with { children... } -// instead of writing their own document. -package layout - -import ( - "github.com/jackielii/structpages" - "github.com/jackielii/structpages/examples/blog/store" -) - -// PublicShell wraps reader-facing pages. Cross-feature links (e.g. the admin -// link) use structpages.Ref so this package never imports admin or blog — -// keeping the dependency graph one-way (features → ui). -templ PublicShell(title string) { - - - - - - { title } — structpages blog - - - - -
- -
-
- { children... } -
- - -} - -// AdminShell wraps the authenticated admin app. -templ AdminShell(title string, current store.User) { - - - - - - Admin — { title } - - - - -
-
- - - blog admin - - -
-
-
- { children... } -
- - -} diff --git a/examples/blog/ui/layout/layout.x.go b/examples/blog/ui/layout/layout.x.go new file mode 100644 index 0000000..12462e4 --- /dev/null +++ b/examples/blog/ui/layout/layout.x.go @@ -0,0 +1,205 @@ +// Code generated by gsx; DO NOT EDIT. + +package layout + +import ( + "context" + "io" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages" + _gsxf0 "github.com/jackielii/structpages" + "github.com/jackielii/structpages/examples/blog/store" +) + +// PublicShell wraps reader-facing pages. Cross-feature links (e.g. the admin +// link) use structpages.Ref so this package never imports admin or blog — +// keeping the dependency graph one-way (features → ui). + +type PublicShellProps struct { + Title string + Children gsx.Node +} + +//line layout.gsx:14:1 +func PublicShell(_gsxp PublicShellProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + title := _gsxp.Title + children := _gsxp.Children + _gsxgw := gsx.W(_gsxw) + _gsxgw.S("") +//line layout.gsx:16:2 + _gsxgw.S("") +//line layout.gsx:17:3 + _gsxgw.S("") +//line layout.gsx:18:4 + _gsxgw.S("") +//line layout.gsx:19:4 + _gsxgw.S("") +//line layout.gsx:23:4 + _gsxgw.S("") +//line layout.gsx:23:11 + _gsxgw.Text(string(title)) + _gsxgw.S(" — structpages blog") +//line layout.gsx:24:4 + _gsxgw.S("") +//line layout.gsx:25:4 + _gsxgw.S("") +//line layout.gsx:27:3 + _gsxgw.S("") +//line layout.gsx:28:4 + _gsxgw.S("
") +//line layout.gsx:29:5 + _gsxgw.S("
") +//line layout.gsx:32:6 + _gsxgw.S("structpages blog") +//line layout.gsx:38:6 + _gsxgw.S("
") +//line layout.gsx:60:4 + _gsxgw.S("
") +//line layout.gsx:61:5 + _gsxgw.Node(ctx, children) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +// AdminShell wraps the authenticated admin app. + +type AdminShellProps struct { + Title string + Current store.User + Children gsx.Node +} + +//line layout.gsx:68:1 +func AdminShell(_gsxp AdminShellProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + title := _gsxp.Title + current := _gsxp.Current + children := _gsxp.Children + _gsxgw := gsx.W(_gsxw) + _gsxgw.S("") +//line layout.gsx:70:2 + _gsxgw.S("") +//line layout.gsx:71:3 + _gsxgw.S("") +//line layout.gsx:72:4 + _gsxgw.S("") +//line layout.gsx:73:4 + _gsxgw.S("") +//line layout.gsx:77:4 + _gsxgw.S("Admin — ") +//line layout.gsx:77:21 + _gsxgw.Text(string(title)) + _gsxgw.S("") +//line layout.gsx:78:4 + _gsxgw.S("") +//line layout.gsx:79:4 + _gsxgw.S("") +//line layout.gsx:81:3 + _gsxgw.S("") +//line layout.gsx:82:4 + _gsxgw.S("
") +//line layout.gsx:83:5 + _gsxgw.S("
") +//line layout.gsx:86:6 + _gsxgw.S("") +//line layout.gsx:90:7 + _gsxgw.S("\"\"blog admin") +//line layout.gsx:97:6 + _gsxgw.S("
") +//line layout.gsx:133:4 + _gsxgw.S("
") +//line layout.gsx:134:5 + _gsxgw.Node(ctx, children) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} diff --git a/examples/htmx-render-target/go.mod b/examples/htmx-render-target/go.mod index dceb489..b714bd1 100644 --- a/examples/htmx-render-target/go.mod +++ b/examples/htmx-render-target/go.mod @@ -4,29 +4,11 @@ go 1.26.1 require github.com/jackielii/structpages v0.0.0 -require github.com/a-h/templ v0.3.1020 // indirect - require ( - github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cli/browser v1.3.0 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gsxhq/gsx v0.0.0 github.com/jackielii/ctxkey v1.0.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/natefinch/atomic v1.0.1 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/tools v0.46.0 // indirect ) replace github.com/jackielii/structpages => ../.. -tool github.com/a-h/templ/cmd/templ - replace github.com/gsxhq/gsx => /Users/jackieli/personal/gsxhq/gsx diff --git a/examples/htmx-render-target/go.sum b/examples/htmx-render-target/go.sum index df0b1bd..2108c87 100644 --- a/examples/htmx-render-target/go.sum +++ b/examples/htmx-render-target/go.sum @@ -1,45 +1,4 @@ -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= -github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= -github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= -github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jackielii/ctxkey v1.0.1 h1:CcgbR+fQbrzZJWxI/7Ec4EhzUbmTU1sfI1gV7MAgjIg= github.com/jackielii/ctxkey v1.0.1/go.mod h1:fo4HOwrvSnc3n8o5qZ5L+FVcSyQn+d67CCnlEbH24uc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/htmx-render-target/pages.gsx b/examples/htmx-render-target/pages.gsx new file mode 100644 index 0000000..72149c8 --- /dev/null +++ b/examples/htmx-render-target/pages.gsx @@ -0,0 +1,175 @@ +package main + +// Shared standalone function components (can be used across multiple pages). +// These demonstrate the power of RenderTarget — no wrapper methods needed. + +component UserStatsWidget(stats UserStats) { +
+

User Statistics

+

Active Users: { stats.ActiveUsers }

+

New Today: { stats.NewToday }

+ +
+} + +component SalesChartWidget(data SalesData) { +
+

Sales Chart

+
+ { for _, point := range data.Points { +
+ } } +
+

Total Sales: ${ data.Total |> format("%.2f") }

+ +
+} + +component NotificationsList(notifications []Notification) { +
+

Recent Notifications

+
    + { for _, n := range notifications { +
  • + { n.Message } ({ n.Time.Format("15:04") }) +
  • + } } +
+ +
+} + +// Dashboard page. + +type dashboard struct{} + +component (p dashboard) Page(props dashboardData) { + +

Dashboard

+

+ This example demonstrates the RenderTarget API with standalone function components. +

+

+ Click "Refresh" buttons to see HTMX partial updates — each widget loads only its own data! +

+
+
id}> + +
+
id}> + +
+
id}> + +
+
+
+

How it works:

+
    +
  • + ✅ + Standalone functions + — UserStatsWidget, SalesChartWidget, NotificationsList are shared components +
  • +
  • + ✅ + Conditional loading + — Props checks target.Is() and loads only needed data +
  • +
  • + ✅ + RenderComponent (direct) + — construct the gsx component with its props struct and pass directly +
  • +
  • + ✅ + No wrapper methods + — No need to create dashboard.UserStats() method! +
  • +
  • + ✅ + HTMX integration + — HTMXRenderTarget automatically handles partial updates +
  • +
+
+ +} + +// Html is the full-page layout. +component Html() { + + + + + + RenderTarget API Example + + + +
{ children }
+ + + +} + +component ErrorPage(err error) { + + + +} + +component ErrorComp(err error) { +

Error

+

{ err.Error() }

+} diff --git a/examples/htmx-render-target/pages.templ b/examples/htmx-render-target/pages.templ deleted file mode 100644 index 9cb3b90..0000000 --- a/examples/htmx-render-target/pages.templ +++ /dev/null @@ -1,270 +0,0 @@ -package main - -import ( - "context" - "fmt" - "math/rand/v2" - "net/http" - "time" - - "github.com/jackielii/structpages" -) - -// Shared standalone function components (can be used across multiple pages) -// These demonstrate the power of RenderTarget - no need for wrapper methods! -templ UserStatsWidget(stats UserStats) { -
-

User Statistics

-

Active Users: { fmt.Sprintf("%d", stats.ActiveUsers) }

-

New Today: { fmt.Sprintf("%d", stats.NewToday) }

- -
-} - -templ SalesChartWidget(data SalesData) { -
-

Sales Chart

-
- for _, point := range data.Points { -
- } -
-

Total Sales: ${ fmt.Sprintf("%.2f", data.Total) }

- -
-} - -templ NotificationsList(notifications []Notification) { -
-

Recent Notifications

-
    - for _, n := range notifications { -
  • { n.Message } ({ n.Time.Format("15:04") })
  • - } -
- -
-} - -// Dashboard page using RenderTarget API - -type dashboard struct{} - -type DashboardProps struct { - Stats UserStats - Sales SalesData - Notifications []Notification -} - -type UserStats struct { - ActiveUsers int - NewToday int -} - -type SalesData struct { - Points []DataPoint - Total float64 -} - -type DataPoint struct { - Label string - Value int -} - -type Notification struct { - Message string - Time time.Time -} - -// Props demonstrates conditional data loading with RenderTarget -func (p dashboard) Props(r *http.Request, target structpages.RenderTarget) (DashboardProps, error) { - // Check which component is being requested and load only necessary data - switch { - case target.Is(UserStatsWidget): - // Only load user stats (lightweight query) - stats := loadUserStats() - comp := UserStatsWidget(stats) - // Use RenderComponent with target to render just this widget - return DashboardProps{}, structpages.RenderComponent(comp) - - case target.Is(SalesChartWidget): - // Only load sales data (expensive query - avoid loading unnecessarily) - sales := loadSalesData() - return DashboardProps{}, structpages.RenderComponent(target, sales) - - case target.Is(NotificationsList): - // Only load notifications - notifications := loadNotifications() - return DashboardProps{}, structpages.RenderComponent(target, notifications) - - case target.Is(p.Page): - // Full page load - load all data - return DashboardProps{ - Stats: loadUserStats(), - Sales: loadSalesData(), - Notifications: loadNotifications(), - }, nil - - default: - // Fallback to full page - return DashboardProps{ - Stats: loadUserStats(), - Sales: loadSalesData(), - Notifications: loadNotifications(), - }, nil - } -} - -templ (p dashboard) Page(props DashboardProps) { - @html() { -

Dashboard

-

This example demonstrates the new RenderTarget API with standalone function components.

-

Click "Refresh" buttons to see HTMX partial updates - each widget loads only its own data!

-
-
- @UserStatsWidget(props.Stats) -
-
- @SalesChartWidget(props.Sales) -
-
- @NotificationsList(props.Notifications) -
-
-
-

How it works:

-
    -
  • Standalone functions - UserStatsWidget, SalesChartWidget, NotificationsList are shared components
  • -
  • Conditional loading - Props checks target.Is() and loads only needed data
  • -
  • RenderComponent - Passes specific data to each widget
  • -
  • No wrapper methods - No need to create dashboard.UserStats() method!
  • -
  • HTMX integration - HTMXRenderTarget automatically handles partial updates
  • -
-
- } -} - -// Mock data loaders (simulating database queries) -// Using random data to showcase HTMX partial updates -func loadUserStats() UserStats { - return UserStats{ - ActiveUsers: 1000 + rand.IntN(500), - NewToday: 10 + rand.IntN(90), - } -} - -func loadSalesData() SalesData { - points := []DataPoint{ - {Label: "Mon", Value: 30 + rand.IntN(100)}, - {Label: "Tue", Value: 30 + rand.IntN(100)}, - {Label: "Wed", Value: 30 + rand.IntN(100)}, - {Label: "Thu", Value: 30 + rand.IntN(100)}, - {Label: "Fri", Value: 30 + rand.IntN(100)}, - } - total := 0.0 - for _, p := range points { - total += float64(p.Value) * 100.0 - } - return SalesData{ - Points: points, - Total: total, - } -} - -func loadNotifications() []Notification { - messages := []string{ - "New user registered", - "Payment received", - "System update available", - "New order placed", - "Report generated", - "Backup completed", - } - count := 3 + rand.IntN(3) - notifications := make([]Notification, count) - for i := 0; i < count; i++ { - notifications[i] = Notification{ - Message: messages[rand.IntN(len(messages))], - Time: time.Now().Add(-time.Duration(rand.IntN(120)) * time.Minute), - } - } - return notifications -} - -// HTML layout -templ html() { - - - - - - RenderTarget API Example - - - -
- { children... } -
- - -} - -// Error handling -templ errorPage(err error) { - @html() { - @errorComp(err) - } -} - -templ errorComp(err error) { -

Error

-

{ err.Error() }

-} - -// Helper functions -func urlFor(ctx context.Context, page any, args ...any) (string, error) { - s, err := structpages.URLFor(ctx, page, args...) - return s, err -} - -func idFor(ctx context.Context, v any) (string, error) { - return structpages.ID(ctx, v) -} - -func idForTarget(ctx context.Context, v any) (string, error) { - return structpages.IDTarget(ctx, v) -} diff --git a/examples/htmx-render-target/pages.x.go b/examples/htmx-render-target/pages.x.go new file mode 100644 index 0000000..85c5820 --- /dev/null +++ b/examples/htmx-render-target/pages.x.go @@ -0,0 +1,316 @@ +// Code generated by gsx; DO NOT EDIT. + +package main + +import ( + "context" + "io" + "strconv" + + "github.com/gsxhq/gsx" + _gsxstd "github.com/gsxhq/gsx/std" + _gsxf0 "github.com/jackielii/structpages" +) + +// Shared standalone function components (can be used across multiple pages). +// These demonstrate the power of RenderTarget — no wrapper methods needed. + +//line pages.gsx:6:1 +func UserStatsWidget(stats UserStats) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:7:2 + _gsxgw.S("
") +//line pages.gsx:8:3 + _gsxgw.S("

User Statistics

") +//line pages.gsx:9:3 + _gsxgw.S("

Active Users: ") +//line pages.gsx:9:20 + _gsxgw.Text(strconv.FormatInt(int64(stats.ActiveUsers), 10)) + _gsxgw.S("

") +//line pages.gsx:10:3 + _gsxgw.S("

New Today: ") +//line pages.gsx:10:17 + _gsxgw.Text(strconv.FormatInt(int64(stats.NewToday), 10)) + _gsxgw.S("

") +//line pages.gsx:11:3 + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +//line pages.gsx:21:1 +func SalesChartWidget(data SalesData) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:22:2 + _gsxgw.S("
") +//line pages.gsx:23:3 + _gsxgw.S("

Sales Chart

") +//line pages.gsx:24:3 + _gsxgw.S("
") +//line pages.gsx:25:4 + for _, point := range data.Points { +//line pages.gsx:26:5 + _gsxgw.S("
") + } + _gsxgw.S("
") +//line pages.gsx:33:3 + _gsxgw.S("

Total Sales: $") +//line pages.gsx:33:20 + _gsxgw.Text(string(_gsxstd.Format((data.Total), "%.2f"))) + _gsxgw.S("

") +//line pages.gsx:34:3 + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +type NotificationsListProps struct { + Notifications []Notification + Attrs gsx.Attrs +} + +//line pages.gsx:44:1 +func NotificationsList(_gsxp NotificationsListProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + notifications := _gsxp.Notifications + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:45:2 + _gsxgw.S("
") +//line pages.gsx:46:3 + _gsxgw.S("

Recent Notifications

") +//line pages.gsx:47:3 + _gsxgw.S("
    ") +//line pages.gsx:48:4 + for _, n := range notifications { +//line pages.gsx:49:5 + _gsxgw.S("
  • ") +//line pages.gsx:50:6 + _gsxgw.Text(string(n.Message)) + _gsxgw.S(" ") +//line pages.gsx:50:20 + _gsxgw.S("(") +//line pages.gsx:50:28 + _gsxgw.Text(string(n.Time.Format("15:04"))) + _gsxgw.S(")
  • ") + } + _gsxgw.S("
") +//line pages.gsx:54:3 + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +// Dashboard page. + +type dashboard struct{} + +//line pages.gsx:68:1 +func (p dashboard) Page(props dashboardData) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:69:2 + _gsxgw.Node(ctx, Html(HtmlProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:70:3 + _gsxgw.S("

Dashboard

") +//line pages.gsx:71:3 + _gsxgw.S("

This example demonstrates the RenderTarget API with standalone function components.

") +//line pages.gsx:74:3 + _gsxgw.S("

Click \"Refresh\" buttons to see HTMX partial updates — each widget loads only its own data!

") +//line pages.gsx:77:3 + _gsxgw.S("
") +//line pages.gsx:80:4 + _gsxgw.S("") +//line pages.gsx:81:5 + _gsxgw.Node(ctx, UserStatsWidget(props.Stats)) + _gsxgw.S("
") +//line pages.gsx:83:4 + _gsxgw.S("") +//line pages.gsx:84:5 + _gsxgw.Node(ctx, SalesChartWidget(props.Sales)) + _gsxgw.S("") +//line pages.gsx:86:4 + _gsxgw.S("") +//line pages.gsx:87:5 + _gsxgw.Node(ctx, NotificationsList(NotificationsListProps{Notifications: props.Notifications})) + _gsxgw.S("") +//line pages.gsx:90:3 + _gsxgw.S("
") +//line pages.gsx:93:4 + _gsxgw.S("

How it works:

") +//line pages.gsx:94:4 + _gsxgw.S("
    ") +//line pages.gsx:95:5 + _gsxgw.S("
  • ✅ ") +//line pages.gsx:96:10 + _gsxgw.S("Standalone functions — UserStatsWidget, SalesChartWidget, NotificationsList are shared components
  • ") +//line pages.gsx:100:5 + _gsxgw.S("
  • ✅ ") +//line pages.gsx:101:10 + _gsxgw.S("Conditional loading — Props checks target.Is() and loads only needed data
  • ") +//line pages.gsx:105:5 + _gsxgw.S("
  • ✅ ") +//line pages.gsx:106:10 + _gsxgw.S("RenderComponent (direct) — construct the gsx component with its props struct and pass directly
  • ") +//line pages.gsx:110:5 + _gsxgw.S("
  • ✅ ") +//line pages.gsx:111:10 + _gsxgw.S("No wrapper methods — No need to create dashboard.UserStats() method!
  • ") +//line pages.gsx:115:5 + _gsxgw.S("
  • ✅ ") +//line pages.gsx:116:10 + _gsxgw.S("HTMX integration — HTMXRenderTarget automatically handles partial updates
") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +// Html is the full-page layout. + +type HtmlProps struct { + Children gsx.Node +} + +//line pages.gsx:126:1 +func Html(_gsxp HtmlProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + children := _gsxp.Children + _gsxgw := gsx.W(_gsxw) + _gsxgw.S("") +//line pages.gsx:128:2 + _gsxgw.S("") +//line pages.gsx:129:3 + _gsxgw.S("") +//line pages.gsx:130:4 + _gsxgw.S("") +//line pages.gsx:131:4 + _gsxgw.S("") +//line pages.gsx:132:4 + _gsxgw.S("RenderTarget API Example") +//line pages.gsx:133:4 + _gsxgw.S("") +//line pages.gsx:151:3 + _gsxgw.S("") +//line pages.gsx:152:4 + _gsxgw.S("
") +//line pages.gsx:152:10 + _gsxgw.Node(ctx, children) + _gsxgw.S("
") +//line pages.gsx:153:4 + _gsxgw.S("") + return _gsxgw.Err() + }) +} + +type ErrorPageProps struct { + Err error +} + +//line pages.gsx:166:1 +func ErrorPage(_gsxp ErrorPageProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + err := _gsxp.Err + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:167:2 + _gsxgw.Node(ctx, Html(HtmlProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:168:3 + _gsxgw.Node(ctx, ErrorComp(ErrorCompProps{Err: err})) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +type ErrorCompProps struct { + Err error +} + +//line pages.gsx:172:1 +func ErrorComp(_gsxp ErrorCompProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + err := _gsxp.Err + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:173:2 + _gsxgw.S("

Error

") +//line pages.gsx:174:2 + _gsxgw.S("

") +//line pages.gsx:174:5 + _gsxgw.Text(string(err.Error())) + _gsxgw.S("

") + return _gsxgw.Err() + }) +} diff --git a/examples/htmx/go.mod b/examples/htmx/go.mod index 922fd71..8cbbdda 100644 --- a/examples/htmx/go.mod +++ b/examples/htmx/go.mod @@ -5,34 +5,10 @@ go 1.26.1 require github.com/jackielii/structpages v0.0.0-00010101000000-000000000000 require ( - github.com/a-h/templ v0.3.1020 // indirect - github.com/tdewolff/parse/v2 v2.8.13 // indirect -) - -require ( - github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cli/browser v1.3.0 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gsxhq/gsx v0.0.0 github.com/jackielii/ctxkey v1.0.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/natefinch/atomic v1.0.1 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/tools v0.46.0 // indirect ) replace github.com/jackielii/structpages => ../.. -tool ( - github.com/a-h/templ/cmd/templ - github.com/gsxhq/gsx/cmd/gsx -) - replace github.com/gsxhq/gsx => ../../../gsxhq/gsx diff --git a/examples/htmx/go.sum b/examples/htmx/go.sum index c04a9f5..2108c87 100644 --- a/examples/htmx/go.sum +++ b/examples/htmx/go.sum @@ -1,48 +1,4 @@ -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= -github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= -github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= -github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jackielii/ctxkey v1.0.1 h1:CcgbR+fQbrzZJWxI/7Ec4EhzUbmTU1sfI1gV7MAgjIg= github.com/jackielii/ctxkey v1.0.1/go.mod h1:fo4HOwrvSnc3n8o5qZ5L+FVcSyQn+d67CCnlEbH24uc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tdewolff/parse/v2 v2.8.13 h1:si/8rLw5BZZTWCCiMm9A3f6x+RmqYfrkEeXCgpX5ick= -github.com/tdewolff/parse/v2 v2.8.13/go.mod h1:XdsoSFThlVIRIajAuqz1evNY7bagZS8LBOPA3aVopwQ= -github.com/tdewolff/test v1.0.12/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/htmx/pages.gsx b/examples/htmx/pages.gsx new file mode 100644 index 0000000..be1a6da --- /dev/null +++ b/examples/htmx/pages.gsx @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" +) + +type index struct { + product `route:"/product Product"` + team `route:"/team Team"` + contact `route:"/contact Contact"` + throw `route:"/throw Throw"` +} +type product struct{} +type team struct{} +type contact struct{} +type throw struct{} + +component (p index) Page() { + + + +} + +component (p index) Main() { +

Welcome to the Index Page

+

+ Navigate to the product, team, or contact pages using the links below: +

+ url} hx-target="#main">Throw (Err) +} + +component (p product) Page() { + + + +} + +component (p product) Main() { +

Product Page

+

This is the product page.

+} + +component (p team) Page() { + + + +} + +component (p team) Main() { +

Team Page

+

This is the team page.

+} + +component (p contact) Page() { + + + +} + +component (p contact) Main() { +

Contact Page

+

This is the contact page.

+} + +func errFunc() (string, error) { return "", fmt.Errorf("this is an error") } +component (p throw) Page() { +

{ errFunc() }

+} + +component Layout() { + + + + + + HTMX Example + + + +
{ children }
+ + +} + +component ErrorPage(err error) { + + + +} + +component ErrorComp(err error) { +

Error

+

{ err.Error() }

+} diff --git a/examples/htmx/pages.templ b/examples/htmx/pages.templ deleted file mode 100644 index 94c3ce5..0000000 --- a/examples/htmx/pages.templ +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "context" - "github.com/jackielii/structpages" -) - -type index struct { - product `route:"/product Product"` - team `route:"/team Team"` - contact `route:"/contact Contact"` -} - -templ (p index) Page() { - @html() { - @p.Main() - } -} - -templ (index) Main() { -

Welcome to the Index Page

-

Navigate to the product, team, or contact pages using the links below:

-} - -type product struct{} - -templ (p product) Page() { - @html() { - @p.Main() - } -} - -templ (product) Main() { -

Product Page

-

This is the product page.

-} - -type team struct{} - -templ (p team) Page() { - @html() { - @p.Main() - } -} - -templ (team) Main() { -

Team Page

-

This is the team page.

-} - -type contact struct{} - -templ (p contact) Page() { - @html() { - @p.Main() - } -} - -templ (contact) Main() { -

Contact Page

-

This is the contact page.

-} - -templ html() { - - - - - - HTMX Example - - - -
- { children... } -
- - -} - -templ errorPage(err error) { - @html() { - @errorComp(err) - } -} - -templ errorComp(err error) { -

Error

-

{ err.Error() }

-} - -func urlFor(ctx context.Context, page any, args ...any) (templ.SafeURL, error) { - s, err := structpages.URLFor(ctx, page, args...) - return templ.SafeURL(s), err -} diff --git a/examples/htmx/pages.x.go b/examples/htmx/pages.x.go new file mode 100644 index 0000000..e16c952 --- /dev/null +++ b/examples/htmx/pages.x.go @@ -0,0 +1,280 @@ +// Code generated by gsx; DO NOT EDIT. + +package main + +import ( + "context" + "fmt" + "io" + + "github.com/gsxhq/gsx" + _gsxf0 "github.com/jackielii/structpages" +) + +type index struct { + product `route:"/product Product"` + team `route:"/team Team"` + contact `route:"/contact Contact"` + throw `route:"/throw Throw"` +} +type product struct{} +type team struct{} +type contact struct{} +type throw struct{} + +//line pages.gsx:18:1 +func (p index) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:19:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:20:3 + _gsxgw.Node(ctx, p.Main()) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line pages.gsx:24:1 +func (p index) Main() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:25:2 + _gsxgw.S("

Welcome to the Index Page

") +//line pages.gsx:26:2 + _gsxgw.S("

Navigate to the product, team, or contact pages using the links below:

") +//line pages.gsx:29:2 + _gsxgw.S("Throw (Err)") + return _gsxgw.Err() + }) +} + +//line pages.gsx:32:1 +func (p product) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:33:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:34:3 + _gsxgw.Node(ctx, p.Main()) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line pages.gsx:38:1 +func (p product) Main() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:39:2 + _gsxgw.S("

Product Page

") +//line pages.gsx:40:2 + _gsxgw.S("

This is the product page.

") + return _gsxgw.Err() + }) +} + +//line pages.gsx:43:1 +func (p team) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:44:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:45:3 + _gsxgw.Node(ctx, p.Main()) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line pages.gsx:49:1 +func (p team) Main() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:50:2 + _gsxgw.S("

Team Page

") +//line pages.gsx:51:2 + _gsxgw.S("

This is the team page.

") + return _gsxgw.Err() + }) +} + +//line pages.gsx:54:1 +func (p contact) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:55:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:56:3 + _gsxgw.Node(ctx, p.Main()) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line pages.gsx:60:1 +func (p contact) Main() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:61:2 + _gsxgw.S("

Contact Page

") +//line pages.gsx:62:2 + _gsxgw.S("

This is the contact page.

") + return _gsxgw.Err() + }) +} + +func errFunc() (string, error) { return "", fmt.Errorf("this is an error") } + +//line pages.gsx:66:1 +func (p throw) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:67:2 + _gsxgw.S("

") +//line pages.gsx:67:5 + _gsxv1, _gsxerr := errFunc() + if _gsxerr != nil { + return _gsxerr + } + _gsxgw.Text(string(_gsxv1)) + _gsxgw.S("

") + return _gsxgw.Err() + }) +} + +type LayoutProps struct { + Children gsx.Node +} + +//line pages.gsx:70:1 +func Layout(_gsxp LayoutProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + children := _gsxp.Children + _gsxgw := gsx.W(_gsxw) + _gsxgw.S("") +//line pages.gsx:72:2 + _gsxgw.S("") +//line pages.gsx:73:3 + _gsxgw.S("") +//line pages.gsx:74:4 + _gsxgw.S("") +//line pages.gsx:75:4 + _gsxgw.S("") +//line pages.gsx:76:4 + _gsxgw.S("HTMX Example") +//line pages.gsx:78:3 + _gsxgw.S("") +//line pages.gsx:79:4 + _gsxgw.S("
") +//line pages.gsx:80:5 + _gsxgw.S("
") +//line pages.gsx:97:4 + _gsxgw.S("
") +//line pages.gsx:97:20 + _gsxgw.Node(ctx, children) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +type ErrorPageProps struct { + Err error +} + +//line pages.gsx:102:1 +func ErrorPage(_gsxp ErrorPageProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + err := _gsxp.Err + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:103:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:104:3 + _gsxgw.Node(ctx, ErrorComp(ErrorCompProps{Err: err})) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +type ErrorCompProps struct { + Err error +} + +//line pages.gsx:108:1 +func ErrorComp(_gsxp ErrorCompProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + err := _gsxp.Err + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:109:2 + _gsxgw.S("

Error

") +//line pages.gsx:110:2 + _gsxgw.S("

") +//line pages.gsx:110:5 + _gsxgw.Text(string(err.Error())) + _gsxgw.S("

") + return _gsxgw.Err() + }) +} diff --git a/examples/lint-misuse/pages.gsx b/examples/lint-misuse/pages.gsx new file mode 100644 index 0000000..8c5391b --- /dev/null +++ b/examples/lint-misuse/pages.gsx @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "strconv" +) + +// BadLinks mirrors the templ original — it deliberately uses hard-coded +// internal URLs so the structpages-lint [url-attr] rule has targets to flag. +// In gsx the component uses inline params just like the templ version. +// +// NOTE ON gsx AUTO-ESCAPING: gsx escapes URL-context attributes +// (href, hx-get, action, …) by context. That means a dynamically- +// constructed javascript: or data: URL would be neutralised, preventing +// XSS via URL sinks. However, the [url-attr] lint rule catches a +// different problem — routing-correctness: hard-coded path strings that +// bypass structpages.URLFor break when routes are renamed. That class of +// bug is orthogonal to XSS escaping, so gsx's auto-escaping does NOT +// make these findings go away. The lint still has value in gsx projects. +component BadLinks(id int, name string) { + Hard-coded internal + Expression literal + Concat + Sprintf + Bad hx-get +
Bad action
+ External (allowed) +} diff --git a/examples/lint-misuse/pages.templ b/examples/lint-misuse/pages.templ deleted file mode 100644 index 52ce673..0000000 --- a/examples/lint-misuse/pages.templ +++ /dev/null @@ -1,19 +0,0 @@ -package main - -import ( - "fmt" - "strconv" -) - -templ BadLinks(id int, name string) { - Hard-coded internal - Expression literal - Concat - Sprintf - Bad hx-get -
Bad action
- External (allowed) - - - Suppressed -} diff --git a/examples/lint-misuse/pages.x.go b/examples/lint-misuse/pages.x.go new file mode 100644 index 0000000..6420e6c --- /dev/null +++ b/examples/lint-misuse/pages.x.go @@ -0,0 +1,64 @@ +// Code generated by gsx; DO NOT EDIT. + +package main + +import ( + "context" + "fmt" + "io" + "strconv" + + "github.com/gsxhq/gsx" +) + +// BadLinks mirrors the templ original — it deliberately uses hard-coded +// internal URLs so the structpages-lint [url-attr] rule has targets to flag. +// In gsx the component uses inline params just like the templ version. +// +// NOTE ON gsx AUTO-ESCAPING: gsx escapes URL-context attributes +// (href, hx-get, action, …) by context. That means a dynamically- +// constructed javascript: or data: URL would be neutralised, preventing +// XSS via URL sinks. However, the [url-attr] lint rule catches a +// different problem — routing-correctness: hard-coded path strings that +// bypass structpages.URLFor break when routes are renamed. That class of +// bug is orthogonal to XSS escaping, so gsx's auto-escaping does NOT +// make these findings go away. The lint still has value in gsx projects. + +type BadLinksProps struct { + Id int + Name string +} + +//line pages.gsx:20:1 +func BadLinks(_gsxp BadLinksProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + id := _gsxp.Id + name := _gsxp.Name + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:21:2 + _gsxgw.S("Hard-coded internal") +//line pages.gsx:22:2 + _gsxgw.S("Expression literal") +//line pages.gsx:23:2 + _gsxgw.S("Concat") +//line pages.gsx:24:2 + _gsxgw.S("Sprintf") +//line pages.gsx:25:2 + _gsxgw.S("Bad hx-get") +//line pages.gsx:26:2 + _gsxgw.S("
Bad action
") +//line pages.gsx:27:2 + _gsxgw.S("External (allowed)") + return _gsxgw.Err() + }) +} diff --git a/examples/simple/go.mod b/examples/simple/go.mod index 688aa22..d1de761 100644 --- a/examples/simple/go.mod +++ b/examples/simple/go.mod @@ -4,29 +4,11 @@ go 1.26.1 require github.com/jackielii/structpages v0.0.0-00010101000000-000000000000 -require github.com/a-h/templ v0.3.1020 // indirect - require ( - github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cli/browser v1.3.0 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gsxhq/gsx v0.0.0 github.com/jackielii/ctxkey v1.0.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/natefinch/atomic v1.0.1 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/tools v0.46.0 // indirect ) replace github.com/jackielii/structpages => ../.. -tool github.com/a-h/templ/cmd/templ - replace github.com/gsxhq/gsx => ../../../gsxhq/gsx diff --git a/examples/simple/go.sum b/examples/simple/go.sum index df0b1bd..2108c87 100644 --- a/examples/simple/go.sum +++ b/examples/simple/go.sum @@ -1,45 +1,4 @@ -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= -github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= -github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= -github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jackielii/ctxkey v1.0.1 h1:CcgbR+fQbrzZJWxI/7Ec4EhzUbmTU1sfI1gV7MAgjIg= github.com/jackielii/ctxkey v1.0.1/go.mod h1:fo4HOwrvSnc3n8o5qZ5L+FVcSyQn+d67CCnlEbH24uc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/simple/pages.gsx b/examples/simple/pages.gsx new file mode 100644 index 0000000..94861ae --- /dev/null +++ b/examples/simple/pages.gsx @@ -0,0 +1,72 @@ +package main + +// Page structs + route tags are plain Go — pass through unchanged. +type index struct { + product `route:"/product Product"` + team `route:"/team Team"` + contact `route:"/contact Contact"` +} +type product struct{} +type team struct{} +type contact struct{} + +component (p index) Page() { + +

Welcome to the Index Page

+

+ Navigate to the product, team, or contact pages using the links below: +

+
+} + +component (p product) Page() { + +

Product Page

+

This is the product page.

+
+} + +component (p team) Page() { + +

Team Page

+

This is the team page.

+
+} + +component (p contact) Page() { + +

Contact Page

+

This is the contact page.

+
+} + +component Layout() { + + + + + Simple Example + + + +
{ children }
+ + +} diff --git a/examples/simple/pages.templ b/examples/simple/pages.templ deleted file mode 100644 index 91157f8..0000000 --- a/examples/simple/pages.templ +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import "context" -import "github.com/jackielii/structpages" - -type index struct { - product `route:"/product Product"` - team `route:"/team Team"` - contact `route:"/contact Contact"` -} - -templ (index) Page() { - @html() { -

Welcome to the Index Page

-

Navigate to the product, team, or contact pages using the links below:

- } -} - -type product struct{} - -templ (product) Page() { - @html() { -

Product Page

-

This is the product page.

- } -} - -type team struct{} - -templ (team) Page() { - @html() { -

Team Page

-

This is the team page.

- } -} - -type contact struct{} - -templ (contact) Page() { - @html() { -

Contact Page

-

This is the contact page.

- } -} - -templ html() { - - - - - Simple Example - - - -
- { children... } -
- - -} - -func urlFor(ctx context.Context, page any, args ...any) (string, error) { - return structpages.URLFor(ctx, page, args...) -} diff --git a/examples/simple/pages.x.go b/examples/simple/pages.x.go new file mode 100644 index 0000000..7380abf --- /dev/null +++ b/examples/simple/pages.x.go @@ -0,0 +1,168 @@ +// Code generated by gsx; DO NOT EDIT. + +package main + +import ( + "context" + "io" + + "github.com/gsxhq/gsx" + _gsxf0 "github.com/jackielii/structpages" +) + +// Page structs + route tags are plain Go — pass through unchanged. +type index struct { + product `route:"/product Product"` + team `route:"/team Team"` + contact `route:"/contact Contact"` +} +type product struct{} +type team struct{} +type contact struct{} + +//line pages.gsx:13:1 +func (p index) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:14:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:15:3 + _gsxgw.S("

Welcome to the Index Page

") +//line pages.gsx:16:3 + _gsxgw.S("

Navigate to the product, team, or contact pages using the links below:

") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line pages.gsx:22:1 +func (p product) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:23:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:24:3 + _gsxgw.S("

Product Page

") +//line pages.gsx:25:3 + _gsxgw.S("

This is the product page.

") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line pages.gsx:29:1 +func (p team) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:30:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:31:3 + _gsxgw.S("

Team Page

") +//line pages.gsx:32:3 + _gsxgw.S("

This is the team page.

") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line pages.gsx:36:1 +func (p contact) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:37:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:38:3 + _gsxgw.S("

Contact Page

") +//line pages.gsx:39:3 + _gsxgw.S("

This is the contact page.

") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +type LayoutProps struct { + Children gsx.Node +} + +//line pages.gsx:43:1 +func Layout(_gsxp LayoutProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + children := _gsxp.Children + _gsxgw := gsx.W(_gsxw) + _gsxgw.S("") +//line pages.gsx:45:2 + _gsxgw.S("") +//line pages.gsx:46:3 + _gsxgw.S("") +//line pages.gsx:47:4 + _gsxgw.S("") +//line pages.gsx:48:4 + _gsxgw.S("Simple Example") +//line pages.gsx:50:3 + _gsxgw.S("") +//line pages.gsx:51:4 + _gsxgw.S("
") +//line pages.gsx:52:5 + _gsxgw.S("
") +//line pages.gsx:69:4 + _gsxgw.S("
") +//line pages.gsx:69:10 + _gsxgw.Node(ctx, children) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} diff --git a/examples/todo/go.mod b/examples/todo/go.mod index 7ccebcf..0c703ab 100644 --- a/examples/todo/go.mod +++ b/examples/todo/go.mod @@ -4,29 +4,11 @@ go 1.26.1 require github.com/jackielii/structpages v0.0.0 -require github.com/a-h/templ v0.3.1020 // indirect - require ( - github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect - github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cli/browser v1.3.0 // indirect - github.com/fatih/color v1.16.0 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gsxhq/gsx v0.0.0 github.com/jackielii/ctxkey v1.0.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/natefinch/atomic v1.0.1 // indirect - golang.org/x/mod v0.37.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/sync v0.21.0 // indirect - golang.org/x/sys v0.46.0 // indirect - golang.org/x/tools v0.46.0 // indirect ) replace github.com/jackielii/structpages => ../.. -tool github.com/a-h/templ/cmd/templ - replace github.com/gsxhq/gsx => /Users/jackieli/personal/gsxhq/gsx diff --git a/examples/todo/go.sum b/examples/todo/go.sum index df0b1bd..2108c87 100644 --- a/examples/todo/go.sum +++ b/examples/todo/go.sum @@ -1,45 +1,4 @@ -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo= -github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ= -github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw= -github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM= -github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= -github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo= -github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/jackielii/ctxkey v1.0.1 h1:CcgbR+fQbrzZJWxI/7Ec4EhzUbmTU1sfI1gV7MAgjIg= github.com/jackielii/ctxkey v1.0.1/go.mod h1:fo4HOwrvSnc3n8o5qZ5L+FVcSyQn+d67CCnlEbH24uc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= -golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/todo/pages.templ b/examples/todo/pages.gsx similarity index 79% rename from examples/todo/pages.templ rename to examples/todo/pages.gsx index 10fee51..c90223f 100644 --- a/examples/todo/pages.templ +++ b/examples/todo/pages.gsx @@ -1,9 +1,10 @@ package main import ( - "github.com/jackielii/structpages" "net/http" "strconv" + + "github.com/jackielii/structpages" ) type index struct { @@ -12,13 +13,13 @@ type index struct { deleteTodo `route:"DELETE /delete/{id} DeleteTodo"` } -templ (p index) Page() { - @html() { +component (p index) Page() { +

TODO App

url} + hx-target={index.TodoList |> target} hx-swap="innerHTML" hx-on:htmx:after-request="this.reset()" > @@ -32,15 +33,15 @@ templ (p index) Page() {
-
- @p.TodoList() +
id}> +
- } +
} -templ (p index) TodoList() { - @todoList() +component (p index) TodoList() { + } type add struct{} @@ -81,38 +82,38 @@ func (d deleteTodo) ServeHTTP(w http.ResponseWriter, r *http.Request) error { return structpages.RenderComponent(index.TodoList) } -templ todoList() { +component TodoList() {
    - for _, todo := range getTodos() { -
  • + { for _, todo := range getTodos() { +
  • url("id", todo.ID)} + hx-target={index.TodoList |> target} hx-swap="innerHTML" /> { todo.Text }
  • - } + } }
- if len(getTodos()) == 0 { + { if len(getTodos()) == 0 {

No todos yet. Add one above!

- } + } } } -templ html() { +component Layout() { @@ -125,20 +126,20 @@ templ html() { margin: 2rem auto; padding: 2rem; } - + .form-group { display: flex; gap: 0.5rem; margin-bottom: 2rem; } - + .form-group input { flex: 1; padding: 0.75rem; border: 1px solid #ddd; border-radius: 4px; } - + .form-group button { padding: 0.75rem 1.5rem; background: #007bff; @@ -147,16 +148,16 @@ templ html() { border-radius: 4px; cursor: pointer; } - + .form-group button:hover { background: #0056b3; } - + .todo-list { list-style: none; padding: 0; } - + .todo-item { display: flex; align-items: center; @@ -167,26 +168,26 @@ templ html() { margin-bottom: 0.5rem; background: white; } - + .todo-item.completed { opacity: 0.6; } - + .todo-item.completed .todo-text { text-decoration: line-through; } - + .todo-content { display: flex; align-items: center; gap: 0.75rem; flex: 1; } - + .todo-text { flex: 1; } - + .delete-btn { background: #dc3545; color: white; @@ -200,11 +201,11 @@ templ html() { align-items: center; justify-content: center; } - + .delete-btn:hover { background: #c82333; } - + .empty-state { text-align: center; color: #666; @@ -214,20 +215,18 @@ templ html() { -
- { children... } -
+
{ children }
} -templ errorPage(err error) { - @html() { - @errorComp(err) - } +component ErrorPage(err error) { + + + } -templ errorComp(err error) { +component ErrorComp(err error) {

Error

{ err.Error() }

} diff --git a/examples/todo/pages.x.go b/examples/todo/pages.x.go new file mode 100644 index 0000000..8a3cdb1 --- /dev/null +++ b/examples/todo/pages.x.go @@ -0,0 +1,257 @@ +// Code generated by gsx; DO NOT EDIT. + +package main + +import ( + "context" + "io" + "net/http" + "strconv" + + "github.com/gsxhq/gsx" + "github.com/jackielii/structpages" + _gsxf0 "github.com/jackielii/structpages" +) + +type index struct { + add `route:"POST /add AddTodo" form:"text"` + toggle `route:"POST /toggle/{id} ToggleTodo"` + deleteTodo `route:"DELETE /delete/{id} DeleteTodo"` +} + +//line pages.gsx:16:1 +func (p index) Page() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:17:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:18:3 + _gsxgw.S("
") +//line pages.gsx:19:4 + _gsxgw.S("

TODO App

") +//line pages.gsx:20:4 + _gsxgw.S("") +//line pages.gsx:26:5 + _gsxgw.S("
") +//line pages.gsx:27:6 + _gsxgw.S("") +//line pages.gsx:33:6 + _gsxgw.S("
") +//line pages.gsx:36:4 + _gsxgw.S("") +//line pages.gsx:37:5 + _gsxgw.Node(ctx, p.TodoList()) + _gsxgw.S("
") + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +//line pages.gsx:43:1 +func (p index) TodoList() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:44:2 + _gsxgw.Node(ctx, TodoList()) + return _gsxgw.Err() + }) +} + +type add struct{} + +func (a add) ServeHTTP(w http.ResponseWriter, r *http.Request) error { + if r.Method == "POST" { + text := r.FormValue("text") + if text != "" { + addTodo(text) + } + } + return structpages.RenderComponent(index.TodoList) +} + +type toggle struct{} + +func (t toggle) ServeHTTP(w http.ResponseWriter, r *http.Request) error { + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + return err + } + if r.Method == "POST" { + toggleTodo(id) + } + return structpages.RenderComponent(index.TodoList) +} + +type deleteTodo struct{} + +func (d deleteTodo) ServeHTTP(w http.ResponseWriter, r *http.Request) error { + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + return err + } + if r.Method == "DELETE" { + removeTodo(id) + } + return structpages.RenderComponent(index.TodoList) +} + +//line pages.gsx:85:1 +func TodoList() gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:86:2 + _gsxgw.S("
    ") +//line pages.gsx:87:3 + for _, todo := range getTodos() { +//line pages.gsx:88:4 + _gsxgw.S("
  • ") +//line pages.gsx:89:5 + _gsxgw.S("
    ") +//line pages.gsx:90:6 + _gsxgw.S("") +//line pages.gsx:97:6 + _gsxgw.S("") +//line pages.gsx:97:30 + _gsxgw.Text(string(todo.Text)) + _gsxgw.S("
    ") +//line pages.gsx:99:5 + _gsxgw.S("
  • ") + } + _gsxgw.S("
") +//line pages.gsx:111:2 + if len(getTodos()) == 0 { +//line pages.gsx:112:3 + _gsxgw.S("

No todos yet. Add one above!

") + } + return _gsxgw.Err() + }) +} + +type LayoutProps struct { + Children gsx.Node +} + +//line pages.gsx:116:1 +func Layout(_gsxp LayoutProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + children := _gsxp.Children + _gsxgw := gsx.W(_gsxw) + _gsxgw.S("") +//line pages.gsx:118:2 + _gsxgw.S("") +//line pages.gsx:119:3 + _gsxgw.S("") +//line pages.gsx:120:4 + _gsxgw.S("") +//line pages.gsx:121:4 + _gsxgw.S("") +//line pages.gsx:122:4 + _gsxgw.S("TODO App") +//line pages.gsx:123:4 + _gsxgw.S("") +//line pages.gsx:217:3 + _gsxgw.S("") +//line pages.gsx:218:4 + _gsxgw.S("
") +//line pages.gsx:218:23 + _gsxgw.Node(ctx, children) + _gsxgw.S("
") + return _gsxgw.Err() + }) +} + +type ErrorPageProps struct { + Err error +} + +//line pages.gsx:223:1 +func ErrorPage(_gsxp ErrorPageProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + err := _gsxp.Err + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:224:2 + _gsxgw.Node(ctx, Layout(LayoutProps{Children: gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:225:3 + _gsxgw.Node(ctx, ErrorComp(ErrorCompProps{Err: err})) + return _gsxgw.Err() + })})) + return _gsxgw.Err() + }) +} + +type ErrorCompProps struct { + Err error +} + +//line pages.gsx:229:1 +func ErrorComp(_gsxp ErrorCompProps) gsx.Node { + return gsx.Func(func(ctx context.Context, _gsxw io.Writer) error { + err := _gsxp.Err + _gsxgw := gsx.W(_gsxw) +//line pages.gsx:230:2 + _gsxgw.S("

Error

") +//line pages.gsx:231:2 + _gsxgw.S("

") +//line pages.gsx:231:5 + _gsxgw.Text(string(err.Error())) + _gsxgw.S("

") + return _gsxgw.Err() + }) +} diff --git a/gsx.toml b/gsx.toml new file mode 100644 index 0000000..2e544d0 --- /dev/null +++ b/gsx.toml @@ -0,0 +1,13 @@ +# gsx project config, shared by every example module in this repo. +# +# Registers the structpages URL/ID helpers as gsx pipeline filters so templates +# can write `{ page{} |> url }`, `{ x |> id }`, `{ x |> target }` instead of the +# ctx-threading, error-returning structpages.URLFor / ID / IDTarget calls. +# +# The stock `gsx` binary discovers this file by walking up from the example dir +# to the git repo root, so no per-example cmd/gen generator is needed. Generate +# with: gsx generate . +[filters] +url = "github.com/jackielii/structpages.URLFor" +id = "github.com/jackielii/structpages.ID" +target = "github.com/jackielii/structpages.IDTarget"