~repos /gromer

#golang#htmx#ssr

git clone https://pyrossh.dev/repos/gromer.git

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


3c331503 Peter John

3 years ago
improve example
_example/assets/js/htmx.json-enc.js DELETED
@@ -1,12 +0,0 @@
1
- htmx.defineExtension('json-enc', {
2
- onEvent: function (name, evt) {
3
- if (name === "htmx:configRequest") {
4
- evt.detail.headers['Content-Type'] = "application/json";
5
- }
6
- },
7
-
8
- encodeParameters : function(xhr, parameters, elt) {
9
- xhr.overrideMimeType('text/json');
10
- return (JSON.stringify(parameters));
11
- }
12
- });
_example/components/header.go DELETED
@@ -1,65 +0,0 @@
1
- package components
2
-
3
- import (
4
- . "github.com/pyros2097/gromer/handlebars"
5
- )
6
-
7
- var _ = Css(`
8
- `)
9
-
10
- func Header() *Template {
11
- return Html(`
12
- <header>
13
- <nav class="navbar" role="navigation" aria-label="main navigation">
14
- <div class="navbar-brand">
15
- <a class="navbar-item" href="https://bulma.io">
16
- <img src="https://bulma.io/images/bulma-logo.png" width="112" height="28">
17
- </a>
18
-
19
- <a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
20
- <span aria-hidden="true"></span>
21
- <span aria-hidden="true"></span>
22
- <span aria-hidden="true"></span>
23
- </a>
24
- </div>
25
-
26
- <div id="navbarBasicExample" class="navbar-menu">
27
- <div class="navbar-start">
28
- <a class="navbar-item" href="/">
29
- Home
30
- </a>
31
-
32
- <a class="navbar-item" href="/about">
33
- About
34
- </a>
35
-
36
- <a class="navbar-item" href="/clock">
37
- Clock
38
- </a>
39
-
40
- <a class="navbar-item" href="/counter">
41
- Counter
42
- </a>
43
-
44
- <a class="navbar-item" href="/api">
45
- API
46
- </a>
47
- </div>
48
-
49
- <div class="navbar-end">
50
- <div class="navbar-item">
51
- <div class="buttons">
52
- <a class="button is-primary">
53
- <strong>Sign up</strong>
54
- </a>
55
- <a class="button is-light">
56
- Log in
57
- </a>
58
- </div>
59
- </div>
60
- </div>
61
- </div>
62
- </nav>
63
- </header>
64
- `)
65
- }
_example/components/page.go CHANGED
@@ -30,7 +30,6 @@ func Page(props PageProps) *Template {
30
30
  <link rel="stylesheet" href="{{ todoCssUrl }}" />
31
31
  <link rel="stylesheet" href="{{ stylesCssUrl }}" />
32
32
  <script src="{{ htmxJsUrl }}"></script>
33
- <script src="{{ htmxJsonUrl }}"></script>
34
33
  <script src="{{ alpineJsUrl }}" defer=""></script>
35
34
  </head>
36
35
  <body>
@@ -42,7 +41,6 @@ func Page(props PageProps) *Template {
42
41
  "todoCssUrl", gromer.GetAssetUrl(assets.FS, "css/todo.css"),
43
42
  "stylesCssUrl", gromer.GetStylesUrl(),
44
43
  "htmxJsUrl", gromer.GetAssetUrl(assets.FS, "js/htmx@1.7.0.js"),
45
- "htmxJsonUrl", gromer.GetAssetUrl(assets.FS, "js/htmx.json-enc.js"),
46
44
  "alpineJsUrl", gromer.GetAssetUrl(assets.FS, "js/alpinejs@3.9.6.js"),
47
45
  )
48
46
  }
_example/components/todo.go CHANGED
@@ -1,21 +1,33 @@
1
1
  package components
2
2
 
3
3
  import (
4
- "github.com/pyros2097/gromer/_example/services"
4
+ "github.com/pyros2097/gromer/_example/services/todos"
5
5
  . "github.com/pyros2097/gromer/handlebars"
6
6
  )
7
7
 
8
+ var _ = Css(`
9
+ `)
10
+
8
11
  type TodoProps struct {
9
- Todo *services.Todo `json:"todo"`
12
+ Todo *todos.Todo `json:"todo"`
10
13
  }
11
14
 
