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


17cb1d3e Peter John

4 years ago
fix issues
Files changed (7) hide show
  1. .snapshots/TestHtmlPage +78 -14
  2. cmd/{main.go → wapp/main.go} +0 -0
  3. context.go +0 -71
  4. css.go +51 -0
  5. html.go +84 -13
  6. html_test.go +9 -0
  7. readme.md +8 -0
.snapshots/TestHtmlPage CHANGED
@@ -4,8 +4,67 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
6
6
  <meta http-equiv="encoding" content="utf-8">
7
+ <title> 123 </title>
8
+
9
+ <meta name="description" content="123">
10
+ <meta name="author" content="123">
11
+ <meta name="keywords" content="123">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover">
13
+ <link rel="icon" href="/assets/icon.png">
14
+ <link rel="apple-touch-icon" href="/assets/icon.png">
15
+ <link rel="stylesheet" href="/assets/styles.css">
16
+ <script src="/assets/alpine.js" defer></script>
17
+
7
18
  <meta name="title" content="title">
8
19
  <style>
20
+ *, ::before, ::after { box-sizing: border-box; }
21
+ html { -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; line-height: 1.15; -webkit-text-size-adjust: 100%; }
22
+ body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; }
23
+ hr { height: 0; color: inherit; }
24
+ abbr[title] { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; }
25
+ b, strong { font-weight: bolder; }
26
+ code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 1em; }
27
+ small { font-size: 80%; }
28
+ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
29
+ sub { bottom: -0.25em; }
30
+ sup { top: -0.5em; }
31
+ table { text-indent: 0; border-color: inherit; }
32
+ button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; }
33
+ button, select { text-transform: none; }
34
+ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; }
35
+ ::-moz-focus-inner { border-style: none; padding: 0; }
36
+ :-moz-focusring { outline: 1px dotted ButtonText; outline: auto; }
37
+ :-moz-ui-invalid { box-shadow: none; }
38
+ legend { padding: 0; }
39
+ progress { vertical-align: baseline; }
40
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
41
+ [type='search'] { -webkit-appearance: textfield; outline-offset: -2px; }
42
+ ::-webkit-search-decoration { -webkit-appearance: none; }
43
+ ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; }
44
+ summary { display: list-item; }
45
+ blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre { margin: 0; }
46
+ button { background-color: transparent; background-image: none; }
47
+ fieldset { margin: 0; padding: 0; }
48
+ ol, ul { list-style: none; margin: 0; padding: 0; }
49
+ html { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; line-height: 1.5; }
50
+ body { font-family: inherit; line-height: inherit; }
51
+ *, ::before, ::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }
52
+ hr { border-top-width: 1px; }
53
+ img { border-style: solid; }
54
+ textarea { resize: vertical; }
55
+ input::-moz-placeholder, textarea::-moz-placeholder { opacity: 1; color: #9ca3af; }
56
+ input:-ms-input-placeholder, textarea:-ms-input-placeholder { opacity: 1; color: #9ca3af; }
57
+ input::placeholder, textarea::placeholder { opacity: 1; color: #9ca3af; }
58
+ button, [role="button"] { cursor: pointer; }
59
+ table { border-collapse: collapse; }
60
+ h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; }
61
+ a { color: inherit; text-decoration: inherit; }
62
+ button, input, optgroup, select, textarea { padding: 0; line-height: inherit; color: inherit; }
63
+ pre, code, kbd, samp { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
64
+ img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; }
65
+ img, video { max-width: 100%; height: auto; }
66
+ [hidden] { display: none; }
67
+ *, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
9
68
  .flex { display: flex; }
10
69
  .flex-col { flex-direction: column; }
11
70
  .justify-center { justify-content: center; }
@@ -15,7 +74,8 @@
15
74
  .flex-row { flex-direction: row; }
16
75
  .underline { text-decoration: underline; }
17
76
  .m-20 { margin: 5rem; }
18
- .text-8xl { font-size: 6rem; line-height: 1; } </style>
77
+ .text-8xl { font-size: 6rem; line-height: 1; }
78
+ </style>
19
79
  </head>
20
80
  <body>
21
81
  <h1> Hello this is a h1 </h1>
@@ -24,14 +84,14 @@
24
84
 
25
85
  <h3 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"> </h3>
26
86
 
27
- <counter> <div class="flex flex-col justify-center items-center text-3xl text-gray-700">
87
+ <counter x-data="counter"> <div class="flex flex-col justify-center items-center text-3xl text-gray-700">
28
88
  <div class="flex flex-row justify-center items-center"> <div class="flex flex-row justify-center items-center underline"> Counter </div>
29
89
  </div>
30
90
 
31
- <div x-data="counter" class="flex flex-row justify-center items-center">
91
+ <div class="flex flex-row justify-center items-center" x-data="counter">
32
92
  <button class="btn m-20" @click="decrement"> - </button>
33
93
 
34
- <div x-text="state.count" class="flex flex-row justify-center items-center m-20 text-8xl"> 4 </div>
94
+ <div class="flex flex-row justify-center items-center m-20 text-8xl" x-text="state.count"> 4 </div>
35
95
 
36
96
  <button class="btn m-20" @click="increment"> + </button>
37
97
  </div>
@@ -39,17 +99,21 @@
39
99
  </counter>
40
100
 
41
101
  <script>
102
+ document.addEventListener('alpine:init', () => {
103
+
42
- Alpine.data('counter', ({
104
+ Alpine.data('counter', () => ({
43
- state: {
105
+ state: {
44
- count:1,
106
+ count:1,
45
-
107
+
46
- },
108
+ },
47
- increment() {this.state.count += 1;},
109
+ increment() {this.state.count += 1;},
48
- decrement() {this.state.count -= 1;},
110
+ decrement() {this.state.count -= 1;},
49
-
111
+
50
- }));
112
+ }));
51
113
 
114
+
115
+ });
52
- </script>
116
+ </script>
53
117
  </body>
54
118
 
55
119
  </html>
cmd/{main.go → wapp/main.go} RENAMED
File without changes
context.go DELETED
@@ -1,71 +0,0 @@
1
- package wapp
2
-
3
- import (
4
- "encoding/json"
5
- "net/http"
6
- "reflect"
7
- )
8
-
9
- type State map[string]interface{}
10
- type Actions map[string]func() string
11
-
12
- type Reducer struct {
13
- Name string
14
- State
15
- Actions
16
- }
17
-
18
- func RespondError(w http.ResponseWriter, status int, err error) {
19
- w.WriteHeader(status)
20
- w.Header().Set("Content-Type", "application/json")
21
- data, _ := json.Marshal(map[string]string{
22
- "error": err.Error(),
23
- })
24
- w.Write(data)
25
- }
26
-
27
- func PerformRequest(h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) error {
28
- args := []reflect.Value{reflect.ValueOf(ctx)}
29
- funcType := reflect.TypeOf(h)
30
- icount := funcType.NumIn()
31
- if icount == 2 {
32
- structType := funcType.In(1)
33
- instance := reflect.New(structType)
34
- if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
35
- err := json.NewDecoder(r.Body).Decode(instance.Interface())
36
- if err != nil {
37
- RespondError(w, 500, err)
38
- return err
39
- }
40
- } else if r.Method == "GET" {
41
- rv := instance.Elem()
42
- for i := 0; i < structType.NumField(); i++ {
43
- if f := rv.Field(i); f.CanSet() {
44
- jsonName := structType.Field(i).Tag.Get("json")
45
- jsonValue := r.URL.Query().Get(jsonName)
46
- f.SetString(jsonValue)
47
- }
48
- }
49
- }
50
- args = append(args, instance.Elem())
51
- }
52
- values := reflect.ValueOf(h).Call(args)
53
- response := values[0].Interface()
54
- responseStatus := values[1].Interface().(int)
55
- responseError := values[2].Interface()
56
- if responseError != nil {
57
- RespondError(w, responseStatus, responseError.(error))
58
- return responseError.(error)
59
- }
60
- if v, ok := response.(*HtmlPage); ok {
61
- w.WriteHeader(responseStatus)
62
- w.Header().Set("Content-Type", "text/html")
63
- v.WriteHtml(w)
64
- return nil
65
- }
66
- w.WriteHeader(responseStatus)
67
- w.Header().Set("Content-Type", "application/json")
68
- data, _ := json.Marshal(response)
69
- w.Write(data)
70
- return nil
71
- }
css.go CHANGED
@@ -469,3 +469,54 @@ func mapApply(obj KeyValues) {
469
469
  }
470
470
  }
