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


5c294ccd Peter John

4 years ago
new stuff
.snapshots/{TestHtmlPage → TestHtmlPage.html} RENAMED
@@ -65,16 +65,16 @@
65
65
  img, video { max-width: 100%; height: auto; }
66
66
  [hidden] { display: none; }
67
67
  *, ::before, ::after { --tw-border-opacity: 1; border-color: rgba(229, 231, 235, var(--tw-border-opacity)); }
68
- .flex { display: flex; }
68
+ .flex { display: flex; }
69
- .flex-col { flex-direction: column; }
69
+ .flex-col { flex-direction: column; }
70
- .justify-center { justify-content: center; }
70
+ .justify-center { justify-content: center; }
71
- .items-center { align-items: center; }
71
+ .items-center { align-items: center; }
72
- .text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
72
+ .text-3xl { font-size: 1.875rem; line-height: 2.25rem; }
73
- .text-gray-700 { color: rgba(55, 65, 81, 1); }
73
+ .text-gray-700 { color: rgba(55, 65, 81, 1); }
74
- .flex-row { flex-direction: row; }
74
+ .flex-row { flex-direction: row; }
75
- .underline { text-decoration: underline; }
75
+ .underline { text-decoration: underline; }
76
- .m-20 { margin: 5rem; }
76
+ .m-20 { margin: 5rem; }
77
- .text-8xl { font-size: 6rem; line-height: 1; }
77
+ .text-5xl { font-size: 3rem; line-height: 1; }
78
78
  </style>
79
79
  </head>
80
80
  <body>
@@ -82,36 +82,23 @@
82
82
 
83
83
  <h2> Hello this is a h2 </h2>
84
84
 
85
- <h3 x-text="message" x-data="{ message: 'I ❤️ Alpine' }"> </h3>
85
+ <h3 x-data="{ message: 'I ❤️ Alpine' }" x-text="message"> </h3>
86
86
 
87
- <counter x-data="counter"> <div class="flex flex-col justify-center items-center text-3xl text-gray-700">
87
+ <div class="flex flex-col justify-center items-center text-3xl text-gray-700">
88
- <div class="flex flex-row justify-center items-center"> <div class="flex flex-row justify-center items-center underline"> Counter </div>
88
+ <div class="flex flex-row justify-center items-center"> <div class="flex flex-row justify-center items-center underline"> Counter </div>
89
- </div>
89
+ </div>
90
90
 
91
- <div x-data="counter" class="flex flex-row justify-center items-center">
91
+ <div class="flex flex-row justify-center items-center">
92
- <button class="btn m-20" @click="decrement"> - </button>
92
+ <button @click="Decrement"> - </button>
93
93
 
94
- <div class="flex flex-row justify-center items-center m-20 text-8xl" x-text="state.count"> 4 </div>
94
+ <div x-text="state.count" class="flex flex-row justify-center items-center m-20 text-5xl"> 4 </div>
95
95
 
96
- <button class="btn m-20" @click="increment"> + </button>
96
+ <button @click="Increment"> + </button>
97
- </div>
98
97
  </div>
99
- </counter>
98
+ </div>
100
99
 
101
100
  <script>
102
101
  document.addEventListener('alpine:init', () => {
103
-
104
- Alpine.data('counter', () => ({
105
- state: {
106
- count:1,
107
-
108
- },
109
- increment() {this.state.count += 1;},
110
- decrement() {this.state.count -= 1;},
111
-
112
- }));
113
-
114
-
115
102
  });
116
103
  </script>
117
104
  </body>