12
15
  func Todo(props TodoProps) *Template {
13
16
  return Html(`
14
17
  <li id="todo-{{ props.Todo.ID }}" {{#if props.Todo.Completed }} class="completed" {{/if}}>
15
18
  <div class="view">
16
- <input class="toggle" hx-post="/api/todos/{{ props.Todo.ID }}/complete" type="checkbox" {{#if props.Todo.Completed }} checked="" {{/if}} hx-target="#todo-{{ props.Todo.ID }}" hx-swap="outerHTML">
17
- <label hx-get="/todos/edit/{{ props.Todo.ID }}" hx-target="#todo-{{ props.Todo.ID }}" hx-swap="outerHTML">{{ props.Todo.Text }}</label>
18
- <button class="destroy" hx-delete="/api/todos/{{ props.Todo.ID }}" hx-target="#todo-{{ props.Todo.ID }}" hx-swap="delete"></button>
19
+ <form hx-target="#todo-{{ props.Todo.ID }}" hx-swap="outerHTML">
20
+ <input type="hidden" name="intent" value="complete" />
21
+ <input type="hidden" name="id" value="{{ props.Todo.ID }}" />
22
+ <input hx-post="/" class="checkbox" type="checkbox" {{#if props.Todo.Completed }} checked="" {{/if}} />
23
+ </form>
24
+ <label>{{ props.Todo.Text }}</label>
25
+ <!-- <label hx-get="/todos/edit/{{ props.Todo.ID }}" hx-target="#todo-{{ props.Todo.ID }}" hx-swap="outerHTML">{{ props.Todo.Text }}</label> -->
26
+ <form hx-post="/" hx-target="#todo-{{ props.Todo.ID }}" hx-swap="delete">
27
+ <input type="hidden" name="intent" value="delete" />
28
+ <input type="hidden" name="id" value="{{ props.Todo.ID }}" />
29
+ <button class="destroy"></button>
30
+ </form>
19
31
  </div>
20
32
  </li>
21
33
  `)
_example/containers/TodoCount.go ADDED
@@ -0,0 +1,33 @@
1
+ package containers
2
+
3
+ import (
4
+ "context"
5
+
6
+ . "github.com/pyros2097/gromer"
7
+ "github.com/pyros2097/gromer/_example/services/todos"
8
+ . "github.com/pyros2097/gromer/handlebars"
9
+ )
10
+
11
+ var _ = Css(`
12
+ `)
13
+
14
+ type TodoCountProps struct {
15
+ Page int `json:"page"`
16
+ Filter string `json:"filter"`
17
+ }
18
+
19
+ func TodoCount(ctx context.Context, props TodoCountProps) (*Template, error) {
20
+ index := Default(props.Page, 1)
21
+ todos, err := todos.GetAllTodo(ctx, todos.GetAllTodoParams{
22
+ Filter: props.Filter,
23
+ Limit: index,
24
+ })
25
+ if err != nil {
26
+ return nil, err
27
+ }
28
+ return Html(`
29
+ <span class="todo-count" id="todo-count" hx-swap-oob="true">
30
+ <strong>{{ count }}</strong> item left
31
+ </span>
32
+ `).Prop("count", len(todos)), nil
33
+ }
_example/containers/TodoList.go ADDED
@@ -0,0 +1,36 @@
1
+ package containers
2
+
3
+ import (
4
+ "context"
5
+
6
+ . "github.com/pyros2097/gromer"
7
+ "github.com/pyros2097/gromer/_example/services/todos"
8
+ . "github.com/pyros2097/gromer/handlebars"
9
+ )
10
+
11
+ var _ = Css(`
12
+ `)
13
+
14
+ type TodoListProps struct {
15
+ ID string `json:"id"`
16
+ Page int `json:"page"`
17
+ Filter string `json:"filter"`
18
+ }
19
+
20
+ func TodoList(ctx context.Context, props TodoListProps) (*Template, error) {
21
+ index := Default(props.Page, 1)
22
+ todos, err := todos.GetAllTodo(ctx, todos.GetAllTodoParams{
23
+ Filter: props.Filter,
24
+ Limit: index,
25
+ })
26
+ if err != nil {
27
+ return nil, err
28
+ }
29
+ return Html(`
30
+ <ul id="todo-list" class="todo-list">
31
+ {{#each todos as |todo|}}
32
+ {{#Todo todo=todo}}{{/Todo}}
33
+ {{/each}}
34
+ </ul>
35
+ `).Prop("todos", todos), nil
36
+ }
_example/main.go CHANGED
@@ -2,8 +2,6 @@
2
2
  package main
3
3
 
4
4
  import (
5
- "os"
6
-
7
5
  "github.com/gorilla/mux"
8
6
  "github.com/pyros2097/gromer"
9
7
  "github.com/rs/zerolog/log"
@@ -11,26 +9,23 @@ import (
11
9
 
12
10
  "github.com/pyros2097/gromer/_example/assets"
13
11
  "github.com/pyros2097/gromer/_example/components"
12
+ "github.com/pyros2097/gromer/_example/containers"
14
13
  "github.com/pyros2097/gromer/_example/pages/404"
15
14
  "github.com/pyros2097/gromer/_example/pages"
16
15
  "github.com/pyros2097/gromer/_example/pages/about"
17
- "github.com/pyros2097/gromer/_example/pages/api/recover"
18
- "github.com/pyros2097/gromer/_example/pages/api/todos"
19
- "github.com/pyros2097/gromer/_example/pages/api/todos/_todoId_"
20
- "github.com/pyros2097/gromer/_example/pages/api/todos/_todoId_/complete"
21
- "github.com/pyros2097/gromer/_example/pages/todos"
22
16
 
23
17
  )
24
18
 
25
19
  func init() {
26
- gromer.RegisterComponent(components.Header)
27
20
  gromer.RegisterComponent(components.Page)
28
21
  gromer.RegisterComponent(components.Todo)
29
22
 
23
+ gromer.RegisterContainer(containers.TodoCount)
24
+ gromer.RegisterContainer(containers.TodoList)
25
+
30
26
  }
31
27
 
32
28
  func main() {
33
- port := os.Getenv("PORT")
34
29
  baseRouter := mux.NewRouter()
35
30
  baseRouter.Use(gromer.LogMiddleware)
36
31
 
@@ -44,25 +39,18 @@ func main() {
44
39
  pageRouter := baseRouter.NewRoute().Subrouter()
45
40
  gromer.ApiExplorerRoute(pageRouter, "/explorer")
46
41
  gromer.Handle(pageRouter, "GET", "/", pages.GET)
42
+ gromer.Handle(pageRouter, "POST", "/", pages.POST)
47
43
  gromer.Handle(pageRouter, "GET", "/about", about.GET)
48
- gromer.Handle(pageRouter, "GET", "/todos", todos_page.GET)
49
- gromer.Handle(pageRouter, "POST", "/todos", todos_page.POST)
50
44
 
51
45
 
52
46
  apiRouter := baseRouter.NewRoute().Subrouter()
53
47
  apiRouter.Use(gromer.CorsMiddleware)
54
- gromer.Handle(apiRouter, "GET", "/api/recover", recover.GET)
55
- gromer.Handle(apiRouter, "GET", "/api/todos", todos.GET)
56
- gromer.Handle(apiRouter, "POST", "/api/todos", todos.POST)
57
- gromer.Handle(apiRouter, "DELETE", "/api/todos/{todoId}", todos_todoId_.DELETE)
58
- gromer.Handle(apiRouter, "GET", "/api/todos/{todoId}", todos_todoId_.GET)
59
- gromer.Handle(apiRouter, "POST", "/api/todos/{todoId}/complete", todos_complete.POST)
60
48
 
61
49
 
62
50
 
63
- log.Info().Msg("http server listening on http://localhost:"+port)
51
+ log.Info().Msg("http server listening on http://localhost:3000")
64
52
  srv := server.New(baseRouter, nil)
65
- if err := srv.ListenAndServe(":"+port); err != nil {
53
+ if err := srv.ListenAndServe(":3000"); err != nil {
66
54
  log.Fatal().Stack().Err(err).Msg("failed to listen")
67
55
  }
68
56
  }
_example/makefile CHANGED
@@ -5,7 +5,6 @@ setup:
5
5
  update:
6
6
  gromer -pkg github.com/pyros2097/gromer/_example
7
7
 
8
- run: export PORT=3000
9
8
  run:
10
9
  gow run main.go
11
10
 
_example/pages/api/recover/get.go DELETED
@@ -1,17 +0,0 @@
1
- package recover
2
-
3
- import (
4
- "context"
5
- "fmt"
6
- )
7
-
8
- type GetParams struct {
9
- Limit int `json:"limit"`
10
- }
11
-
12
- func GET(ctx context.Context, params GetParams) (*GetParams, int, error) {
13
- arr := []string{}
14
- v := arr[55]
15
- fmt.Printf("%s", v)
16
- return &params, 200, nil
17
- }
_example/pages/api/todos/_todoId_/complete/post.go DELETED
@@ -1,22 +0,0 @@
1
- package todos_complete
2
-
3
- import (
4
- "context"
5
-
6
- todos_todoId_ "github.com/pyros2097/gromer/_example/pages/api/todos/_todoId_"
7
- "github.com/pyros2097/gromer/_example/services"
8
- )
9
-
10
- func POST(ctx context.Context, id string) (*services.Todo, int, error) {
11
- _, status, err := todos_todoId_.GET(ctx, id)
12
- if err != nil {
13
- return nil, status, err
14
- }
15
- todo, err := services.UpdateTodo(ctx, id, services.UpdateTodoParams{
16
- Completed: true,
17
- })
18
- if err != nil {
19
- return nil, 500, err
20
- }
21
- return todo, 200, nil
22
- }
_example/pages/api/todos/_todoId_/delete.go DELETED
@@ -1,19 +0,0 @@
1
- package todos_todoId_
2
-
3
- import (
4
- "context"
5
-
6
- "github.com/pyros2097/gromer/_example/services"
7
- )
8
-
9
- func DELETE(ctx context.Context, id string) (string, int, error) {
10
- _, status, err := GET(ctx, id)
11
- if err != nil {
12
- return "", status, err
13
- }
14
- _, err = services.DeleteTodo(ctx, id)
15
- if err != nil {
16
- return id, 500, err
17
- }
18
- return id, 200, nil
19
- }
_example/pages/api/todos/_todoId_/get.go DELETED
@@ -1,15 +0,0 @@
1
- package todos_todoId_
2
-
3
- import (
4
- "context"
5
-
6
- "github.com/pyros2097/gromer/_example/services"
7
- )
8
-
9
- func GET(ctx context.Context, id string) (*services.Todo, int, error) {
10
- todo, err := services.GetTodo(ctx, id)
11
- if err != nil {
12
- return nil, 500, err
13
- }
14
- return todo, 200, nil
15
- }
_example/pages/api/todos/get.go DELETED
@@ -1,39 +0,0 @@
1
- package todos
2
-
3
- import (
4
- "context"
5
-
6
- . "github.com/pyros2097/gromer"
7
- "github.com/pyros2097/gromer/_example/services"
8
- )
9
-
10
- type GetParams struct {
11
- Limit int `json:"limit"`
12
- Filter string `json:"filter"`
13
- }
14
-
15
- func GET(ctx context.Context, params GetParams) ([]*services.Todo, int, error) {
16
- limit := Default(params.Limit, 10)
17
- todos := services.GetAllTodo(ctx, services.GetAllTodoParams{
18
- Limit: limit,
19
- })
20
- if params.Filter == "completed" {
21
- newTodos := []*services.Todo{}
22
- for _, v := range todos {
23
- if v.Completed {
24
- newTodos = append(newTodos, v)
25
- }
26
- }
27
- return newTodos, 200, nil
28
- }
29
- if params.Filter == "active" {
30
- newTodos := []*services.Todo{}
31
- for _, v := range todos {
32
- if !v.Completed {
33
- newTodos = append(newTodos, v)
34
- }
35
- }
36
- return newTodos, 200, nil
37
- }
38
- return todos, 200, nil
39
- }
_example/pages/api/todos/post.go DELETED
@@ -1,27 +0,0 @@
1
- package todos
2
-
3
- import (
4
- "context"
5
- "time"
6
-
7
- "github.com/google/uuid"
8
- "github.com/pyros2097/gromer/_example/services"
9
- )
10
-
11
- type PostParams struct {
12
- Text string `json:"text"`
13
- }
14
-
15
- func POST(ctx context.Context, b PostParams) (*services.Todo, int, error) {
16
- todo, err := services.CreateTodo(ctx, services.Todo{
17
- ID: uuid.New().String(),
18
- Text: b.Text,
19
- Completed: false,
20
- CreatedAt: time.Now(),
21
- UpdatedAt: time.Now(),
22
- })
23
- if err != nil {
24
- return nil, 500, err
25
- }
26
- return todo, 200, nil
27
- }
_example/pages/get.go CHANGED
@@ -8,14 +8,48 @@ import (
8
8
  )
9
9
 
10
10
  type GetParams struct {
11
+ Page int `json:"page"`
12
+ Filter string `json:"filter"`
11
13
  }
12
14
 
13
15
  func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
14
16
  return Html(`
15
17
  {{#Page title="gromer example"}}
18
+ <section class="todoapp">
19
+ <header class="header">
20
+ <h1>todos</h1>
21
+ <form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
22
+ <input type="hidden" name="intent" value="create" />
23
+ <input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off">
24
+ </form>
16
- {{#Header}}{{/Header}}
25
+ </header>
26
+ <section class="main">
27
+ <input class="toggle-all" id="toggle-all" type="checkbox">
28
+ <label for="toggle-all">Mark all as complete</label>
29
+ {{#TodoList id="todo-list" page=page filter=filter }}{{/TodoList}}
30
+ </section>
31
+ <footer class="footer">
32
+ {{#TodoCount page=page filter=filter}}{{/TodoCount}}
33
+ <ul class="filters">
17
- Home Page
34
+ <li>
35
+ <a href="?filter=all">All</a>
36
+ </li>
37
+ <li>
38
+ <a href="?filter=active">Active</a>
39
+ </li>
40
+ <li>
41
+ <a href="?filter=completed">Completed</a>
42
+ </li>
43
+ </ul>
44
+ <form hx-target="#todo-list" hx-post="/">
45
+ <input type="hidden" name="intent" value="clear_completed" />
46
+ <button type="submit" class="clear-completed" >Clear completed</button>
47
+ </form>
48
+ </footer>
49
+ </section>
18
50
  {{/Page}}
19
51
  `).
52
+ Prop("page", params.Page).
53
+ Prop("filter", params.Filter).
20
54
  Render()
21
55
  }
_example/pages/post.go ADDED
@@ -0,0 +1,72 @@
1
+ package pages
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ _ "github.com/pyros2097/gromer/_example/components"
8
+ "github.com/pyros2097/gromer/_example/services/todos"
9
+ . "github.com/pyros2097/gromer/handlebars"
10
+ )
11
+
12
+ type PostParams struct {
13
+ Intent string `json:"intent"`
14
+ ID string `json:"id"`
15
+ Text string `json:"text"`
16
+ }
17
+
18
+ func POST(ctx context.Context, params PostParams) (HtmlContent, int, error) {
19
+ if params.Intent == "clear_completed" {
20
+ allTodos, err := todos.GetAllTodo(ctx, todos.GetAllTodoParams{
21
+ Filter: "all",
22
+ Limit: 1000,
23
+ })
24
+ if err != nil {
25
+ return HtmlErr(500, err)
26
+ }
27
+ for _, t := range allTodos {
28
+ _, err := todos.DeleteTodo(ctx, t.ID)
29
+ if err != nil {
30
+ return HtmlErr(500, err)
31
+ }
32
+ }
33
+ return Html(`
34
+ {{#TodoList id="todo-list"}}{{/TodoList}}
35
+ `).Render()
36
+ } else if params.Intent == "create" {
37
+ todo, err := todos.CreateTodo(ctx, params.Text)
38
+ if err != nil {
39
+ return HtmlErr(500, err)
40
+ }
41
+ return Html(`
42
+ {{#Todo todo=todo}}{{/Todo}}
43
+ {{#TodoCount filter="all" page=1}}{{/TodoCount}}
44
+ `).
45
+ Prop("todo", todo).
46
+ Render()
47
+ } else if params.Intent == "delete" {
48
+ _, err := todos.DeleteTodo(ctx, params.ID)
49
+ if err != nil {
50
+ return HtmlErr(500, err)
51
+ }
52
+ return HtmlEmpty()
53
+ } else if params.Intent == "complete" {
54
+ todo, err := todos.GetTodo(ctx, params.ID)
55
+ if err != nil {
56
+ return HtmlErr(500, err)
57
+ }
58
+ _, err = todos.UpdateTodo(ctx, params.ID, todos.UpdateTodoParams{
59
+ Text: todo.Text,
60
+ Completed: !todo.Completed,
61
+ })
62
+ if err != nil {
63
+ return HtmlErr(500, err)
64
+ }
65
+ return Html(`
66
+ {{#Todo todo=todo}}{{/Todo}}
67
+ `).
68
+ Prop("todo", todo).
69
+ Render()
70
+ }
71
+ return HtmlErr(404, fmt.Errorf("Intent not specified: %s", params.Intent))
72
+ }
_example/pages/todos/get.go DELETED
@@ -1,69 +0,0 @@
1
- package todos_page
2
-
3
- import (
4
- "context"
5
-
6
- . "github.com/pyros2097/gromer"
7
- _ "github.com/pyros2097/gromer/_example/components"
8
- "github.com/pyros2097/gromer/_example/pages/api/todos"
9
- . "github.com/pyros2097/gromer/handlebars"
10
- )
11
-
12
- type GetParams struct {
13
- Filter string `json:"filter"`
14
- Page int `json:"limit"`
15
- }
16
-
17
- func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
18
- index := Default(params.Page, 1)
19
- todos, status, err := todos.GET(ctx, todos.GetParams{
20
- Filter: params.Filter,
21
- Limit: index * 10,
22
- })
23
- if err != nil {
24
- return HtmlErr(status, err)
25
- }
26
- return Html(`
27
- {{#Page title="gromer example"}}
28
- {{#Header}}{{/Header}}
29
- <section class="todoapp">
30
- <header class="header">
31
- <h1>todos</h1>
32
- <form hx-post="/todos" hx-target="#todo-list" hx-swap="afterbegin" hx-ext="json-enc" _="on htmx:afterOnLoad set #text.value to ''">
33
- <input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off">
34
- </form>
35
- </header>
36
- <section class="main">
37
- <input class="toggle-all" id="toggle-all" type="checkbox">
38
- <label for="toggle-all">Mark all as complete</label>
39
- <ul id="todo-list" class="todo-list">
40
- {{#each todos as |todo|}}
41
- {{#Todo todo=todo}}{{/Todo}}
42
- {{/each}}
43
- </ul>
44
- </section>
45
- <footer class="footer">
46
- <!-- This should be '0 items left' by default-->
47
- <span class="todo-count" id="todo-count" hx-swap-oob="true">
48
- <strong>1</strong> item left </span>
49
- <!-- Remove this if you don't implement routing-->
50
- <ul class="filters">
51
- <li>
52
- <a href="/todos?filter=all">All</a>
53
- </li>
54
- <li>
55
- <a href="/todos?filter=active">Active</a>
56
- </li>
57
- <li>
58
- <a href="/todos?filter=completed">Completed</a>
59
- </li>
60
- </ul>
61
- <!-- Hidden if no completed items are left ↓-->
62
- <button class="clear-completed" hx-post="/todos/clear-completed" hx-target="#todo-list">Clear completed</button>
63
- </footer>
64
- </section>
65
- {{/Page}}
66
- `).
67
- Prop("todos", todos).
68
- Render()
69
- }
_example/pages/todos/post.go DELETED
@@ -1,20 +0,0 @@
1
- package todos_page
2
-
3
- import (
4
- "context"
5
-
6
- "github.com/pyros2097/gromer/_example/pages/api/todos"
7
- . "github.com/pyros2097/gromer/handlebars"
8
- )
9
-
10
- func POST(ctx context.Context, b todos.PostParams) (HtmlContent, int, error) {
11
- todo, status, err := todos.POST(ctx, b)
12
- if err != nil {
13
- return HtmlErr(status, err)
14
- }
15
- return Html(`
16
- {{#Todo todo=todo}}{{/Todo}}
17
- `).
18
- Prop("todo", todo).
19
- Render()
20
- }
_example/services/todo.go DELETED
@@ -1,77 +0,0 @@
1
- // Code generated by sqlc. DO NOT EDIT.
2
-
3
- package services
4
-
5
- import (
6
- "context"
7
- "errors"
8
- "time"
9
- )
10
-
11
- type Todo struct {
12
- ID string `json:"id"`
13
- Text string `json:"text"`
14
- Completed bool `json:"completed"`
15
- CreatedAt time.Time `json:"createdAt"`
16
- UpdatedAt time.Time `json:"updatedAt"`
17
- }
18
-
19
- var todos = []*Todo{}
20
-
21
- func CreateTodo(ctx context.Context, todo Todo) (*Todo, error) {
22
- todos = append(todos, &todo)
23
- return &todo, nil
24
- }
25
-
26
- type UpdateTodoParams struct {
27
- Text string `json:"text"`
28
- Completed bool `json:"completed"`
29
- }
30
-
31
- func UpdateTodo(ctx context.Context, id string, params UpdateTodoParams) (*Todo, error) {
32
- updateIndex := -1
33
- for i, todo := range todos {
34
- if todo.ID == id {
35
- updateIndex = i
36
- }
37
- }
38
- if updateIndex != -1 {
39
- todos[updateIndex].Text = params.Text
40
- todos[updateIndex].Completed = params.Completed
41
- todos[updateIndex].UpdatedAt = time.Now()
42
- return todos[updateIndex], nil
43
- }
44
- return nil, errors.New("Todo not found")
45
- }
46
-
47
- func DeleteTodo(ctx context.Context, id string) (string, error) {
48
- deleteIndex := -1
49
- for i, todo := range todos {
50
- if todo.ID == id {
51
- deleteIndex = i
52
- }
53
- }
54
- if deleteIndex != -1 {
55
- todos = append(todos[:deleteIndex], todos[deleteIndex+1:]...)
56
- return id, nil
57
- }
58
- return "", errors.New("Todo not found")
59
- }
60
-
61
- func GetTodo(ctx context.Context, id string) (*Todo, error) {
62
- for _, todo := range todos {
63
- if todo.ID == id {
64
- return todo, nil
65
- }
66
- }
67
- return nil, errors.New("Todo not found")
68
- }
69
-
70
-
71
- type GetAllTodoParams struct {
72
- Limit int `json:"limit"`
73
- }
74
-
75
- func GetAllTodo(ctx context.Context, params GetAllTodoParams) []*Todo {
76
- return todos
77
- }
_example/services/todos/todo.go ADDED
@@ -0,0 +1,103 @@
1
+ package todos
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "time"
7
+
8
+ "github.com/google/uuid"
9
+ )
10
+
11
+ type Todo struct {
12
+ ID string `json:"id"`
13
+ Text string `json:"text"`
14
+ Completed bool `json:"completed"`
15
+ CreatedAt time.Time `json:"createdAt"`
16
+ UpdatedAt time.Time `json:"updatedAt"`
17
+ }
18
+
19
+ var globalTodos = []*Todo{}
20
+
21
+ func CreateTodo(ctx context.Context, text string) (*Todo, error) {
22
+ todo := &Todo{
23
+ ID: uuid.New().String(),
24
+ Text: text,
25
+ Completed: false,
26
+ CreatedAt: time.Now(),
27
+ UpdatedAt: time.Now(),
28
+ }
29
+ globalTodos = append(globalTodos, todo)
30
+ return todo, nil
31
+ }
32
+
33
+ type UpdateTodoParams struct {
34
+ Text string `json:"text"`
35
+ Completed bool `json:"completed"`
36
+ }
37
+
38
+ func UpdateTodo(ctx context.Context, id string, params UpdateTodoParams) (*Todo, error) {
39
+ updateIndex := -1
40
+ for i, todo := range globalTodos {
41
+ if todo.ID == id {
42
+ updateIndex = i
43
+ }
44
+ }
45
+ if updateIndex != -1 {
46
+ globalTodos[updateIndex].Text = params.Text
47
+ globalTodos[updateIndex].Completed = params.Completed
48
+ globalTodos[updateIndex].UpdatedAt = time.Now()
49
+ return globalTodos[updateIndex], nil
50
+ }
51
+ return nil, errors.New("Todo not found")
52
+ }
53
+
54
+ func DeleteTodo(ctx context.Context, id string) (string, error) {
55
+ deleteIndex := -1
56
+ for i, todo := range globalTodos {
57
+ if todo.ID == id {
58
+ deleteIndex = i
59
+ }
60
+ }
61
+ if deleteIndex != -1 {
62
+ globalTodos = append(globalTodos[:deleteIndex], globalTodos[deleteIndex+1:]...)
63
+ return id, nil
64
+ }
65
+ return "", errors.New("Todo not found")
66
+ }
67
+
68
+ func GetTodo(ctx context.Context, id string) (*Todo, error) {
69
+ for _, todo := range globalTodos {
70
+ if todo.ID == id {
71
+ return todo, nil
72
+ }
73
+ }
74
+ return nil, errors.New("Todo not found")
75
+ }
76
+
77
+ type GetAllTodoParams struct {
78
+ Limit int `json:"limit"`
79
+ Filter string `json:"filter"`
80
+ }
81
+
82
+ func GetAllTodo(ctx context.Context, params GetAllTodoParams) ([]*Todo, error) {
83
+ // limit := Default(params.Limit, 10)
84
+ if params.Filter == "completed" {
85
+ newTodos := []*Todo{}
86
+ for _, v := range globalTodos {
87
+ if v.Completed {
88
+ newTodos = append(newTodos, v)
89
+ }
90
+ }
91
+ return newTodos, nil
92
+ }
93
+ if params.Filter == "active" {
94
+ newTodos := []*Todo{}
95
+ for _, v := range globalTodos {
96
+ if !v.Completed {
97
+ newTodos = append(newTodos, v)
98
+ }
99
+ }
100
+ return newTodos, nil
101
+ }
102
+ return globalTodos, nil
103
+ }
cmd/gromer/main.go CHANGED
@@ -156,26 +156,31 @@ func main() {
156
156
  }
