gromer

#golang#htmx#ssr

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
[![Version](https://badge.fury.io/gh/pyros2097%2Fgromer.svg)](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