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
# gromer

[![Version](https://badge.fury.io/gh/pyros2097%2Fgromer.svg)](https://github.com/pyros2097/gromer)

**gromer** is a framework and cli to build multipage web apps in golang using [htmx](https://htmx.org/) and [alpinejs](https://alpinejs.dev/).

It uses a declarative syntax using inline jsx like templates for components and pages.

It also generates http handlers for your routes which follow a particular folder structure. Similar to other frameworks like nextjs, sveltekit.

You can install this extension [vscode-go-inline-html](https://marketplace.visualstudio.com/items?itemName=pyros2097.vscode-go-inline-html) to get
syntax highlighting for these templates.

# Requirements

```sh
go >= v1.18
```

# Install

```sh
go get -u -v github.com/pyros2097/gromer/cmd/gromer
```

# Using

You need to follow this directory structure similar to nextjs for the api route handlers to be generated and run the gromer command.

[Example](https://github.com/pyros2097/gromer/tree/master/_example)

**These are some components**

`routes/todo.go`

```go
func Todo(c Context, todo *todos.Todo) *Node {
  return c.Render(`
    <li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
      <div class="view">
        <form  hx-target="#todo-{todo.ID}" hx-swap="outerHTML">
          <input type="hidden" name="intent" value="complete" />
          <input type="hidden" name="id" value="{todo.ID}" />
          <input class="checkbox" type="checkbox" checked="{value}" />
        </form>
        <label>{todo.Text}</label>
        <form hx-post="/" hx-target="#todo-{todo.ID}" hx-swap="delete">
          <input type="hidden" name="intent" value="delete" />
          <input type="hidden" name="id" value="{todo.ID}" />
          <button class="destroy"></button>
        </form>
      </div>
    </li>
  `)
}
```

**These are normal page routes**

`routes/get.go`

```go
type GetParams struct {
  Page   int    `json:"page"`
  Filter string `json:"filter"`
}

func GET(c Context, params GetParams) (*Node, int, error) {
  c.Meta("title", "Gromer Todos")
  c.Meta("description", "Gromer Todos")
  c.Meta("author", "gromer")
  c.Meta("keywords", "gromer")
  return c.Render(`
    <div class="todoapp">
      <header class="header">
        <h1>todos</h1>
        <form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
          <input type="hidden" name="intent" value="create" />
          <input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off" />
        </form>
      </header>
      <section class="main">
        <input class="toggle-all" id="toggle-all" type="checkbox" />
        <label for="toggle-all">Mark all as complete</label>
        <TodoList id="todo-list" page="{params.Page}" filter="{params.Filter}" />
      </section>
      <footer class="footer">
        <TodoCount filter="{params.Filter}" />
        <ul class="filters">
          <li>
            <a href="?filter=all">All</a>
          </li>
          <li>
            <a href="?filter=active">Active</a>
          </li>
          <li>
            <a href="?filter=completed">Completed</a>
          </li>
        </ul>
        <form hx-target="#todo-list" hx-post="/">
          <input type="hidden" name="intent" value="clear_completed" />
          <button type="submit" class="clear-completed" >Clear completed</button>
        </form>
      </footer>
    </div>
  `), 200, nil
}
```

And then run the gromer cli command annd it will generate the route handlers in a main.go file,

`main.go`

```go
// Code generated by gromer. DO NOT EDIT.
package main

import (
	"github.com/gorilla/mux"
	"github.com/pyros2097/gromer"
	"github.com/pyros2097/gromer/assets"
	"github.com/pyros2097/gromer/gsx"
	"github.com/rs/zerolog/log"
	"gocloud.dev/server"

	"github.com/pyros2097/gromer/_example/assets"
	"github.com/pyros2097/gromer/_example/components"
	"github.com/pyros2097/gromer/_example/containers"
	"github.com/pyros2097/gromer/_example/routes/404"
	"github.com/pyros2097/gromer/_example/routes"
	"github.com/pyros2097/gromer/_example/routes/about"
	
)

func init() {
	gsx.RegisterComponent(components.Todo, "todo")
	gsx.RegisterComponent(components.Checkbox, "value")
	gsx.RegisterComponent(containers.TodoCount, "filter")
	gsx.RegisterComponent(containers.TodoList, "page", "filter")
}

func main() {
	baseRouter := mux.NewRouter()
	baseRouter.Use(gromer.LogMiddleware)
	baseRouter.NotFoundHandler = gromer.StatusHandler(not_found_404.GET)
	
	staticRouter := baseRouter.NewRoute().Subrouter()
	staticRouter.Use(gromer.CacheMiddleware)
	gromer.StaticRoute(staticRouter, "/gromer/", gromer_assets.FS)
	gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
	gromer.StylesRoute(staticRouter, "/styles.css")

	pageRouter := baseRouter.NewRoute().Subrouter()
	gromer.Handle(pageRouter, "GET", "/", routes.GET)
	gromer.Handle(pageRouter, "POST", "/", routes.POST)
	gromer.Handle(pageRouter, "GET", "/about", about.GET)
	
	log.Info().Msg("http server listening on http://localhost:3000")
	srv := server.New(baseRouter, nil)
	if err := srv.ListenAndServe(":3000"); err != nil {
		log.Fatal().Stack().Err(err).Msg("failed to listen")
	}
}
```

## TODO:

Add inline css formatting

Add inline html formatting