~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.


603e578e Peter John

3 years ago
add component props
example/components/page.go CHANGED
@@ -6,7 +6,12 @@ import (
6
6
  . "github.com/pyros2097/gromer"
7
7
  )
8
8
 
9
+ type PageProps struct {
10
+ Title string `json:"title"`
11
+ Children template.HTML `json:"children"`
12
+ }
13
+
9
- func Page(title string, children template.HTML) *HandlersTemplate {
14
+ func Page(props PageProps) *HandlersTemplate {
10
15
  return Html(`
11
16
  <!DOCTYPE html>
12
17
  <html lang="en">
@@ -14,8 +19,8 @@ func Page(title string, children template.HTML) *HandlersTemplate {
14
19
  <meta charset="UTF-8" />
15
20
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
16
21
  <meta content="utf-8" http-equiv="encoding" />
17
- <title>{{ title }}</title>
22
+ <title>{{ props.Title }}</title>
18
- <meta name="description" content="{{ title }}" />
23
+ <meta name="description" content="{{ props.Title }}" />
19
24
  <meta name="author" content="pyros.sh" />
20
25
  <meta content="pyros.sh, gromer" name="keywords" />
21
26
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover" />
@@ -25,11 +30,8 @@ func Page(title string, children template.HTML) *HandlersTemplate {
25
30
  <script src="/assets/alpine.js" defer=""></script>
26
31
  </head>
27
32
  <body>
28
- {{ children }}
33
+ {{ props.Children }}
29
34
  </body>
30
35
  </html>
31
- `).Props(
32
- "title", title,
33
- "children", children,
34
- )
36
+ `)
35
37
  }
example/components/todo.go ADDED
@@ -0,0 +1,26 @@
1
+ package components
2
+
3
+ import (
4
+ . "github.com/pyros2097/gromer"
5
+ "github.com/pyros2097/gromer/example/db"
6
+ )
7
+
8
+ type TodoProps struct {
9
+ Todo *db.Todo `json:"todo"`
10
+ }
11
+
12
+ func Todo(props TodoProps) *HandlersTemplate {
13
+ return Html(`
14
+ <tr>
15
+ <td>{{ props.Todo.ID }}</td>
16
+ <td>Book1</td>
17
+ <td>Author1</td>
18
+ <td>
19
+ <button class="button is-primary">Edit</button>
20
+ </td>
21
+ <td>
22
+ <button hx-swap="delete" class="button is-danger" hx-delete="/api/todos/{{ props.Todo.ID }}">Delete</button>
23
+ </td>
24
+ </tr>
25
+ `)
26
+ }
example/main.go CHANGED
@@ -22,7 +22,7 @@ import (
22
22
  func init() {
23
23
  gromer.RegisterComponent(components.Header)
24
24
  gromer.RegisterComponent(components.Page)
25
-
25
+ gromer.RegisterComponent(components.Todo)
26
26
  }
27
27
 
28
28
  func main() {
example/pages/get.go CHANGED
@@ -22,9 +22,8 @@ func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
22
22
  return HtmlErr(status, err)
23
23
  }
24
24
  return Html(`
25
- {{#Page "gromer example"}}
25
+ {{#Page title="gromer example"}}
26
- {{#Header "123"}}
27
- {{/Header}}
26
+ {{#Header}}{{/Header}}
28
27
  <main class="box center">
29
28
  <div class="columns">
30
29
  <div>
@@ -46,17 +45,7 @@ func GET(ctx context.Context, params GetParams) (HtmlContent, int, error) {
46
45
  </thead>
47
46
  <tbody id="new-book" hx-target="closest tr" hx-swap="outerHTML swap:0.5s">
48
47
  {{#each todos as |todo|}}
49
- <tr>
50
- <td>{{todo.ID}}</td>
48
+ {{#Todo todo=todo}}{{/Todo}}
51
- <td>Book1</td>
52
- <td>Author1</td>
53
- <td>
54
- <button class="button is-primary">Edit</button>
55
- </td>
56
- <td>
57
- <button hx-swap="delete" class="button is-danger" hx-delete="/api/todos/{{todo.ID}}">Delete</button>
58
- </td>
59
- </tr>
60
49
  {{/each}}
61
50
  </tbody>
62
51
  </table>
http.go CHANGED
@@ -79,24 +79,41 @@ func GetFunctionName(temp interface{}) string {
79
79
  return strs[len(strs)-1]
80
80
  }
81
81
 
82
- func RegisterComponent(fn interface{}, props ...string) {
82
+ func RegisterComponent(fn any, props ...string) {
83
83
  name := GetFunctionName(fn)
84
84
  fnType := reflect.TypeOf(fn)
85
85
  fnValue := reflect.ValueOf(fn)
86
- handlebars.GlobalHelpers.Add(name, func(title string, c handlebars.HelperContext) (template.HTML, error) {
86
+ handlebars.GlobalHelpers.Add(name, func(help handlebars.HelperContext) (template.HTML, error) {
87
- s, err := c.Block()
88
- if err != nil {
89
- return "", err
90
- }
91
87
  args := []reflect.Value{}
88
+ var props any
92
89
  if fnType.NumIn() > 0 {
90
+ structType := fnType.In(0)
93
- args = append(args, reflect.ValueOf(title))
91
+ instance := reflect.New(structType)
92
+ if structType.Kind() != reflect.Struct {
93
+ log.Fatal().Msgf("component '%s' props should be a struct", name)
94
+ }
95
+ rv := instance.Elem()
96
+ for i := 0; i < structType.NumField(); i++ {
97
+ if f := rv.Field(i); f.CanSet() {
98
+ jsonName := structType.Field(i).Tag.Get("json")
99
+ fmt.Printf("jsonName %s %+v\n", jsonName, help.Context.Get(jsonName))
100
+ if jsonName == "children" {
101
+ s, err := help.Block()
102
+ if err != nil {
94
- if s != "" {
103
+ return "", err
104
+ }
95
- args = append(args, reflect.ValueOf(template.HTML(s)))
105
+ f.Set(reflect.ValueOf(template.HTML(s)))
106
+ } else {
107
+ f.Set(reflect.ValueOf(help.Context.Get(jsonName)))
108
+ }
109
+ }
96
110
  }
111
+ args = append(args, rv)
112
+ props = rv.Interface()
97
113
  }
98
114
  res := fnValue.Call(args)
99
115
  tpl := res[0].Interface().(*HandlersTemplate)
116
+ tpl.ctx.Set("props", props)
100
117
  comp, err := handlebars.Render(tpl.text, tpl.ctx)
101
118
  if err != nil {
102
119
  return "", err
@@ -120,11 +137,11 @@ func init() {
120
137
  func RespondError(w http.ResponseWriter, status int, err error) {
121
138
  w.Header().Set("Content-Type", "application/json")
122
139
  w.WriteHeader(status) // always write status last
140
+ w.(*LogResponseWriter).SetError(err)
123
141
  merror := map[string]interface{}{
124
142
  "error": err.Error(),
125
143
  }
126
144
  if status >= 500 {
127
- log.Error().Str("type", "panic").Msg(err.Error())
128
145
  merror["error"] = "Internal Server Error"
129
146
  }
130
147
  validationErrors, ok := err.(validator.ValidationErrors)
@@ -268,6 +285,7 @@ type LogResponseWriter struct {
268
285
  responseStatusCode int
269
286
  responseContentLength int
270
287
  responseHeaderSize int
288
+ err error
271
289
  }
272
290
 
273
291
  func NewLogResponseWriter(w http.ResponseWriter) *LogResponseWriter {
@@ -278,7 +296,6 @@ func (w *LogResponseWriter) WriteHeader(code int) {
278
296
  w.ResponseWriter.WriteHeader(code)
279
297
  w.responseStatusCode = code
280
298
  w.responseHeaderSize = int(headerSize(w.Header()))
281
-
282
299
  }
283
300
 
284
301
  func (w *LogResponseWriter) Write(body []byte) (int, error) {
@@ -286,12 +303,16 @@ func (w *LogResponseWriter) Write(body []byte) (int, error) {
286
303
  return w.ResponseWriter.Write(body)
287
304
  }
288
305
 
306
+ func (w *LogResponseWriter) SetError(err error) {
307
+ w.err = err
308
+ }
309
+
289
310
  var LogMiddleware = mux.MiddlewareFunc(func(next http.Handler) http.Handler {
290
311
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
291
312
  defer func() {
292
313
  if err := recover(); err != nil {
293
314
  stack := string(debug.Stack())
294
- RespondError(w, 500, fmt.Errorf("panic: %+v\n %s", err, stack))
315
+ RespondError(w, 599, fmt.Errorf("panic: %+v\n %s", err, stack))
295
316
  }
296
317
  }()
297
318
  startTime := time.Now()
@@ -301,7 +322,11 @@ var LogMiddleware = mux.MiddlewareFunc(func(next http.Handler) http.Handler {
301
322
  if len(ip) > 0 && ip[0] == '[' {
302
323
  ip = ip[1 : len(ip)-1]
303
324
  }
325
+ logger := log.WithLevel(zerolog.InfoLevel)
326
+ if logRespWriter.err != nil {
327
+ logger = log.WithLevel(zerolog.ErrorLevel).Err(logRespWriter.err).Stack()
328
+ }
304
- log.Info().
329
+ logger.
305
330
  Str("method", r.Method).
306
331
  Str("url", r.URL.String()).
307
332
  Int("header_size", int(headerSize(r.Header))).