~repos /gromer
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 +10 -8
- example/components/todo.go +26 -0
- example/main.go +1 -1
- example/pages/get.go +3 -14
- http.go +38 -13
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(
|
|
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>{{
|
|
22
|
+
<title>{{ props.Title }}</title>
|
|
18
|
-
<meta name="description" content="{{
|
|
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
|
-
{{
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
103
|
+
return "", err
|
|
104
|
+
}
|
|
95
|
-
|
|
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,
|
|
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
|
-
|
|
329
|
+
logger.
|
|
305
330
|
Str("method", r.Method).
|
|
306
331
|
Str("url", r.URL.String()).
|
|
307
332
|
Int("header_size", int(headerSize(r.Header))).
|