471
471
  }
472
+
473
+ var normalizeStyles = `
474
+ *, ::before, ::after { box-sizing: border-box; }
475
+ html { -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; line-height: 1.15; -webkit-text-size-adjust: 100%; }
476
+ body { margin: 0; font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji'; }
477
+ hr { height: 0; color: inherit; }
478
+ abbr[title] { -webkit-text-decoration: underline dotted; text-decoration: underline dotted; }
479
+ b, strong { font-weight: bolder; }
480
+ code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace; font-size: 1em; }
481
+ small { font-size: 80%; }
482
+ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
483
+ sub { bottom: -0.25em; }
484
+ sup { top: -0.5em; }
485
+ table { text-indent: 0; border-color: inherit; }
486
+ button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; }
487
+ button, select { text-transform: none; }
488
+ button, [type='button'], [type='reset'], [type='submit'] { -webkit-appearance: button; }
489
+ ::-moz-focus-inner { border-style: none; padding: 0; }
490
+ :-moz-focusring { outline: 1px dotted ButtonText; outline: auto; }
491
+ :-moz-ui-invalid { box-shadow: none; }
492
+ legend { padding: 0; }
493
+ progress { vertical-align: baseline; }
494
+ ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
495
+ [type='search'] { -webkit-appearance: textfield; outline-offset: -2px; }
496
+ ::-webkit-search-decoration { -webkit-appearance: none; }
497
+ ::-webkit-file-upload-button { -webkit-appearance: button; font: inherit; }
498
+ summary { display: list-item; }
499
+ blockquote, dl, dd, h1, h2, h3, h4, h5, h6, hr, figure, p, pre { margin: 0; }
500
+ button { background-color: transparent; background-image: none; }
501
+ fieldset { margin: 0; padding: 0; }
502
+ ol, ul { list-style: none; margin: 0; padding: 0; }
503
+ html { font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; line-height: 1.5; }
504
+ body { font-family: inherit; line-height: inherit; }
505
+ *, ::before, ::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: currentColor; }
506
+ hr { border-top-width: 1px; }
507
+ img { border-style: solid; }
508
+ textarea { resize: vertical; }
509
+ input::-moz-placeholder, textarea::-moz-placeholder { opacity: 1; color: #9ca3af; }
510
+ input:-ms-input-placeholder, textarea:-ms-input-placeholder { opacity: 1; color: #9ca3af; }
511
+ input::placeholder, textarea::placeholder { opacity: 1; color: #9ca3af; }
512
+ button, [role="button"] { cursor: pointer; }
513
+ table { border-collapse: collapse; }
514
+ h1, h2, h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; }
515
+ a { color: inherit; text-decoration: inherit; }
516
+ button, input, optgroup, select, textarea { padding: 0; line-height: inherit; color: inherit; }
517
+ pre, code, kbd, samp { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
518
+ img, svg, video, canvas, audio, iframe, embed, object { display: block; vertical-align: middle; }
519
+ img, video { max-width: 100%; height: auto; }
520
+ [hidden] { display: none; }
521
+ *, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
522
+ `
html.go CHANGED
@@ -2,8 +2,11 @@ package wapp
2
2
 