aa/main.go ADDED
@@ -0,0 +1,131 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+
7
+ "golang.org/x/net/html"
8
+ )
9
+
10
+ type M map[string]interface{}
11
+
12
+ type ReqContext struct {
13
+ JS *bytes.Buffer
14
+ CSS *bytes.Buffer
15
+ }
16
+
17
+ type Component func(ReqContext) string
18
+
19
+ var components = map[string]Component{}
20
+
21
+ func RegisterComponent(k string, v Component) {
22
+ components[k] = v
23
+ }
24
+
25
+ func Html2(ctx ReqContext, input string, data map[string]interface{}) string {
26
+ return input
27
+ }
28
+
29
+ func ParseHTML(ctx ReqContext, input string, data map[string]interface{}) *html.Node {
30
+ doc, err := html.Parse(bytes.NewBufferString(input))
31
+ if err != nil {
32
+ panic(err)
33
+ }
34
+ return doc
35
+ }
36
+
37
+ func init() {
38
+ RegisterComponent("Layout", func(c ReqContext) string {
39
+ return Html2(c, `
40
+ <html>
41
+ <body>
42
+ <div>
43
+ <slot></slot>
44
+ </div>
45
+ </body>
46
+ </html>
47
+ `, M{})
48
+ })
49
+ }
50
+
51
+ func properTitle(input string) string {
52
+ words := strings.Split(input, " ")
53
+ smallwords := " a an on the to "
54
+
55
+ for index, word := range words {
56
+ if strings.Contains(smallwords, " "+word+" ") && word != string(word[0]) {
57
+ words[index] = word
58
+ } else {
59
+ words[index] = strings.Title(word)
60
+ }
61
+ }
62
+ return strings.Join(words, " ")
63
+ }
64
+
65
+ func main() {
66
+ // vctx := velvet.NewContext()
67
+ // for k, v := range data {
68
+ // vctx.Set(k, v)
69
+ // }
70
+ // s, err := velvet.Render(textOutput, vctx)
71
+ // if err != nil {
72
+ // panic(err)
73
+ // }
74
+ ctx := ReqContext{JS: bytes.NewBuffer(nil), CSS: bytes.NewBuffer(nil)}
75
+ txt := `
76
+ <Layout>
77
+ <p>
78
+ Hello world
79
+ </p>
80
+ </Layout>
81
+ `
82
+ docs := ParseHTML(ctx, txt, M{})
83
+ textOutput := ""
84
+ var f func(txt string, n *html.Node)
85
+ f = func(txt string, n *html.Node) {
86
+ removeExtraStuff := func(tag string) bool {
87
+ return !strings.Contains(txt, "<"+tag+">") && n.Type == html.ElementNode && n.Data == tag
88
+ }
89
+ constainsHtml := removeExtraStuff("html") || removeExtraStuff("body") || removeExtraStuff("head")
90
+ if !constainsHtml && n.Type == html.ElementNode {
91
+ textOutput += "<" + n.Data + ">"
92
+ }
93
+ if n.Type == html.TextNode {
94
+ textOutput += n.Data
95
+ }
96
+ if n.Type == html.ElementNode && n.Data == "slot" {
97
+ }
98
+ if c, ok := components[properTitle(n.Data)]; ok {
99
+ ctext := c(ctx)
100
+ newNodes := ParseHTML(ctx, ctext, M{})
101
+ f(ctext, newNodes)
102
+ }
103
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
104
+ f(txt, c)
105
+ }
106
+ if !constainsHtml && n.Type == html.ElementNode {
107
+ textOutput += "</" + n.Data + ">"
108
+ }
109
+ }
110
+ f(txt, docs)
111
+ println("textOutput", textOutput)
112
+ }
113
+
114
+ // func Index2(c *context.ReqContext) (interface{}, int, error) {
115
+ // data := M{
116
+ // "userID": c.UserID,
117
+ // "message": "I ❤️ Alpine",
118
+ // }
119
+ // return Html(`
120
+ // <page x-data="pageData">
121
+ // <div class="flex flex-col items-center justify-center">
122
+ // <header></header>
123
+ // <h1>Hello <template x-text="userID"></template></h1>
124
+ // <h2>Hello this is a h1</h1>
125
+ // <h2>Hello this is a h2</h1>
126
+ // <h3 x-text="message"></h3>
127
+ // <counter start={4}></counter>
128
+ // </div>
129
+ // </page>
130
+ // `, data), 200, nil
131
+ // }
cmd/scripts/migrate/main.go ADDED
@@ -0,0 +1,32 @@
1
+ package main
2
+
3
+ import (
4
+ c "context"
5
+ "io/ioutil"
6
+
7
+ "wapp-example/context"
8
+ )
9
+
10
+ func main() {
11
+ db := context.InitDB()
12
+ ctx := c.Background()
13
+ tx, err := context.BeginTransaction(db, ctx)
14
+ if err != nil {
15
+ panic(err)
16
+ }
17
+ files, err := ioutil.ReadDir("./migrations")
18
+ if err != nil {
19
+ panic(err)
20
+ }
21
+ for _, f := range files {
22
+ data, err := ioutil.ReadFile("./migrations/" + f.Name())
23
+ if err != nil {
24
+ panic(err)
25
+ }
26
+ tx.MustExec(string(data))
27
+ }
28
+ err = tx.Commit()
29
+ if err != nil {
30
+ panic(err)
31
+ }
32
+ }
cmd/scripts/seed/main.go ADDED
@@ -0,0 +1,38 @@
1
+ package main
2
+
3
+ import (
4
+ c "context"
5
+
6
+ "github.com/bxcodec/faker/v3"
7
+
8
+ "wapp-example/context"
9
+ "wapp-example/pages/api/todos"
10
+ )
11
+
12
+ func main() {
13
+ db := context.InitDB()
14
+ ctx := c.Background()
15
+ tx, err := context.BeginTransaction(db, ctx)
16
+ if err != nil {
17
+ panic(err)
18
+ }
19
+ reqContext := context.ReqContext{
20
+ Tx: tx,
21
+ UserID: "123",
22
+ }
23
+ for i := 0; i < 20; i++ {
24
+ ti := todos.TodoInput{}
25
+ err := faker.FakeData(&ti)
26
+ if err != nil {
27
+ panic(err)
28
+ }
29
+ _, _, err = todos.POST(reqContext, ti)
30
+ if err != nil {
31
+ panic(err)
32
+ }
33
+ }
34
+ err = tx.Commit()
35
+ if err != nil {
36
+ panic(err)
37
+ }
38
+ }
css.go CHANGED
@@ -3,6 +3,7 @@ package wapp
3
3
  type M map[string]interface{}