157
157
  }
158
158
  componentNames := []string{}
159
+ containerNames := []string{}
160
+ for _, p := range []string{"components", "containers"} {
159
- err = filepath.Walk("components",
161
+ err = filepath.Walk(p,
160
- func(filesrc string, info os.FileInfo, err error) error {
162
+ func(filesrc string, info os.FileInfo, err error) error {
161
- if err != nil {
163
+ if err != nil {
162
- return err
164
+ return err
163
- }
165
+ }
164
- if !info.IsDir() {
166
+ if !info.IsDir() {
165
- filename := strings.ReplaceAll(filepath.Base(filesrc), ".go", "")
167
+ filename := strings.ReplaceAll(filepath.Base(filesrc), ".go", "")
168
+ if p == "containers" {
169
+ containerNames = append(containerNames, strings.Title((filename)))
170
+ } else {
166
- componentNames = append(componentNames, strings.Title((filename)))
171
+ componentNames = append(componentNames, strings.Title((filename)))
167
- }
172
+ }
173
+ }
168
- return nil
174
+ return nil
169
- })
175
+ })
170
- if err != nil {
176
+ if err != nil {
171
- log.Fatal(err)
177
+ log.Fatal(err)
178
+ }
172
179
  }
173
180
  s, _, err := Html(`// Code generated by gromer. DO NOT EDIT.
174
181
  package main
175
182
 
176
183
  import (
177
- "os"
178
-
179
184
  "github.com/gorilla/mux"
180
185
  "github.com/pyros2097/gromer"
181
186
  "github.com/rs/zerolog/log"
@@ -183,6 +188,7 @@ import (
183
188
 
184
189
  "{{ moduleName }}/assets"
185
190
  "{{ moduleName }}/components"
191
+ "{{ moduleName }}/containers"
186
192
  {{#if notFoundPkg}}"{{ moduleName }}/pages/404"{{/if}}
187
193
  {{#each routeImports as |route| }}"{{ moduleName }}/pages{{ route.PkgPath }}"
188
194
  {{/each}}
@@ -191,10 +197,11 @@ import (
191
197
  func init() {
192
198
  {{#each componentNames as |name| }}gromer.RegisterComponent(components.{{ name }})
193
199
  {{/each}}
200
+ {{#each containerNames as |name| }}gromer.RegisterContainer(containers.{{ name }})
201
+ {{/each}}
194
202
  }
195
203
 
196
204
  func main() {
197
- port := os.Getenv("PORT")
198
205
  baseRouter := mux.NewRouter()
199
206
  baseRouter.Use(gromer.LogMiddleware)
200
207
  {{#if notFoundPkg}}
@@ -216,9 +223,9 @@ func main() {
216
223
  {{/each}}
217
224
 
218
225
 
219
- log.Info().Msg("http server listening on http://localhost:"+port)
226
+ log.Info().Msg("http server listening on http://localhost:3000")
220
227
  srv := server.New(baseRouter, nil)
221
- if err := srv.ListenAndServe(":"+port); err != nil {
228
+ if err := srv.ListenAndServe(":3000"); err != nil {
222
229
  log.Fatal().Stack().Err(err).Msg("failed to listen")
223
230
  }
224
231
  }
@@ -228,6 +235,7 @@ func main() {
228
235
  "apiRoutes", apiRoutes,
229
236
  "routeImports", routeImports,
230
237
  "componentNames", componentNames,
238
+ "containerNames", containerNames,
231
239
  "notFoundPkg", notFoundPkg,
232
240
  "tick", "`",
233
241
  ).Render()
handlebars/template.go CHANGED
@@ -111,6 +111,10 @@ func HtmlErr(status int, err error) (HtmlContent, int, error) {
111
111
  return HtmlContent("ErrorPage/AccessDeniedPage/NotFoundPage based on status code"), status, err
112
112
  }
113
113
 
114
+ func HtmlEmpty() (HtmlContent, int, error) {
115
+ return Html("").Render()
116
+ }
117
+
114
118
  func Css(v string) CssContent {
115
119
  stylesCss += CssContent(v)
116
120
  return CssContent(v)
http.go CHANGED
@@ -114,6 +114,57 @@ func RegisterComponent(fn any, props ...string) {
114
114
  })
115
115
  }
