gromer
git clone https://git.pyrossh.dev/gromer
gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.
readme.md
| 3ce8bcf | 1 | # gromer |
| 07e4615 | 2 | |
| a851088 | 3 | [](https://github.com/pyros2097/gromer) |
| a851088 | 4 | |
| a2a840f | 5 | **gromer** is a framework and cli to build multipage web apps in golang using [htmx](https://htmx.org/) and [alpinejs](https://alpinejs.dev/). |
| 0674dcc | 6 | |
| a2a840f | 7 | It uses a declarative syntax using inline jsx like templates for components and pages. |
| 0674dcc | 8 | |
| 0a51af8 | 9 | It also generates http handlers for your routes which follow a particular folder structure. Similar to other frameworks like nextjs, sveltekit. |
| a2a840f | 10 | |
| 0674dcc | 11 | You can install this extension [vscode-go-inline-html](https://marketplace.visualstudio.com/items?itemName=pyros2097.vscode-go-inline-html) to get |
| a2a840f | 12 | syntax highlighting for these templates. |
| eaef29a | 13 | |
| eaef29a | 14 | # Requirements |
| eaef29a | 15 | |
| eaef29a | 16 | ```sh |
| eaef29a | 17 | go >= v1.18 |
| eaef29a | 18 | ``` |
| 0a51af8 | 19 | |
| 0a51af8 | 20 | # Install |
| 395a367 | 21 | |
| 395a367 | 22 | ```sh |
| b7b241e | 23 | go get -u -v github.com/pyros2097/gromer/cmd/gromer |
| 395a367 | 24 | ``` |
| 395a367 | 25 | |
| 3ce8bcf | 26 | # Using |
| 0a51af8 | 27 | |
| 3cf5ff9 | 28 | You need to follow this directory structure similar to nextjs for the api route handlers to be generated and run the gromer command. |
| 327e79a | 29 | |
| 8b959c7 | 30 | [Example](https://github.com/pyros2097/gromer/tree/master/_example) |
| 37ef144 | 31 | |
| a2a840f | 32 | **These are some components** |
| 3444f28 | 33 | |
| a2a840f | 34 | `routes/todo.go` |
| 3444f28 | 35 | |
| a2a840f | 36 | ```go |
| a2a840f | 37 | func Todo(c Context, todo *todos.Todo) *Node { |
| 0674dcc | 38 | return c.Render(` |
| 0674dcc | 39 | <li id="todo-{todo.ID}" class="{ completed: todo.Completed }"> |
| 0674dcc | 40 | <div class="view"> |
| 0674dcc | 41 | <form hx-target="#todo-{todo.ID}" hx-swap="outerHTML"> |
| 0674dcc | 42 | <input type="hidden" name="intent" value="complete" /> |
| 0674dcc | 43 | <input type="hidden" name="id" value="{todo.ID}" /> |
| 0674dcc | 44 | <input class="checkbox" type="checkbox" checked="{value}" /> |
| 0674dcc | 45 | </form> |
| 0674dcc | 46 | <label>{todo.Text}</label> |
| 0674dcc | 47 | <form hx-post="/" hx-target="#todo-{todo.ID}" hx-swap="delete"> |
| 0674dcc | 48 | <input type="hidden" name="intent" value="delete" /> |
| 0674dcc | 49 | <input type="hidden" name="id" value="{todo.ID}" /> |
| 0674dcc | 50 | <button class="destroy"></button> |
| 0674dcc | 51 | </form> |
| 0674dcc | 52 | </div> |
| 0674dcc | 53 | </li> |
| 0674dcc | 54 | `) |
| 3444f28 | 55 | } |
| 3444f28 | 56 | ``` |
| 3444f28 | 57 | |
| a2a840f | 58 | **These are normal page routes** |
| 3444f28 | 59 | |
| a2a840f | 60 | `routes/get.go` |
| 3444f28 | 61 | |
| a2a840f | 62 | ```go |
| 3444f28 | 63 | type GetParams struct { |
| 0674dcc | 64 | Page int `json:"page"` |
| 0674dcc | 65 | Filter string `json:"filter"` |
| 3444f28 | 66 | } |
| 3444f28 | 67 | |
| a2a840f | 68 | func GET(c Context, params GetParams) (*Node, int, error) { |
| 0674dcc | 69 | c.Meta("title", "Gromer Todos") |
| 0674dcc | 70 | c.Meta("description", "Gromer Todos") |
| 0674dcc | 71 | c.Meta("author", "gromer") |
| 0674dcc | 72 | c.Meta("keywords", "gromer") |
| 0674dcc | 73 | return c.Render(` |
| 0674dcc | 74 | <div class="todoapp"> |
| 0674dcc | 75 | <header class="header"> |
| 0674dcc | 76 | <h1>todos</h1> |
| 0674dcc | 77 | <form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''"> |
| 0674dcc | 78 | <input type="hidden" name="intent" value="create" /> |
| 0674dcc | 79 | <input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off" /> |
| 0674dcc | 80 | </form> |
| 0674dcc | 81 | </header> |
| 0674dcc | 82 | <section class="main"> |
| 0674dcc | 83 | <input class="toggle-all" id="toggle-all" type="checkbox" /> |
| 0674dcc | 84 | <label for="toggle-all">Mark all as complete</label> |
| 0674dcc | 85 | <TodoList id="todo-list" page="{params.Page}" filter="{params.Filter}" /> |
| 0674dcc | 86 | </section> |
| 0674dcc | 87 | <footer class="footer"> |
| 0674dcc | 88 | <TodoCount filter="{params.Filter}" /> |
| 0674dcc | 89 | <ul class="filters"> |
| 0674dcc | 90 | <li> |
| 0674dcc | 91 | <a href="?filter=all">All</a> |
| 0674dcc | 92 | </li> |
| 0674dcc | 93 | <li> |
| 0674dcc | 94 | <a href="?filter=active">Active</a> |
| 0674dcc | 95 | </li> |
| 0674dcc | 96 | <li> |
| 0674dcc | 97 | <a href="?filter=completed">Completed</a> |
| 0674dcc | 98 | </li> |
| 0674dcc | 99 | </ul> |
| 0674dcc | 100 | <form hx-target="#todo-list" hx-post="/"> |
| 0674dcc | 101 | <input type="hidden" name="intent" value="clear_completed" /> |
| 0674dcc | 102 | <button type="submit" class="clear-completed" >Clear completed</button> |
| 0674dcc | 103 | </form> |
| 0674dcc | 104 | </footer> |
| 0674dcc | 105 | </div> |
| 0674dcc | 106 | `), 200, nil |
| 3444f28 | 107 | } |
| 3444f28 | 108 | ``` |
| 3444f28 | 109 | |
| a2a840f | 110 | And then run the gromer cli command annd it will generate the route handlers in a main.go file, |
| 3444f28 | 111 | |
| a2a840f | 112 | `main.go` |
| 3444f28 | 113 | |
| 3cf5ff9 | 114 | ```go |
| 3cf5ff9 | 115 | // Code generated by gromer. DO NOT EDIT. |
| 3cf5ff9 | 116 | package main |
| 3cf5ff9 | 117 | |
| 3cf5ff9 | 118 | import ( |
| dae285d | 119 | "github.com/gorilla/mux" |
| dae285d | 120 | "github.com/pyros2097/gromer" |
| dae285d | 121 | "github.com/pyros2097/gromer/assets" |
| dae285d | 122 | "github.com/pyros2097/gromer/gsx" |
| dae285d | 123 | "github.com/rs/zerolog/log" |
| dae285d | 124 | "gocloud.dev/server" |
| dae285d | 125 | |
| dae285d | 126 | "github.com/pyros2097/gromer/_example/assets" |
| dae285d | 127 | "github.com/pyros2097/gromer/_example/components" |
| dae285d | 128 | "github.com/pyros2097/gromer/_example/containers" |
| dae285d | 129 | "github.com/pyros2097/gromer/_example/routes/404" |
| dae285d | 130 | "github.com/pyros2097/gromer/_example/routes" |
| dae285d | 131 | "github.com/pyros2097/gromer/_example/routes/about" |
| dae285d | 132 | |
| 3cf5ff9 | 133 | ) |
| 3cf5ff9 | 134 | |
| 3cf5ff9 | 135 | func init() { |
| dae285d | 136 | gsx.RegisterComponent(components.Todo, "todo") |
| dae285d | 137 | gsx.RegisterComponent(components.Checkbox, "value") |
| dae285d | 138 | gsx.RegisterComponent(containers.TodoCount, "filter") |
| dae285d | 139 | gsx.RegisterComponent(containers.TodoList, "page", "filter") |
| 3cf5ff9 | 140 | } |
| 3cf5ff9 | 141 | |
| 3cf5ff9 | 142 | func main() { |
| dae285d | 143 | baseRouter := mux.NewRouter() |
| dae285d | 144 | baseRouter.Use(gromer.LogMiddleware) |
| dae285d | 145 | baseRouter.NotFoundHandler = gromer.StatusHandler(not_found_404.GET) |
| dae285d | 146 | |
| dae285d | 147 | staticRouter := baseRouter.NewRoute().Subrouter() |
| dae285d | 148 | staticRouter.Use(gromer.CacheMiddleware) |
| dae285d | 149 | gromer.StaticRoute(staticRouter, "/gromer/", gromer_assets.FS) |
| dae285d | 150 | gromer.StaticRoute(staticRouter, "/assets/", assets.FS) |
| dae285d | 151 | gromer.StylesRoute(staticRouter, "/styles.css") |
| dae285d | 152 | |
| dae285d | 153 | pageRouter := baseRouter.NewRoute().Subrouter() |
| dae285d | 154 | gromer.Handle(pageRouter, "GET", "/", routes.GET) |
| dae285d | 155 | gromer.Handle(pageRouter, "POST", "/", routes.POST) |
| dae285d | 156 | gromer.Handle(pageRouter, "GET", "/about", about.GET) |
| dae285d | 157 | |
| dae285d | 158 | log.Info().Msg("http server listening on http://localhost:3000") |
| dae285d | 159 | srv := server.New(baseRouter, nil) |
| dae285d | 160 | if err := srv.ListenAndServe(":3000"); err != nil { |
| dae285d | 161 | log.Fatal().Stack().Err(err).Msg("failed to listen") |
| dae285d | 162 | } |
| 3cf5ff9 | 163 | } |
| 90c603c | 164 | ``` |
| 90c603c | 165 | |
| 523be0b | 166 | ## TODO: |
| a2a840f | 167 | |
| 90c603c | 168 | Add inline css formatting |
| a2a840f | 169 | |
| a2a840f | 170 | Add inline html formatting |