3
3
  import (
4
4
  "bytes"
5
+ "encoding/json"
5
6
  "fmt"
6
7
  "io"
8
+ "net/http"
9
+ "reflect"
7
10
  "strconv"
8
11
  "strings"
9
12
 
@@ -52,7 +55,7 @@ func (p *HtmlPage) computeCss(elems []*Element) {
52
55
  if s, ok := twClassLookup[c]; ok {
53
56
  if _, ok2 := p.classLookup[c]; !ok2 {
54
57
  p.classLookup[c] = true
55
- p.css.WriteString("\n ." + c + " { " + s + " } ")
58
+ p.css.WriteString(" ." + c + " { " + s + " } \n")
56
59
  }
57
60
  }
58
61
  }
@@ -79,9 +82,13 @@ func (p *HtmlPage) WriteHtml(w io.Writer) {
79
82
  w.Write([]byte("<html>\n"))
80
83
  p.computeCss(p.Body.body)
81
84
  p.computeJs(p.Body.body)
82
- p.Head.body = append(p.Head.body, StyleTag(Text(p.css.String())))
85
+ p.Head.body = append(p.Head.body, StyleTag(Text(normalizeStyles+p.css.String())))
83
86
  p.Head.writeHtmlIndent(w, 1)
84
- p.Body.body = append(p.Body.body, Script(Text(p.js.String())))
87
+ p.Body.body = append(p.Body.body, Script(Text(fmt.Sprintf(`
88
+ document.addEventListener('alpine:init', () => {
89
+ %s
90
+ });
91
+ `, p.js.String()))))
85
92
  p.Body.writeHtmlIndent(w, 1)
86
93
  w.Write([]byte("\n</html>"))
87
94
  }
@@ -109,7 +116,7 @@ func Body(elems ...*Element) *Element {
109
116
  return &Element{tag: "body", body: elems}
110
117
  }
111
118
 
112
- func Component(r Reducer, elems ...*Element) *Element {
119
+ func Component(r Reducer, uis ...interface{}) *Element {
113
120
  v := velvet.NewContext()
114
121
  stateMap := map[string]interface{}{}
115
122
  actionsMap := map[string]interface{}{}
@@ -124,19 +131,19 @@ func Component(r Reducer, elems ...*Element) *Element {
124
131
  v.Set("state", stateMap)
125
132
  v.Set("actions", actionsMap)
126
133
  s, err := velvet.Render(`
127
- Alpine.data('{{ name }}', ({
134
+ Alpine.data('{{ name }}', () => ({
128
- state: {
135
+ state: {
129
- {{#each state}}{{ @key }}{{ @value }},
136
+ {{#each state}}{{ @key }}{{ @value }},
130
- {{/each}}
137
+ {{/each}}
131
- },
138
+ },
132
- {{#each actions}}{{ @key }}{{ @value }},
139
+ {{#each actions}}{{ @key }}{{ @value }},
133
- {{/each}}
140
+ {{/each}}
134
- }));
141
+ }));
135
142
  `, v)
136
143
  if err != nil {
137
144
  panic(err)
138
145
  }
139
- return &Element{tag: r.Name, text: "wapp_js|" + s, body: elems}
146
+ return mergeAttributes(&Element{tag: r.Name, text: "wapp_js|" + s}, append([]interface{}{XData(r.Name)}, uis...)...)
140
147
  }
141
148
 
142
149
  type Element struct {
@@ -429,3 +436,67 @@ func XData(v string) Attribute {
429
436
  func XText(v string) Attribute {
430
437
  return Attribute{"x-text", v}
431
438
  }
439
+
440
+ type State map[string]interface{}
441
+ type Actions map[string]func() string
442
+
443
+ type Reducer struct {
444
+ Name string
445
+ State
446
+ Actions
447
+ }
448
+
449
+ func RespondError(w http.ResponseWriter, status int, err error) {
450
+ w.WriteHeader(status)
451
+ w.Header().Set("Content-Type", "application/json")
452
+ data, _ := json.Marshal(map[string]string{
453
+ "error": err.Error(),
454
+ })
455
+ w.Write(data)
456
+ }
457
+
458
+ func PerformRequest(h interface{}, ctx interface{}, w http.ResponseWriter, r *http.Request) error {
459
+ args := []reflect.Value{reflect.ValueOf(ctx)}
460
+ funcType := reflect.TypeOf(h)
461
+ icount := funcType.NumIn()
462
+ if icount == 2 {
463
+ structType := funcType.In(1)
464
+ instance := reflect.New(structType)
465
+ if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {
466
+ err := json.NewDecoder(r.Body).Decode(instance.Interface())
467
+ if err != nil {
468
+ RespondError(w, 500, err)
469
+ return err
470
+ }
471
+ } else if r.Method == "GET" {
472
+ rv := instance.Elem()
473
+ for i := 0; i < structType.NumField(); i++ {
474
+ if f := rv.Field(i); f.CanSet() {
475
+ jsonName := structType.Field(i).Tag.Get("json")
476
+ jsonValue := r.URL.Query().Get(jsonName)
477
+ f.SetString(jsonValue)
478
+ }
479
+ }
480
+ }
481
+ args = append(args, instance.Elem())
482
+ }
483
+ values := reflect.ValueOf(h).Call(args)
484
+ response := values[0].Interface()
485
+ responseStatus := values[1].Interface().(int)
486
+ responseError := values[2].Interface()
487
+ if responseError != nil {
488
+ RespondError(w, responseStatus, responseError.(error))
489
+ return responseError.(error)
490
+ }
491
+ if v, ok := response.(HtmlPage); ok {
492
+ w.WriteHeader(responseStatus)
493
+ w.Header().Set("Content-Type", "text/html")
494
+ v.WriteHtml(w)
495
+ return nil
496
+ }
497
+ w.WriteHeader(responseStatus)
498
+ w.Header().Set("Content-Type", "application/json")
499
+ data, _ := json.Marshal(response)
500
+ w.Write(data)
501
+ return nil
502
+ }
html_test.go CHANGED
@@ -98,6 +98,15 @@ func TestHtmlPage(t *testing.T) {
98
98
  b := bytes.NewBuffer(nil)
99
99
  p := Html(
100
100
  Head(
101
+ Title("123"),
102
+ Meta("description", "123"),
103
+ Meta("author", "123"),
104
+ Meta("keywords", "123"),
105
+ Meta("viewport", "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, viewport-fit=cover"),
106
+ Link("icon", "/assets/icon.png"),
107
+ Link("apple-touch-icon", "/assets/icon.png"),
108
+ Link("stylesheet", "/assets/styles.css"),
109
+ Script(Src("/assets/alpine.js"), Defer()),
101
110
  Meta("title", "title"),
102
111
  ),
103
112
  Body(
readme.md CHANGED
@@ -15,6 +15,14 @@ It uses a declarative syntax using funcs that allows creating and dealing with H
15
15
  go mod init
16
16
  go get -u -v github.com/pyros2097/wapp
17
17
  ```
18
+
19
+ # Install Cli
20
+
21
+ ```sh
22
+ go mod init
23
+ go get -u -v github.com/pyros2097/wapp/cmd/wapp
24
+ ```
25
+
18
26
  # Example
19
27
 
20
28
  https://github.com/pyros2097/wapp-example