~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.
6eb01df1
—
Peter John 3 years ago
fix various issues
- _example/components/Checkbox.go +41 -0
- _example/components/todo.go +3 -3
- _example/containers/TodoCount.go +1 -1
- _example/containers/TodoList.go +0 -28
- _example/main.go +1 -0
- _example/routes/404/get.go +9 -12
- _example/routes/about/get.go +8 -10
- _example/routes/get.go +9 -287
- _example/routes/post.go +7 -5
- _example/routes/styles.go +256 -0
- go.mod +1 -2
- go.sum +0 -2
- gsx/gsx.go +60 -36
- gsx/gsx_test.go +32 -20
- http.go +6 -104
_example/components/Checkbox.go
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package components
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
var _ = Css(`
|
|
8
|
+
.checkbox {
|
|
9
|
+
text-align: center;
|
|
10
|
+
width: 40px;
|
|
11
|
+
/* auto, since non-WebKit browsers doesn't support input styling */
|
|
12
|
+
height: auto;
|
|
13
|
+
position: absolute;
|
|
14
|
+
top: 0;
|
|
15
|
+
bottom: 0;
|
|
16
|
+
margin: auto 0;
|
|
17
|
+
border: none; /* Mobile Safari */
|
|
18
|
+
-webkit-appearance: none;
|
|
19
|
+
appearance: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.checkbox {
|
|
23
|
+
opacity: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.checkbox + label {
|
|
27
|
+
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
|
28
|
+
background-repeat: no-repeat;
|
|
29
|
+
background-position: center left;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.checkbox:checked + label {
|
|
33
|
+
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
|
|
34
|
+
}
|
|
35
|
+
`)
|
|
36
|
+
|
|
37
|
+
func Checkbox(c Context, value bool) *Node {
|
|
38
|
+
return c.Render(`
|
|
39
|
+
<input class="checkbox" type="checkbox" checked="{value}" />
|
|
40
|
+
`)
|
|
41
|
+
}
|
_example/components/todo.go
CHANGED
|
@@ -8,6 +8,8 @@ import (
|
|
|
8
8
|
var _ = Css(`
|
|
9
9
|
`)
|
|
10
10
|
|
|
11
|
+
// <Checkbox hx-post="/" value="{todo.Completed}" />
|
|
12
|
+
|
|
11
13
|
func Todo(c Context, todo *todos.Todo) *Node {
|
|
12
14
|
return c.Render(`
|
|
13
15
|
<li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
|
|
@@ -15,7 +17,7 @@ func Todo(c Context, todo *todos.Todo) *Node {
|
|
|
15
17
|
<form hx-target="#todo-{todo.ID}" hx-swap="outerHTML">
|
|
16
18
|
<input type="hidden" name="intent" value="complete" />
|
|
17
19
|
<input type="hidden" name="id" value="{todo.ID}" />
|
|
18
|
-
<input
|
|
20
|
+
<input class="checkbox" type="checkbox" checked="{value}" />
|
|
19
21
|
</form>
|
|
20
22
|
<label>{todo.Text}</label>
|
|
21
23
|
<form hx-post="/" hx-target="#todo-{todo.ID}" hx-swap="delete">
|
|
@@ -27,5 +29,3 @@ func Todo(c Context, todo *todos.Todo) *Node {
|
|
|
27
29
|
</li>
|
|
28
30
|
`)
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
// <!-- <label hx-get="/todos/edit/{todo.ID}" hx-target="#todo-{todo.ID}" hx-swap="outerHTML">{{ props.Todo.Text }}</label> -->
|
_example/containers/TodoCount.go
CHANGED
|
@@ -26,7 +26,7 @@ func TodoCount(c Context, filter string) (*Node, error) {
|
|
|
26
26
|
}
|
|
27
27
|
c.Set("count", len(todos))
|
|
28
28
|
return c.Render(`
|
|
29
|
-
<span
|
|
29
|
+
<span id="todo-count" class="todo-count" hx-swap-oob="true">
|
|
30
30
|
<strong>{count}</strong> item left
|
|
31
31
|
</span>
|
|
32
32
|
`), nil
|
_example/containers/TodoList.go
CHANGED
|
@@ -39,34 +39,6 @@ var _ = Css(`
|
|
|
39
39
|
display: none;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
.todo-list li .toggle {
|
|
43
|
-
text-align: center;
|
|
44
|
-
width: 40px;
|
|
45
|
-
/* auto, since non-WebKit browsers doesn't support input styling */
|
|
46
|
-
height: auto;
|
|
47
|
-
position: absolute;
|
|
48
|
-
top: 0;
|
|
49
|
-
bottom: 0;
|
|
50
|
-
margin: auto 0;
|
|
51
|
-
border: none; /* Mobile Safari */
|
|
52
|
-
-webkit-appearance: none;
|
|
53
|
-
appearance: none;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
.todo-list li .toggle {
|
|
57
|
-
opacity: 0;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.todo-list li .toggle + label {
|
|
61
|
-
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
|
|
62
|
-
background-repeat: no-repeat;
|
|
63
|
-
background-position: center left;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
.todo-list li .toggle:checked + label {
|
|
67
|
-
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
42
|
.todo-list li label {
|
|
71
43
|
word-break: break-all;
|
|
72
44
|
padding: 15px 15px 15px 60px;
|
_example/main.go
CHANGED
|
@@ -19,6 +19,7 @@ import (
|
|
|
19
19
|
|
|
20
20
|
func init() {
|
|
21
21
|
gsx.RegisterComponent(components.Todo, "todo")
|
|
22
|
+
gsx.RegisterComponent(components.Checkbox, "value")
|
|
22
23
|
|
|
23
24
|
gsx.RegisterComponent(containers.TodoCount, "filter")
|
|
24
25
|
gsx.RegisterComponent(containers.TodoList, "page", "filter")
|
_example/routes/404/get.go
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
package not_found_404
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"context"
|
|
5
|
-
|
|
6
4
|
. "github.com/pyros2097/gromer/gsx"
|
|
7
5
|
)
|
|
8
6
|
|
|
9
|
-
func GET(
|
|
7
|
+
func GET(c Context) (*Node, int, error) {
|
|
8
|
+
c.Meta("title", "Page Not Found")
|
|
10
|
-
return
|
|
9
|
+
return c.Render(`
|
|
11
|
-
<Page title="Page Not Found">
|
|
12
|
-
|
|
10
|
+
<main class="box center">
|
|
13
|
-
|
|
11
|
+
<h1>Page Not Found</h1>
|
|
14
|
-
|
|
12
|
+
<h2 class="mt-6">
|
|
15
|
-
|
|
13
|
+
<a class="is-underlined" href="/">Go Back</a>
|
|
16
|
-
|
|
14
|
+
</h2>
|
|
17
|
-
|
|
15
|
+
</main>
|
|
18
|
-
</Page>
|
|
19
16
|
`), 404, nil
|
|
20
17
|
}
|
_example/routes/about/get.go
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
package about
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"context"
|
|
5
|
-
|
|
6
4
|
. "github.com/pyros2097/gromer/gsx"
|
|
7
5
|
)
|
|
8
6
|
|
|
9
|
-
func GET(
|
|
7
|
+
func GET(c Context) (*Node, int, error) {
|
|
8
|
+
c.Meta("title", "About Gromer")
|
|
9
|
+
c.Meta("description", "About Gromer")
|
|
10
|
-
return
|
|
10
|
+
return c.Render(`
|
|
11
|
-
<Page title="About me">
|
|
12
|
-
|
|
11
|
+
<div class="flex flex-col justify-center items-center">
|
|
13
|
-
|
|
12
|
+
A new link is here
|
|
14
|
-
|
|
13
|
+
P<h1>About Me</h1>
|
|
15
|
-
|
|
14
|
+
</div>
|
|
16
|
-
</Page>
|
|
17
15
|
`), 200, nil
|
|
18
16
|
}
|
_example/routes/get.go
CHANGED
|
@@ -1,314 +1,36 @@
|
|
|
1
1
|
package routes
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
-
"context"
|
|
5
|
-
|
|
6
4
|
_ "github.com/pyros2097/gromer/_example/components"
|
|
7
5
|
. "github.com/pyros2097/gromer/gsx"
|
|
8
6
|
)
|
|
9
7
|
|
|
10
|
-
var _ = Css(`
|
|
11
|
-
hr {
|
|
12
|
-
margin: 20px 0;
|
|
13
|
-
border: 0;
|
|
14
|
-
border-top: 1px dashed #c5c5c5;
|
|
15
|
-
border-bottom: 1px dashed #f7f7f7;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
html,
|
|
19
|
-
body {
|
|
20
|
-
margin: 0;
|
|
21
|
-
padding: 0;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
button {
|
|
25
|
-
margin: 0;
|
|
26
|
-
padding: 0;
|
|
27
|
-
border: 0;
|
|
28
|
-
background: none;
|
|
29
|
-
font-size: 100%;
|
|
30
|
-
vertical-align: baseline;
|
|
31
|
-
font-family: inherit;
|
|
32
|
-
font-weight: inherit;
|
|
33
|
-
color: inherit;
|
|
34
|
-
-webkit-appearance: none;
|
|
35
|
-
appearance: none;
|
|
36
|
-
-webkit-font-smoothing: antialiased;
|
|
37
|
-
-moz-osx-font-smoothing: grayscale;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
body {
|
|
41
|
-
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
42
|
-
line-height: 1.4em;
|
|
43
|
-
background: #f5f5f5;
|
|
44
|
-
color: #4d4d4d;
|
|
45
|
-
min-width: 230px;
|
|
46
|
-
max-width: 550px;
|
|
47
|
-
margin: 0 auto;
|
|
48
|
-
-webkit-font-smoothing: antialiased;
|
|
49
|
-
-moz-osx-font-smoothing: grayscale;
|
|
50
|
-
font-weight: 300;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
:focus {
|
|
54
|
-
outline: 0;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.hidden {
|
|
58
|
-
display: none;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.new-todo,
|
|
62
|
-
.edit {
|
|
63
|
-
position: relative;
|
|
64
|
-
margin: 0;
|
|
65
|
-
width: 100%;
|
|
66
|
-
font-size: 24px;
|
|
67
|
-
font-family: inherit;
|
|
68
|
-
font-weight: inherit;
|
|
69
|
-
line-height: 1.4em;
|
|
70
|
-
border: 0;
|
|
71
|
-
color: inherit;
|
|
72
|
-
padding: 6px;
|
|
73
|
-
border: 1px solid #999;
|
|
74
|
-
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
|
75
|
-
box-sizing: border-box;
|
|
76
|
-
-webkit-font-smoothing: antialiased;
|
|
77
|
-
-moz-osx-font-smoothing: grayscale;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.new-todo {
|
|
81
|
-
padding: 16px 16px 16px 60px;
|
|
82
|
-
border: none;
|
|
83
|
-
background: rgba(0, 0, 0, 0.003);
|
|
84
|
-
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
.main {
|
|
88
|
-
position: relative;
|
|
89
|
-
z-index: 2;
|
|
90
|
-
border-top: 1px solid #e6e6e6;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
.toggle-all {
|
|
94
|
-
text-align: center;
|
|
95
|
-
border: none; /* Mobile Safari */
|
|
96
|
-
opacity: 0;
|
|
97
|
-
position: absolute;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
.toggle-all + label {
|
|
101
|
-
width: 60px;
|
|
102
|
-
height: 34px;
|
|
103
|
-
font-size: 0;
|
|
104
|
-
position: absolute;
|
|
105
|
-
top: -52px;
|
|
106
|
-
left: -13px;
|
|
107
|
-
-webkit-transform: rotate(90deg);
|
|
108
|
-
transform: rotate(90deg);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.toggle-all + label:before {
|
|
112
|
-
content: '❯';
|
|
113
|
-
font-size: 22px;
|
|
114
|
-
color: #e6e6e6;
|
|
115
|
-
padding: 10px 27px 10px 27px;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.toggle-all:checked + label:before {
|
|
119
|
-
color: #737373;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
.footer {
|
|
123
|
-
color: #777;
|
|
124
|
-
padding: 10px 15px;
|
|
125
|
-
height: 20px;
|
|
126
|
-
text-align: center;
|
|
127
|
-
border-top: 1px solid #e6e6e6;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.footer:before {
|
|
131
|
-
content: '';
|
|
132
|
-
position: absolute;
|
|
133
|
-
right: 0;
|
|
134
|
-
bottom: 0;
|
|
135
|
-
left: 0;
|
|
136
|
-
height: 50px;
|
|
137
|
-
overflow: hidden;
|
|
138
|
-
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.filters {
|
|
142
|
-
margin: 0;
|
|
143
|
-
padding: 0;
|
|
144
|
-
list-style: none;
|
|
145
|
-
position: absolute;
|
|
146
|
-
right: 0;
|
|
147
|
-
left: 0;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.filters li {
|
|
151
|
-
display: inline;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.filters li a {
|
|
155
|
-
color: inherit;
|
|
156
|
-
margin: 3px;
|
|
157
|
-
padding: 3px 7px;
|
|
158
|
-
text-decoration: none;
|
|
159
|
-
border: 1px solid transparent;
|
|
160
|
-
border-radius: 3px;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.filters li a:hover {
|
|
164
|
-
border-color: rgba(175, 47, 47, 0.1);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.filters li a.selected {
|
|
168
|
-
border-color: rgba(175, 47, 47, 0.2);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.clear-completed,
|
|
172
|
-
html .clear-completed:active {
|
|
173
|
-
float: right;
|
|
174
|
-
position: relative;
|
|
175
|
-
line-height: 20px;
|
|
176
|
-
text-decoration: none;
|
|
177
|
-
cursor: pointer;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.clear-completed:hover {
|
|
181
|
-
text-decoration: underline;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.info {
|
|
185
|
-
margin: 65px auto 0;
|
|
186
|
-
color: #bfbfbf;
|
|
187
|
-
font-size: 10px;
|
|
188
|
-
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
|
189
|
-
text-align: center;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
.info p {
|
|
193
|
-
line-height: 1;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
.info a {
|
|
197
|
-
color: inherit;
|
|
198
|
-
text-decoration: none;
|
|
199
|
-
font-weight: 400;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
.info a:hover {
|
|
203
|
-
text-decoration: underline;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
|
207
|
-
.toggle-all {
|
|
208
|
-
background: none;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
@media (max-width: 430px) {
|
|
213
|
-
.footer {
|
|
214
|
-
height: 50px;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
.filters {
|
|
218
|
-
bottom: 10px;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
.todoapp {
|
|
223
|
-
background: #fff;
|
|
224
|
-
margin: 130px 0 40px 0;
|
|
225
|
-
position: relative;
|
|
226
|
-
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
.todoapp input::-webkit-input-placeholder {
|
|
230
|
-
font-style: italic;
|
|
231
|
-
font-weight: 300;
|
|
232
|
-
color: #e6e6e6;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
.todoapp input::-moz-placeholder {
|
|
236
|
-
font-style: italic;
|
|
237
|
-
font-weight: 300;
|
|
238
|
-
color: #e6e6e6;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
.todoapp input::input-placeholder {
|
|
242
|
-
font-style: italic;
|
|
243
|
-
font-weight: 300;
|
|
244
|
-
color: #e6e6e6;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.todoapp h1 {
|
|
248
|
-
position: absolute;
|
|
249
|
-
top: -155px;
|
|
250
|
-
width: 100%;
|
|
251
|
-
font-size: 100px;
|
|
252
|
-
font-weight: 100;
|
|
253
|
-
text-align: center;
|
|
254
|
-
color: rgba(175, 47, 47, 0.15);
|
|
255
|
-
-webkit-text-rendering: optimizeLegibility;
|
|
256
|
-
-moz-text-rendering: optimizeLegibility;
|
|
257
|
-
text-rendering: optimizeLegibility;
|
|
258
|
-
}
|
|
259
|
-
`)
|
|
260
|
-
|
|
261
|
-
// var (
|
|
262
|
-
// Container = Css(`
|
|
263
|
-
// background: #fff;
|
|
264
|
-
// margin: 130px 0 40px 0;
|
|
265
|
-
// position: relative;
|
|
266
|
-
// box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
|
267
|
-
// `)
|
|
268
|
-
// )
|
|
269
|
-
|
|
270
8
|
type GetParams struct {
|
|
271
9
|
Page int `json:"page"`
|
|
272
10
|
Filter string `json:"filter"`
|
|
273
11
|
}
|
|
274
12
|
|
|
275
|
-
// func IndexLoader() {
|
|
276
|
-
// Data: M{},
|
|
277
|
-
// Meta: []*Meta{},
|
|
278
|
-
// Links: []*Link{},
|
|
279
|
-
// }
|
|
280
|
-
|
|
281
|
-
func IndexAction(h Context, ctx context.Context, params PostParams) {
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
func IndexPage() {
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// <meta name="description" content="{title}" />
|
|
288
|
-
// <meta name="author" content="pyrossh" />
|
|
289
|
-
// <meta name="keywords" content="pyros.sh, pyrossh, gromer" />
|
|
290
|
-
// <meta />
|
|
291
|
-
|
|
292
13
|
func GET(c Context, params GetParams) (*Node, int, error) {
|
|
293
|
-
c.Meta("title", "Todos")
|
|
14
|
+
c.Meta("title", "Gromer Todos")
|
|
294
|
-
c.
|
|
15
|
+
c.Meta("description", "Gromer Todos")
|
|
295
|
-
c.
|
|
16
|
+
c.Meta("author", "gromer")
|
|
17
|
+
c.Meta("keywords", "gromer")
|
|
296
18
|
return c.Render(`
|
|
297
|
-
<
|
|
19
|
+
<div class="todoapp">
|
|
298
20
|
<header class="header">
|
|
299
21
|
<h1>todos</h1>
|
|
300
22
|
<form hx-post="/" hx-target="#todo-list" hx-swap="afterbegin" _="on htmx:afterOnLoad set #text.value to ''">
|
|
301
23
|
<input type="hidden" name="intent" value="create" />
|
|
302
|
-
<input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off" /
|
|
24
|
+
<input class="new-todo" id="text" name="text" placeholder="What needs to be done?" autofocus="false" autocomplete="off" />
|
|
303
25
|
</form>
|
|
304
26
|
</header>
|
|
305
27
|
<section class="main">
|
|
306
28
|
<input class="toggle-all" id="toggle-all" type="checkbox" />
|
|
307
29
|
<label for="toggle-all">Mark all as complete</label>
|
|
308
|
-
<TodoList id="todo-list" page="{
|
|
30
|
+
<TodoList id="todo-list" page="{params.Page}" filter="{params.Filter}" />
|
|
309
31
|
</section>
|
|
310
32
|
<footer class="footer">
|
|
311
|
-
<TodoCount filter="{
|
|
33
|
+
<TodoCount filter="{params.Filter}" />
|
|
312
34
|
<ul class="filters">
|
|
313
35
|
<li>
|
|
314
36
|
<a href="?filter=all">All</a>
|
|
@@ -325,6 +47,6 @@ func GET(c Context, params GetParams) (*Node, int, error) {
|
|
|
325
47
|
<button type="submit" class="clear-completed" >Clear completed</button>
|
|
326
48
|
</form>
|
|
327
49
|
</footer>
|
|
328
|
-
</
|
|
50
|
+
</div>
|
|
329
51
|
`), 200, nil
|
|
330
52
|
}
|
_example/routes/post.go
CHANGED
|
@@ -32,8 +32,10 @@ func POST(c Context, params PostParams) (*Node, int, error) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
return c.Render(`
|
|
35
|
+
<div>
|
|
35
|
-
|
|
36
|
+
<TodoList id="todo-list" filter="all" page="1"></TodoList>
|
|
36
|
-
|
|
37
|
+
<TodoCount filter="all" page="1"></TodoCount>
|
|
38
|
+
</div>
|
|
37
39
|
`), 200, nil
|
|
38
40
|
} else if params.Intent == "create" {
|
|
39
41
|
todo, err := todos.CreateTodo(c, params.Text)
|
|
@@ -42,8 +44,8 @@ func POST(c Context, params PostParams) (*Node, int, error) {
|
|
|
42
44
|
}
|
|
43
45
|
c.Set("todo", todo)
|
|
44
46
|
return c.Render(`
|
|
45
|
-
<Todo
|
|
47
|
+
<Todo />
|
|
46
|
-
<TodoCount filter="all" page="1"
|
|
48
|
+
<TodoCount filter="all" page="1" />
|
|
47
49
|
`), 200, nil
|
|
48
50
|
} else if params.Intent == "delete" {
|
|
49
51
|
_, err := todos.DeleteTodo(c, params.ID)
|
|
@@ -65,7 +67,7 @@ func POST(c Context, params PostParams) (*Node, int, error) {
|
|
|
65
67
|
}
|
|
66
68
|
c.Set("todo", todo)
|
|
67
69
|
return c.Render(`
|
|
68
|
-
|
|
70
|
+
<Todo />
|
|
69
71
|
`), 200, nil
|
|
70
72
|
}
|
|
71
73
|
return nil, 404, fmt.Errorf("Intent not specified: %s", params.Intent)
|
_example/routes/styles.go
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
package routes
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
. "github.com/pyros2097/gromer/gsx"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
var _ = Css(`
|
|
8
|
+
hr {
|
|
9
|
+
margin: 20px 0;
|
|
10
|
+
border: 0;
|
|
11
|
+
border-top: 1px dashed #c5c5c5;
|
|
12
|
+
border-bottom: 1px dashed #f7f7f7;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
html,
|
|
16
|
+
body {
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
button {
|
|
22
|
+
margin: 0;
|
|
23
|
+
padding: 0;
|
|
24
|
+
border: 0;
|
|
25
|
+
background: none;
|
|
26
|
+
font-size: 100%;
|
|
27
|
+
vertical-align: baseline;
|
|
28
|
+
font-family: inherit;
|
|
29
|
+
font-weight: inherit;
|
|
30
|
+
color: inherit;
|
|
31
|
+
-webkit-appearance: none;
|
|
32
|
+
appearance: none;
|
|
33
|
+
-webkit-font-smoothing: antialiased;
|
|
34
|
+
-moz-osx-font-smoothing: grayscale;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
39
|
+
line-height: 1.4em;
|
|
40
|
+
background: #f5f5f5;
|
|
41
|
+
color: #4d4d4d;
|
|
42
|
+
min-width: 230px;
|
|
43
|
+
max-width: 550px;
|
|
44
|
+
margin: 0 auto;
|
|
45
|
+
-webkit-font-smoothing: antialiased;
|
|
46
|
+
-moz-osx-font-smoothing: grayscale;
|
|
47
|
+
font-weight: 300;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
:focus {
|
|
51
|
+
outline: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.hidden {
|
|
55
|
+
display: none;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.new-todo,
|
|
59
|
+
.edit {
|
|
60
|
+
position: relative;
|
|
61
|
+
margin: 0;
|
|
62
|
+
width: 100%;
|
|
63
|
+
font-size: 24px;
|
|
64
|
+
font-family: inherit;
|
|
65
|
+
font-weight: inherit;
|
|
66
|
+
line-height: 1.4em;
|
|
67
|
+
border: 0;
|
|
68
|
+
color: inherit;
|
|
69
|
+
padding: 6px;
|
|
70
|
+
border: 1px solid #999;
|
|
71
|
+
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
|
|
72
|
+
box-sizing: border-box;
|
|
73
|
+
-webkit-font-smoothing: antialiased;
|
|
74
|
+
-moz-osx-font-smoothing: grayscale;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.new-todo {
|
|
78
|
+
padding: 16px 16px 16px 60px;
|
|
79
|
+
border: none;
|
|
80
|
+
background: rgba(0, 0, 0, 0.003);
|
|
81
|
+
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.main {
|
|
85
|
+
position: relative;
|
|
86
|
+
z-index: 2;
|
|
87
|
+
border-top: 1px solid #e6e6e6;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.toggle-all {
|
|
91
|
+
text-align: center;
|
|
92
|
+
border: none; /* Mobile Safari */
|
|
93
|
+
opacity: 0;
|
|
94
|
+
position: absolute;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.toggle-all + label {
|
|
98
|
+
width: 60px;
|
|
99
|
+
height: 34px;
|
|
100
|
+
font-size: 0;
|
|
101
|
+
position: absolute;
|
|
102
|
+
top: -52px;
|
|
103
|
+
left: -13px;
|
|
104
|
+
-webkit-transform: rotate(90deg);
|
|
105
|
+
transform: rotate(90deg);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.toggle-all + label:before {
|
|
109
|
+
content: '❯';
|
|
110
|
+
font-size: 22px;
|
|
111
|
+
color: #e6e6e6;
|
|
112
|
+
padding: 10px 27px 10px 27px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.toggle-all:checked + label:before {
|
|
116
|
+
color: #737373;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.footer {
|
|
120
|
+
color: #777;
|
|
121
|
+
padding: 10px 15px;
|
|
122
|
+
height: 20px;
|
|
123
|
+
text-align: center;
|
|
124
|
+
border-top: 1px solid #e6e6e6;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.footer:before {
|
|
128
|
+
content: '';
|
|
129
|
+
position: absolute;
|
|
130
|
+
right: 0;
|
|
131
|
+
bottom: 0;
|
|
132
|
+
left: 0;
|
|
133
|
+
height: 50px;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.filters {
|
|
139
|
+
margin: 0;
|
|
140
|
+
padding: 0;
|
|
141
|
+
list-style: none;
|
|
142
|
+
position: absolute;
|
|
143
|
+
right: 0;
|
|
144
|
+
left: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.filters li {
|
|
148
|
+
display: inline;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.filters li a {
|
|
152
|
+
color: inherit;
|
|
153
|
+
margin: 3px;
|
|
154
|
+
padding: 3px 7px;
|
|
155
|
+
text-decoration: none;
|
|
156
|
+
border: 1px solid transparent;
|
|
157
|
+
border-radius: 3px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.filters li a:hover {
|
|
161
|
+
border-color: rgba(175, 47, 47, 0.1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.filters li a.selected {
|
|
165
|
+
border-color: rgba(175, 47, 47, 0.2);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.clear-completed,
|
|
169
|
+
html .clear-completed:active {
|
|
170
|
+
float: right;
|
|
171
|
+
position: relative;
|
|
172
|
+
line-height: 20px;
|
|
173
|
+
text-decoration: none;
|
|
174
|
+
cursor: pointer;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.clear-completed:hover {
|
|
178
|
+
text-decoration: underline;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.info {
|
|
182
|
+
margin: 65px auto 0;
|
|
183
|
+
color: #bfbfbf;
|
|
184
|
+
font-size: 10px;
|
|
185
|
+
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
|
|
186
|
+
text-align: center;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.info p {
|
|
190
|
+
line-height: 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.info a {
|
|
194
|
+
color: inherit;
|
|
195
|
+
text-decoration: none;
|
|
196
|
+
font-weight: 400;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.info a:hover {
|
|
200
|
+
text-decoration: underline;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
@media screen and (-webkit-min-device-pixel-ratio: 0) {
|
|
204
|
+
.toggle-all {
|
|
205
|
+
background: none;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@media (max-width: 430px) {
|
|
210
|
+
.footer {
|
|
211
|
+
height: 50px;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.filters {
|
|
215
|
+
bottom: 10px;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.todoapp {
|
|
220
|
+
background: #fff;
|
|
221
|
+
margin: 130px 0 40px 0;
|
|
222
|
+
position: relative;
|
|
223
|
+
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.todoapp input::-webkit-input-placeholder {
|
|
227
|
+
font-style: italic;
|
|
228
|
+
font-weight: 300;
|
|
229
|
+
color: #e6e6e6;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.todoapp input::-moz-placeholder {
|
|
233
|
+
font-style: italic;
|
|
234
|
+
font-weight: 300;
|
|
235
|
+
color: #e6e6e6;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.todoapp input::input-placeholder {
|
|
239
|
+
font-style: italic;
|
|
240
|
+
font-weight: 300;
|
|
241
|
+
color: #e6e6e6;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.todoapp h1 {
|
|
245
|
+
position: absolute;
|
|
246
|
+
top: -155px;
|
|
247
|
+
width: 100%;
|
|
248
|
+
font-size: 100px;
|
|
249
|
+
font-weight: 100;
|
|
250
|
+
text-align: center;
|
|
251
|
+
color: rgba(175, 47, 47, 0.15);
|
|
252
|
+
-webkit-text-rendering: optimizeLegibility;
|
|
253
|
+
-moz-text-rendering: optimizeLegibility;
|
|
254
|
+
text-rendering: optimizeLegibility;
|
|
255
|
+
}
|
|
256
|
+
`)
|
go.mod
CHANGED
|
@@ -3,9 +3,9 @@ module github.com/pyros2097/gromer
|
|
|
3
3
|
go 1.18
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
|
-
github.com/alecthomas/repr v0.1.0
|
|
7
6
|
github.com/go-playground/validator/v10 v10.9.0
|
|
8
7
|
github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f
|
|
8
|
+
github.com/google/uuid v1.3.0
|
|
9
9
|
github.com/gorilla/mux v1.8.0
|
|
10
10
|
github.com/imdario/mergo v0.3.12
|
|
11
11
|
github.com/rs/zerolog v1.26.1
|
|
@@ -30,7 +30,6 @@ require (
|
|
|
30
30
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
|
31
31
|
github.com/golang/protobuf v1.5.2 // indirect
|
|
32
32
|
github.com/google/go-cmp v0.5.6 // indirect
|
|
33
|
-
github.com/google/uuid v1.3.0 // indirect
|
|
34
33
|
github.com/google/wire v0.5.0 // indirect
|
|
35
34
|
github.com/googleapis/gax-go/v2 v2.1.0 // indirect
|
|
36
35
|
github.com/gorilla/css v1.0.0 // indirect
|
go.sum
CHANGED
|
@@ -99,8 +99,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|
|
99
99
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
|
100
100
|
github.com/GoogleCloudPlatform/cloudsql-proxy v1.24.0/go.mod h1:3tx938GhY4FC+E1KT/jNjDw7Z5qxAEtIiERJ2sXjnII=
|
|
101
101
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
|
102
|
-
github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE=
|
|
103
|
-
github.com/alecthomas/repr v0.1.0/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
|
104
102
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
|
105
103
|
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
|
106
104
|
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
gsx/gsx.go
CHANGED
|
@@ -8,9 +8,9 @@ import (
|
|
|
8
8
|
"reflect"
|
|
9
9
|
"regexp"
|
|
10
10
|
"runtime"
|
|
11
|
+
"strconv"
|
|
11
12
|
"strings"
|
|
12
13
|
|
|
13
|
-
"github.com/alecthomas/repr"
|
|
14
14
|
"golang.org/x/net/html"
|
|
15
15
|
"golang.org/x/net/html/atom"
|
|
16
16
|
)
|
|
@@ -41,18 +41,19 @@ type (
|
|
|
41
41
|
}
|
|
42
42
|
Context struct {
|
|
43
43
|
context.Context
|
|
44
|
+
hxRequest bool
|
|
44
|
-
data
|
|
45
|
+
data map[string]interface{}
|
|
45
|
-
metas
|
|
46
|
+
metas map[string]string
|
|
46
|
-
links
|
|
47
|
+
links map[string]link
|
|
47
|
-
scripts
|
|
48
|
+
scripts map[string]bool
|
|
48
49
|
}
|
|
49
50
|
Node struct {
|
|
50
51
|
html.Node
|
|
51
52
|
}
|
|
52
53
|
)
|
|
53
54
|
|
|
54
|
-
func NewContext(c context.Context) Context {
|
|
55
|
+
func NewContext(c context.Context, hxRequest bool) Context {
|
|
55
|
-
return Context{Context: c, data: map[string]interface{}{}, metas: map[string]string{}, links: map[string]link{}, scripts: map[string]bool{}}
|
|
56
|
+
return Context{Context: c, hxRequest: hxRequest, data: map[string]interface{}{}, metas: map[string]string{}, links: map[string]link{}, scripts: map[string]bool{}}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
func (h Context) Get(k string) interface{} {
|
|
@@ -86,34 +87,38 @@ func (h Context) Render(tpl string) *Node {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
func (n *Node) Write(ctx Context, w io.Writer) {
|
|
90
|
+
if !ctx.hxRequest {
|
|
89
|
-
|
|
91
|
+
w.Write([]byte(`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">`))
|
|
90
|
-
|
|
92
|
+
w.Write([]byte(`<meta http-equiv="Content-Type" content="text/html;charset=utf-8"><meta content="utf-8" http-equiv="encoding">`))
|
|
91
|
-
|
|
93
|
+
w.Write([]byte(`<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover">`))
|
|
92
|
-
|
|
94
|
+
for k, v := range ctx.metas {
|
|
93
|
-
|
|
95
|
+
w.Write([]byte(fmt.Sprintf(`<meta name="%s" content="%s">`, k, v)))
|
|
94
|
-
}
|
|
95
|
-
for k, v := range ctx.metas {
|
|
96
|
-
if k == "title" {
|
|
97
|
-
w.Write([]byte(fmt.Sprintf(`<title>%s</title>`, v)))
|
|
98
96
|
}
|
|
97
|
+
for k, v := range ctx.metas {
|
|
98
|
+
if k == "title" {
|
|
99
|
+
w.Write([]byte(fmt.Sprintf(`<title>%s</title>`, v)))
|
|
99
|
-
|
|
100
|
+
}
|
|
100
|
-
for _, v := range ctx.links {
|
|
101
|
-
if v.Type != "" || v.As != "" {
|
|
102
|
-
w.Write([]byte(fmt.Sprintf(`<link rel="%s" href="%s" type="%s" as="%s">`, v.Rel, v.Href, v.Type, v.As)))
|
|
103
|
-
} else {
|
|
104
|
-
w.Write([]byte(fmt.Sprintf(`<link rel="%s" href="%s">`, v.Rel, v.Href)))
|
|
105
101
|
}
|
|
102
|
+
for _, v := range ctx.links {
|
|
103
|
+
if v.Type != "" || v.As != "" {
|
|
104
|
+
w.Write([]byte(fmt.Sprintf(`<link rel="%s" href="%s" type="%s" as="%s">`, v.Rel, v.Href, v.Type, v.As)))
|
|
105
|
+
} else {
|
|
106
|
+
w.Write([]byte(fmt.Sprintf(`<link rel="%s" href="%s">`, v.Rel, v.Href)))
|
|
106
|
-
|
|
107
|
+
}
|
|
108
|
+
}
|
|
107
|
-
|
|
109
|
+
for src, sdefer := range ctx.scripts {
|
|
108
|
-
|
|
110
|
+
if sdefer {
|
|
109
|
-
|
|
111
|
+
w.Write([]byte(fmt.Sprintf(`<script src="%s" defer="true"></script>`, src)))
|
|
110
|
-
|
|
112
|
+
} else {
|
|
111
|
-
|
|
113
|
+
w.Write([]byte(fmt.Sprintf(`<script src="%s"></script>`, src)))
|
|
114
|
+
}
|
|
112
115
|
}
|
|
116
|
+
w.Write([]byte(`</head><body>`))
|
|
113
117
|
}
|
|
114
|
-
w.Write([]byte(`</head><body>`))
|
|
115
118
|
html.Render(w, &n.Node)
|
|
119
|
+
if !ctx.hxRequest {
|
|
116
|
-
|
|
120
|
+
w.Write([]byte(`</body></html>`))
|
|
121
|
+
}
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
func (n *Node) String() string {
|
|
@@ -180,10 +185,13 @@ func convert(ref string, i interface{}) interface{} {
|
|
|
180
185
|
} else {
|
|
181
186
|
return iv
|
|
182
187
|
}
|
|
188
|
+
case int:
|
|
189
|
+
return iv
|
|
183
190
|
case string:
|
|
184
191
|
return iv
|
|
192
|
+
default:
|
|
193
|
+
return iv
|
|
185
194
|
}
|
|
186
|
-
return nil
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
func getRefValue(ctx Context, ref string) interface{} {
|
|
@@ -193,8 +201,14 @@ func getRefValue(ctx Context, ref string) interface{} {
|
|
|
193
201
|
parts := strings.Split(strings.ReplaceAll(ref, "!", ""), ".")
|
|
194
202
|
if len(parts) == 2 {
|
|
195
203
|
if v, ok := ctx.data[parts[0]]; ok {
|
|
204
|
+
a := reflect.ValueOf(v)
|
|
205
|
+
if a.Kind() == reflect.Ptr {
|
|
196
|
-
|
|
206
|
+
i := a.Elem().FieldByName(parts[1]).Interface()
|
|
197
|
-
|
|
207
|
+
return convert(ref, i)
|
|
208
|
+
} else {
|
|
209
|
+
i := a.FieldByName(parts[1]).Interface()
|
|
210
|
+
return convert(ref, i)
|
|
211
|
+
}
|
|
198
212
|
}
|
|
199
213
|
}
|
|
200
214
|
return convert(ref, ctx.data[ref])
|
|
@@ -288,7 +302,6 @@ func populate(ctx Context, n *html.Node) {
|
|
|
288
302
|
}
|
|
289
303
|
continue
|
|
290
304
|
}
|
|
291
|
-
repr.Println("AAAAAAA", n.Data)
|
|
292
305
|
if n.FirstChild == nil {
|
|
293
306
|
continue
|
|
294
307
|
}
|
|
@@ -351,12 +364,23 @@ func populate(ctx Context, n *html.Node) {
|
|
|
351
364
|
|
|
352
365
|
func renderComponent(ctx Context, comp ComponentFunc, n *html.Node) *Node {
|
|
353
366
|
args := []reflect.Value{reflect.ValueOf(ctx)}
|
|
367
|
+
funcType := reflect.TypeOf(comp.Func)
|
|
354
|
-
for
|
|
368
|
+
for i, arg := range comp.Args {
|
|
355
369
|
if v, ok := ctx.data[arg]; ok {
|
|
356
370
|
args = append(args, reflect.ValueOf(v))
|
|
357
371
|
} else {
|
|
358
372
|
v := getAttribute(arg, n.Attr)
|
|
373
|
+
t := funcType.In(i + 1)
|
|
374
|
+
switch t.Kind() {
|
|
375
|
+
case reflect.Int:
|
|
376
|
+
value, _ := strconv.Atoi(v)
|
|
377
|
+
args = append(args, reflect.ValueOf(value))
|
|
378
|
+
case reflect.Bool:
|
|
379
|
+
value, _ := strconv.ParseBool(v)
|
|
380
|
+
args = append(args, reflect.ValueOf(value))
|
|
381
|
+
default:
|
|
359
|
-
|
|
382
|
+
args = append(args, reflect.ValueOf(v))
|
|
383
|
+
}
|
|
360
384
|
}
|
|
361
385
|
}
|
|
362
386
|
result := reflect.ValueOf(comp.Func).Call(args)
|
gsx/gsx_test.go
CHANGED
|
@@ -15,12 +15,12 @@ type TodoData struct {
|
|
|
15
15
|
func Todo(h Context, todo *TodoData) *Node {
|
|
16
16
|
return h.Render(`
|
|
17
17
|
<li id="todo-{todo.ID}" class="{ completed: todo.Completed }">
|
|
18
|
-
<div class="
|
|
18
|
+
<div class="upper">
|
|
19
19
|
<span>{todo.Text}</span>
|
|
20
20
|
<span>{todo.Text}</span>
|
|
21
21
|
</div>
|
|
22
22
|
{children}
|
|
23
|
-
<div class="
|
|
23
|
+
<div class="bottom">
|
|
24
24
|
<span>{todo.Completed}</span>
|
|
25
25
|
<span>{todo.Completed}</span>
|
|
26
26
|
</div>
|
|
@@ -36,6 +36,14 @@ func TodoList(ctx Context, todos []*TodoData) (*Node, error) {
|
|
|
36
36
|
`), nil
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
func TodoCount(ctx Context, count int) (*Node, error) {
|
|
40
|
+
return ctx.Render(`
|
|
41
|
+
<span id="todo-count" class="todo-count" hx-swap-oob="true">
|
|
42
|
+
<strong>{count}</strong> item left
|
|
43
|
+
</span>
|
|
44
|
+
`), nil
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
func WebsiteName() string {
|
|
40
48
|
return "My Website"
|
|
41
49
|
}
|
|
@@ -57,6 +65,7 @@ func TestComponent(t *testing.T) {
|
|
|
57
65
|
<span>{todo.Completed}</span>
|
|
58
66
|
</div>
|
|
59
67
|
</Todo>
|
|
68
|
+
<Todo />
|
|
60
69
|
</div>
|
|
61
70
|
`).String()
|
|
62
71
|
expected := stripWhitespace(`
|
|
@@ -177,21 +186,24 @@ func TestForComponent(t *testing.T) {
|
|
|
177
186
|
r.Equal(expected, actual)
|
|
178
187
|
}
|
|
179
188
|
|
|
180
|
-
|
|
189
|
+
func TestMultipleComonent(t *testing.T) {
|
|
181
|
-
|
|
190
|
+
r := require.New(t)
|
|
182
|
-
|
|
191
|
+
RegisterComponent(Todo, "todo")
|
|
192
|
+
RegisterComponent(TodoCount, "count")
|
|
183
|
-
|
|
193
|
+
h := Context{
|
|
184
|
-
|
|
194
|
+
data: map[string]interface{}{
|
|
195
|
+
"todo": &TodoData{
|
|
196
|
+
ID: "3",
|
|
197
|
+
Text: "My third todo",
|
|
185
|
-
|
|
198
|
+
Completed: false,
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
}
|
|
186
|
-
|
|
202
|
+
actual := h.Render(`
|
|
187
|
-
// <Page title="test">
|
|
188
|
-
|
|
203
|
+
<Todo />
|
|
189
|
-
|
|
204
|
+
<TodoCount />
|
|
190
|
-
|
|
205
|
+
`).String()
|
|
191
|
-
|
|
206
|
+
expected := stripWhitespace(`
|
|
192
|
-
// <page title="test">
|
|
193
|
-
// <meta charset="UTF-8"/>
|
|
194
|
-
// </page>
|
|
195
|
-
|
|
207
|
+
`)
|
|
196
|
-
|
|
208
|
+
r.Equal(expected, actual)
|
|
197
|
-
|
|
209
|
+
}
|
http.go
CHANGED
|
@@ -19,7 +19,6 @@ import (
|
|
|
19
19
|
"sync"
|
|
20
20
|
"time"
|
|
21
21
|
|
|
22
|
-
"github.com/alecthomas/repr"
|
|
23
22
|
"github.com/go-playground/validator/v10"
|
|
24
23
|
"github.com/google/uuid"
|
|
25
24
|
"github.com/gorilla/mux"
|
|
@@ -77,105 +76,6 @@ func RegisterAssets(fs embed.FS) {
|
|
|
77
76
|
appAssets = fs
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
// func RegisterComponent(fn any, props ...string) {
|
|
81
|
-
// name := getFunctionName(fn)
|
|
82
|
-
// fnType := reflect.TypeOf(fn)
|
|
83
|
-
// fnValue := reflect.ValueOf(fn)
|
|
84
|
-
// handlebars.GlobalHelpers.Add(name, func(help handlebars.HelperContext) (template.HTML, error) {
|
|
85
|
-
// args := []reflect.Value{}
|
|
86
|
-
// var props any
|
|
87
|
-
// if fnType.NumIn() > 0 {
|
|
88
|
-
// structType := fnType.In(0)
|
|
89
|
-
// instance := reflect.New(structType)
|
|
90
|
-
// if structType.Kind() != reflect.Struct {
|
|
91
|
-
// log.Fatal().Msgf("component '%s' props should be a struct", name)
|
|
92
|
-
// }
|
|
93
|
-
// rv := instance.Elem()
|
|
94
|
-
// for i := 0; i < structType.NumField(); i++ {
|
|
95
|
-
// if f := rv.Field(i); f.CanSet() {
|
|
96
|
-
// jsonName := structType.Field(i).Tag.Get("json")
|
|
97
|
-
// defaultValue := structType.Field(i).Tag.Get("default")
|
|
98
|
-
// if jsonName == "children" {
|
|
99
|
-
// s, err := help.Block()
|
|
100
|
-
// if err != nil {
|
|
101
|
-
// return "", err
|
|
102
|
-
// }
|
|
103
|
-
// f.Set(reflect.ValueOf(template.HTML(s)))
|
|
104
|
-
// } else {
|
|
105
|
-
// v := help.Context.Get(jsonName)
|
|
106
|
-
// if v == nil {
|
|
107
|
-
// f.Set(reflect.ValueOf(defaultValue))
|
|
108
|
-
// } else {
|
|
109
|
-
// f.Set(reflect.ValueOf(v))
|
|
110
|
-
// }
|
|
111
|
-
// }
|
|
112
|
-
// }
|
|
113
|
-
// }
|
|
114
|
-
// args = append(args, rv)
|
|
115
|
-
// props = rv.Interface()
|
|
116
|
-
// }
|
|
117
|
-
// res := fnValue.Call(args)
|
|
118
|
-
// tpl := res[0].Interface().(*handlebars.Template)
|
|
119
|
-
// tpl.Context.Set("props", props)
|
|
120
|
-
// s, _, err := tpl.Render()
|
|
121
|
-
// if err != nil {
|
|
122
|
-
// return "", err
|
|
123
|
-
// }
|
|
124
|
-
// return template.HTML(s), nil
|
|
125
|
-
// })
|
|
126
|
-
// }
|
|
127
|
-
|
|
128
|
-
// func RegisterContainer(fn any, props ...string) {
|
|
129
|
-
// name := getFunctionName(fn)
|
|
130
|
-
// fnType := reflect.TypeOf(fn)
|
|
131
|
-
// fnValue := reflect.ValueOf(fn)
|
|
132
|
-
// // shandlebars.GlobalHelpers.Add(name, func(help handlebars.HelperContext) (template.HTML, error) {
|
|
133
|
-
// args := []reflect.Value{reflect.ValueOf(context.TODO())}
|
|
134
|
-
// var props any
|
|
135
|
-
// if fnType.NumIn() > 1 {
|
|
136
|
-
// structType := fnType.In(1)
|
|
137
|
-
// instance := reflect.New(structType)
|
|
138
|
-
// if structType.Kind() != reflect.Struct {
|
|
139
|
-
// log.Fatal().Msgf("component '%s' props should be a struct", name)
|
|
140
|
-
// }
|
|
141
|
-
// rv := instance.Elem()
|
|
142
|
-
// for i := 0; i < structType.NumField(); i++ {
|
|
143
|
-
// if f := rv.Field(i); f.CanSet() {
|
|
144
|
-
// jsonName := structType.Field(i).Tag.Get("json")
|
|
145
|
-
// defaultValue := structType.Field(i).Tag.Get("default")
|
|
146
|
-
// if jsonName == "children" {
|
|
147
|
-
// s, err := help.Block()
|
|
148
|
-
// if err != nil {
|
|
149
|
-
// return "", err
|
|
150
|
-
// }
|
|
151
|
-
// f.Set(reflect.ValueOf(template.HTML(s)))
|
|
152
|
-
// } else {
|
|
153
|
-
// v := help.Context.Get(jsonName)
|
|
154
|
-
// if v == nil {
|
|
155
|
-
// f.Set(reflect.ValueOf(defaultValue))
|
|
156
|
-
// } else {
|
|
157
|
-
// f.Set(reflect.ValueOf(v))
|
|
158
|
-
// }
|
|
159
|
-
// }
|
|
160
|
-
// }
|
|
161
|
-
// }
|
|
162
|
-
// args = append(args, rv)
|
|
163
|
-
// props = rv.Interface()
|
|
164
|
-
// }
|
|
165
|
-
// res := fnValue.Call(args)
|
|
166
|
-
// tpl := res[0].Interface().(*handlebars.Template)
|
|
167
|
-
// // if res[1].Interface() != nil {
|
|
168
|
-
// // show error in component
|
|
169
|
-
// // }
|
|
170
|
-
// tpl.Context.Set("props", props)
|
|
171
|
-
// s, _, err := tpl.Render()
|
|
172
|
-
// if err != nil {
|
|
173
|
-
// return "", err
|
|
174
|
-
// }
|
|
175
|
-
// return template.HTML(s), nil
|
|
176
|
-
// })
|
|
177
|
-
// }
|
|
178
|
-
|
|
179
79
|
func RespondError(w http.ResponseWriter, status int, err error) {
|
|
180
80
|
w.Header().Set("Content-Type", "application/json")
|
|
181
81
|
w.WriteHeader(status) // always write status last
|
|
@@ -227,7 +127,7 @@ func addRouteDef(method, route string, h interface{}) {
|
|
|
227
127
|
|
|
228
128
|
func PerformRequest(route string, h interface{}, ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
|
229
129
|
params := GetRouteParams(route)
|
|
230
|
-
renderContext := gsx.NewContext(ctx)
|
|
130
|
+
renderContext := gsx.NewContext(ctx, r.Header.Get("HX-Request") == "true")
|
|
231
131
|
renderContext.Set("requestId", uuid.NewString())
|
|
232
132
|
renderContext.Link("rel", GetAssetUrl("images/icon.png"), "", "")
|
|
233
133
|
renderContext.Link("stylesheet", GetStylesUrl(), "", "")
|
|
@@ -240,7 +140,6 @@ func PerformRequest(route string, h interface{}, ctx context.Context, w http.Res
|
|
|
240
140
|
for _, k := range params {
|
|
241
141
|
args = append(args, reflect.ValueOf(vars[k]))
|
|
242
142
|
}
|
|
243
|
-
repr.Println(len(args), icount)
|
|
244
143
|
if len(args) != icount {
|
|
245
144
|
structType := funcType.In(icount - 1)
|
|
246
145
|
instance := reflect.New(structType)
|
|
@@ -308,6 +207,7 @@ func PerformRequest(route string, h interface{}, ctx context.Context, w http.Res
|
|
|
308
207
|
RespondError(w, 400, fmt.Errorf("Illegal Content-Type tag found %s", contentType))
|
|
309
208
|
return
|
|
310
209
|
}
|
|
210
|
+
renderContext.Set("params", instance.Elem().Interface())
|
|
311
211
|
args = append(args, instance.Elem())
|
|
312
212
|
}
|
|
313
213
|
values := reflect.ValueOf(h).Call(args)
|
|
@@ -387,6 +287,7 @@ func (w *LogResponseWriter) LogRequest(r *http.Request) {
|
|
|
387
287
|
logger := log.WithLevel(zerolog.InfoLevel)
|
|
388
288
|
if w.err != nil {
|
|
389
289
|
stack := string(debug.Stack())
|
|
290
|
+
println(stack)
|
|
390
291
|
logger = log.WithLevel(zerolog.ErrorLevel).Err(w.err).Str("stack", stack).Stack()
|
|
391
292
|
}
|
|
392
293
|
ua := useragent.Parse(r.UserAgent())
|
|
@@ -451,7 +352,8 @@ func CacheMiddleware(next http.Handler) http.Handler {
|
|
|
451
352
|
func StatusHandler(h interface{}) http.Handler {
|
|
452
353
|
return LogMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
453
354
|
ctx := context.WithValue(context.WithValue(r.Context(), "url", r.URL), "header", r.Header)
|
|
355
|
+
renderContext := gsx.NewContext(ctx, r.Header.Get("HX-Request") == "true")
|
|
454
|
-
values := reflect.ValueOf(h).Call([]reflect.Value{reflect.ValueOf(
|
|
356
|
+
values := reflect.ValueOf(h).Call([]reflect.Value{reflect.ValueOf(renderContext)})
|
|
455
357
|
response := values[0].Interface()
|
|
456
358
|
responseStatus := values[1].Interface().(int)
|
|
457
359
|
responseError := values[2].Interface()
|
|
@@ -463,7 +365,7 @@ func StatusHandler(h interface{}) http.Handler {
|
|
|
463
365
|
|
|
464
366
|
// This has to be at end always after headers are set
|
|
465
367
|
w.WriteHeader(responseStatus)
|
|
466
|
-
response.(*gsx.Node).Write(
|
|
368
|
+
response.(*gsx.Node).Write(renderContext, w)
|
|
467
369
|
})).(http.Handler)
|
|
468
370
|
}
|
|
469
371
|
|