4
4
  type MS map[string]string
5
5
  type Arr []interface{}
6
+
6
7
  type KeyValues struct {
7
8
  Keys M
8
9
  Values MS
html.go CHANGED
@@ -7,10 +7,9 @@ import (
7
7
  "io"
8
8
  "net/http"
9
9
  "reflect"
10
+ "runtime"
10
11
  "strconv"
11
12
  "strings"
12
-
13
- "github.com/gobuffalo/velvet"
14
13
  )
15
14
 
16
15
  func writeIndent(w io.Writer, indent int) {
@@ -55,7 +54,7 @@ func (p *HtmlPage) computeCss(elems []*Element) {
55
54
  if s, ok := twClassLookup[c]; ok {
56
55
  if _, ok2 := p.classLookup[c]; !ok2 {
57
56
  p.classLookup[c] = true
58
- p.css.WriteString(" ." + c + " { " + s + " } \n")
57
+ p.css.WriteString(" ." + c + " { " + s + " }\n")
59
58
  }
60
59
  }
61
60
  }
@@ -69,7 +68,7 @@ func (p *HtmlPage) computeCss(elems []*Element) {
69
68
  func (p *HtmlPage) computeJs(elems []*Element) {
70
69
  for _, el := range elems {
71
70
  if strings.HasPrefix(el.text, "wapp_js|") {
72
- p.js.WriteString(strings.Replace(el.text, "wapp_js|", "", 1) + "\n")
71
+ p.js.WriteString(strings.Replace(el.text, "wapp_js|", "", 1)) // + "\n" // TODO check with multiple components
73
72
  }
74
73
  if len(el.children) > 0 {
75
74
  p.computeJs(el.children)
@@ -85,8 +84,7 @@ func (p *HtmlPage) WriteHtml(w io.Writer) {
85
84
  p.Head.children = append(p.Head.children, StyleTag(Text(normalizeStyles+p.css.String())))
86
85
  p.Head.writeHtmlIndent(w, 1)
87
86
  p.Body.children = append(p.Body.children, Script(Text(fmt.Sprintf(`
88
- document.addEventListener('alpine:init', () => {
87
+ document.addEventListener('alpine:init', () => {%s
89
- %s
90
88
  });
91
89
  `, p.js.String()))))
92
90
  p.Body.writeHtmlIndent(w, 1)
@@ -116,34 +114,26 @@ func Body(elems ...*Element) *Element {
116
114
  return &Element{tag: "body", children: elems}
117
115
  }
118
116
 
119
- func Component(r Reducer, uis ...interface{}) *Element {
117
+ func Component(state interface{}, uis ...interface{}) *Element {
120
- v := velvet.NewContext()
118
+ pc, _, _, _ := runtime.Caller(1)
119
+ details := runtime.FuncForPC(pc)
120
+ arr := strings.Split(details.Name(), ".")
121
- stateMap := map[string]interface{}{}
121
+ name := strings.ToLower(arr[len(arr)-1])
122
- actionsMap := map[string]interface{}{}
122
+ // actionsMap := map[string]interface{}{}
123
- // structType := reflect.TypeOf(r)
123
+ // actionsType := reflect.TypeOf(actions)
124
- for k, v := range r.State {
125
- stateMap[k+":"] = v
126
- }
127
- for k, v := range r.Actions {
124
+ // for k, v := range r.Actions {
128
- actionsMap[k] = "() {" + v() + "}"
125
+ // actionsMap[k] = "() {" + v() + "}"
126
+ // }
127
+ stateData, err := json.MarshalIndent(state, " ", " ")
128
+ if err != nil {
129
+ panic(err)
129
130
  }
130
- v.Set("name", r.Name)
131
- v.Set("state", stateMap)
132
- v.Set("actions", actionsMap)
133
- s, err := velvet.Render(`
131
+ js := fmt.Sprintf(`
134
- Alpine.data('{{ name }}', () => ({
132
+ Alpine.data('%s', () => (%s));`, name, string(stateData))
135
- state: {
136
- {{#each state}}{{ @key }}{{ @value }},
137
- {{/each}}
138
- },
139
- {{#each actions}}{{ @key }}{{ @value }},
140
- {{/each}}
141
- }));
142
- `, v)
143
133
  if err != nil {
144
134
  panic(err)
145
135
  }
146
- return mergeAttributes(&Element{tag: r.Name, text: "wapp_js|" + s}, append([]interface{}{XData(r.Name)}, uis...)...)
136
+ return mergeAttributes(&Element{tag: name, text: "wapp_js|" + js}, append([]interface{}{XData(name)}, uis...)...)
147
137
  }
148
138
 
149
139
  type Element struct {
@@ -349,8 +339,14 @@ func Attr(k, v string) Attribute {
349
339
  return Attribute{k, v}
350
340
  }
351
341
 
342
+ func GetFunctionName(i interface{}) string {
343
+ fnName := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
344
+ parts := strings.Split(fnName, ".")
345
+ return strings.Replace(parts[len(parts)-1], "-fm", "", 1)
346
+ }
347
+
352
- func OnClick(v string) Attribute {
348
+ func OnClick(v interface{}) Attribute {
353
- return Attribute{"@click", v}
349
+ return Attribute{"@click", GetFunctionName(v)}
354
350
  }
355
351
 
356
352
  func ID(v string) Attribute {
@@ -437,15 +433,6 @@ func XText(v string) Attribute {
437
433
  return Attribute{"x-text", v}
438
434
  }
439
435
 
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
436
  func RespondError(w http.ResponseWriter, status int, err error) {
450
437
  w.WriteHeader(status)
451
438
  w.Header().Set("Content-Type", "application/json")
html_test.go CHANGED
@@ -16,84 +16,63 @@ func Col(uis ...interface{}) *Element {
16
16
  return NewElement("div", false, append([]interface{}{Css("flex flex-col justify-center items-center")}, uis...)...)
17
17
  }
18
18
 
19
- // type Container func() *Element
20
-
21
- // func CounterData(start int) Container {
22
- // return CreateContainer(Reducer{
23
- // Name: "counter",
24
- // State: State{
25
- // "count": start,
26
- // },
27
- // Actions: Actions{
28
- // "increment": func() string { return "this.state.count += 1;" },
29
- // "decrement": func() string { return "this.state.count -= 1;" },
30
- // },
31
- // })
32
- // }
33
-
34
- // type CounterState struct {
19
+ type CounterState struct {
35
- // Count int
20
+ Count int
36
- // }
21
+ }
37
-
38
- // type CounterActions struct {
39
- // Increment func() string
40
- // Decrement func() string
41
- // }
42
22
 
43
- // type CounterReducer struct {
23
+ func Counter(start int) *Element {
44
- // Reducer
45
- // State CounterState
24
+ c := CounterState{Count: start}
46
- // Actions CounterActions
47
- // }
25
+ return c.Render()
26
+ }
48
27
 
49
- // func CounterData(start int) CounterReducer {
50
- // return CounterReducer{
51
- // Reducer: "counter",
52
- // State: CounterState{
28
+ func (c *CounterState) Increment() {
53
- // Count: 1,
54
- // },
55
- // Actions: CounterActions{
56
- // Increment: func() string { return "this.state.count += 1;" },
57
- // Decrement: func() string { return "this.state.count -= 1;" },
58
- // },
59
- // }
60
- // }
29
+ c.Count += 1
30
+ }
61
31
 
62
- func CounterData(start int) Reducer {
32
+ func (c *CounterState) Decrement() {
63
- return Reducer{
64
- Name: "counter",
65
- State: State{
66
- "count": 1,
33
+ c.Count -= 1
67
- },
68
- Actions: Actions{
69
- "increment": func() string { return "this.state.count += 1;" },
70
- "decrement": func() string { return "this.state.count -= 1;" },
71
- },
72
- }
73
34
  }
74
35
 
75
- func Counter(start int) *Element {
36
+ func (c *CounterState) Render() *Element {
76
- data := CounterData(start)
77
- return Component(data, Col(Css("text-3xl text-gray-700"),
37
+ return Col(Css("text-3xl text-gray-700"),
78
38
  Row(
79
39
  Row(Css("underline"),
80
40
  Text("Counter"),
81
41
  ),
82
42
  ),
83
- Row(XData("counter"),
43
+ Row(
84
- Button(Css("btn m-20"), OnClick("decrement"),
44
+ Button(OnClick(c.Decrement), Text("-")),
85
- Text("-"),
86
- ),
87
- Row(Css("m-20 text-8xl"), XText("state.count"),
45
+ Row(Css("m-20 text-5xl"), XText("state.count"),
88
- Text(strconv.Itoa(start)),
46
+ Text(strconv.Itoa(c.Count)),
89
- ),
90
- Button(Css("btn m-20"), OnClick("increment"),
91
- Text("+"),
92
47
  ),
48
+ Button(OnClick(c.Increment), Text("+")),
93
49
  ),
94
- ))
50
+ )
95
51
  }
96
52
 
53
+ // func CounterHtml() string {
54
+ // return `
55
+ // <div class="flex flex-col justify-center items-center text-3xl text-gray-700">
56
+ // <div class="flex flex-row justify-center items-center">
57
+ // <div class="flex flex-row justify-center items-center underline">
58
+ // Counter
59
+ // </div>
60
+ // </div>
61
+ // <div class="flex flex-row justify-center items-center">
62
+ // <button class="btn m-20" @click={{actions.Increment}}>
63
+ // -
64
+ // </button>
65
+ // <div class="flex flex-row justify-center items-center m-20 text-8xl">
66
+ // {{ state.count }}
67
+ // </div>
68
+ // <button class="btn m-20" @click={{actions.Decrement}}>
69
+ // +
70
+ // </button>
71
+ // </div>
72
+ // </div>
73
+ // `
74
+ // }
75
+
97
76
  func TestHtmlPage(t *testing.T) {
98
77
  b := bytes.NewBuffer(nil)
99
78
  p := Html(
@@ -117,5 +96,6 @@ func TestHtmlPage(t *testing.T) {
117
96
  ),
118
97
  )
119
98
  p.WriteHtml(b)
99
+ c := cupaloy.New(cupaloy.SnapshotFileExtension(".html"))
120
- cupaloy.SnapshotT(t, b.String())
100
+ c.SnapshotT(t, b.String())
121
101
  }