116
116
 
117
+ func RegisterContainer(fn any, props ...string) {
118
+ name := getFunctionName(fn)
119
+ fnType := reflect.TypeOf(fn)
120
+ fnValue := reflect.ValueOf(fn)
121
+ handlebars.GlobalHelpers.Add(name, func(help handlebars.HelperContext) (template.HTML, error) {
122
+ args := []reflect.Value{reflect.ValueOf(context.TODO())}
123
+ var props any
124
+ if fnType.NumIn() > 1 {
125
+ structType := fnType.In(1)
126
+ instance := reflect.New(structType)
127
+ if structType.Kind() != reflect.Struct {
128
+ log.Fatal().Msgf("component '%s' props should be a struct", name)
129
+ }
130
+ rv := instance.Elem()
131
+ for i := 0; i < structType.NumField(); i++ {
132
+ if f := rv.Field(i); f.CanSet() {
133
+ jsonName := structType.Field(i).Tag.Get("json")
134
+ defaultValue := structType.Field(i).Tag.Get("default")
135
+ if jsonName == "children" {
136
+ s, err := help.Block()
137
+ if err != nil {
138
+ return "", err
139
+ }
140
+ f.Set(reflect.ValueOf(template.HTML(s)))
141
+ } else {
142
+ v := help.Context.Get(jsonName)
143
+ if v == nil {
144
+ f.Set(reflect.ValueOf(defaultValue))
145
+ } else {
146
+ f.Set(reflect.ValueOf(v))
147
+ }
148
+ }
149
+ }
150
+ }
151
+ args = append(args, rv)
152
+ props = rv.Interface()
153
+ }
154
+ res := fnValue.Call(args)
155
+ tpl := res[0].Interface().(*handlebars.Template)
156
+ // if res[1].Interface() != nil {
157
+ // show error in component
158
+ // }
159
+ tpl.Context.Set("props", props)
160
+ s, _, err := tpl.Render()
161
+ if err != nil {
162
+ return "", err
163
+ }
164
+ return template.HTML(s), nil
165
+ })
166
+ }
167
+
117
168
  func RespondError(w http.ResponseWriter, status int, err error) {
118
169
  w.Header().Set("Content-Type", "application/json")
119
170
  w.WriteHeader(status) // always write status last
@@ -178,21 +229,24 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
178
229
  if structType.Kind() != reflect.Struct {
179
230
  log.Fatal().Msgf("router '%s' func final param should be a struct", route)
180
231
  }
181
- if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
182
- // r.ParseForm()
183
- // r.PostForm
232
+ method := r.Method
184
- // if r.Header.Set("Content-Type") == ""
233
+ contentType := r.Header.Get("Content-Type")
234
+ if method == "GET" || ((method == "POST" || method == "PUT" || method == "PATCH") && contentType == "application/x-www-form-urlencoded") {
185
- err := json.NewDecoder(r.Body).Decode(instance.Interface())
235
+ err := r.ParseForm()
186
236
  if err != nil {
187
237
  RespondError(w, 400, err)
188
238
  return
189
239
  }
190
- } else if r.Method == "GET" {
191
240
  rv := instance.Elem()
192
241
  for i := 0; i < structType.NumField(); i++ {
193
242
  if f := rv.Field(i); f.CanSet() {
194
243
  jsonName := structType.Field(i).Tag.Get("json")
244
+ jsonValue := ""
245
+ if method == "GET" {
195
- jsonValue := r.URL.Query().Get(jsonName)
246
+ jsonValue = r.URL.Query().Get(jsonName)
247
+ } else {
248
+ jsonValue = r.Form.Get(jsonName)
249
+ }
196
250
  if f.Kind() == reflect.String {
197
251
  f.SetString(jsonValue)
198
252
  } else if f.Kind() == reflect.Int64 || f.Kind() == reflect.Int32 || f.Kind() == reflect.Int {
@@ -222,10 +276,19 @@ func PerformRequest(route string, h interface{}, ctx interface{}, w http.Respons
222
276
  f.Set(reflect.ValueOf(v))
223
277
  }
224
278
  } else {
225
- log.Fatal().Msgf("Uknown query param: '%s' '%s'", jsonName, jsonValue)
279
+ log.Fatal().Msgf("Uknown form param: '%s' '%s'", jsonName, jsonValue)
226
280
  }
227
281
  }
228
282
  }
283
+ } else if (method == "POST" || method == "PUT" || method == "PATCH") && contentType == "application/json" {
284
+ err := json.NewDecoder(r.Body).Decode(instance.Interface())
285
+ if err != nil {
286
+ RespondError(w, 400, err)
287
+ return
288
+ }
289
+ } else {
290
+ RespondError(w, 400, fmt.Errorf("Illegal Content-Type tag found %s", contentType))
291
+ return
229
292
  }
230
293
  args = append(args, instance.Elem())
231
294
  }
readme.md CHANGED
@@ -218,4 +218,8 @@ func main() {
218
218
  log.Fatal().Stack().Err(err).Msg("failed to listen")
219
219
  }
220
220
  }
221
- ```
221
+ ```
222
+
223
+ # TODO:
224
+ Add inline css formatting
225
+ ADd inline html formatting