~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.
b327ed96
—
pyros2097 5 years ago
remove filterUI elements
- app_nowasm.go +1 -2
- attributes.go +2 -2
- attributes_test.go +1 -1
- component.go +1 -3
- element.go +2 -3
- html.go +87 -2
- node.go +0 -32
- node_test.go +0 -13
- selectors.go +0 -109
- selectors_test.go +0 -263
app_nowasm.go
CHANGED
|
@@ -75,7 +75,6 @@ func createPage(ui UI) *bytes.Buffer {
|
|
|
75
75
|
isLambda := os.Getenv("_LAMBDA_SERVER_PORT") != ""
|
|
76
76
|
page := bytes.NewBuffer(nil)
|
|
77
77
|
page.WriteString("<!DOCTYPE html>\n")
|
|
78
|
-
elems := FilterUIElems(ui)
|
|
79
78
|
basePath := "/assets/"
|
|
80
79
|
if isLambda {
|
|
81
80
|
basePath = "https://go-app-bucket-111.s3.amazonaws.com/"
|
|
@@ -92,7 +91,7 @@ func createPage(ui UI) *bytes.Buffer {
|
|
|
92
91
|
Link("stylesheet", basePath+"styles.css"),
|
|
93
92
|
Script(`const enosys = () => { const a = new Error("not implemented"); return a.code = "ENOSYS", a }; let outputBuf = ""; window.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, writeSync(a, b) { outputBuf += decoder.decode(b); const c = outputBuf.lastIndexOf("\n"); return -1 != c && (console.log(outputBuf.substr(0, c)), outputBuf = outputBuf.substr(c + 1)), b.length }, write(a, b, c, d, e, f) { if (0 !== c || d !== b.length || null !== e) return void f(enosys()); const g = this.writeSync(a, b); f(null, g) } }; const encoder = new TextEncoder("utf-8"), decoder = new TextDecoder("utf-8"); class Go { constructor() { this.argv = ["js"], this.env = {}, this.exit = a => { 0 !== a && console.warn("exit code:", a) }, this._exitPromise = new Promise(a => { this._resolveExitPromise = a }), this._pendingEvent = null, this._scheduledTimeouts = new Map, this._nextCallbackTimeoutID = 1; const a = (a, b) => { this.mem.setUint32(a + 0, b, !0), this.mem.setUint32(a + 4, Math.floor(b / 4294967296), !0) }, b = a => { const b = this.mem.getUint32(a + 0, !0), c = this.mem.getInt32(a + 4, !0); return b + 4294967296 * c }, c = a => { const b = this.mem.getFloat64(a, !0); if (0 !== b) { if (!isNaN(b)) return b; const c = this.mem.getUint32(a, !0); return this._values[c] } }, d = (a, b) => { const c = 2146959360; if ("number" == typeof b) return isNaN(b) ? (this.mem.setUint32(a + 4, 2146959360, !0), void this.mem.setUint32(a, 0, !0)) : 0 === b ? (this.mem.setUint32(a + 4, 2146959360, !0), void this.mem.setUint32(a, 1, !0)) : void this.mem.setFloat64(a, b, !0); switch (b) { case void 0: return void this.mem.setFloat64(a, 0, !0); case null: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 2, !0); case !0: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 3, !0); case !1: return this.mem.setUint32(a + 4, c, !0), void this.mem.setUint32(a, 4, !0); }let d = this._ids.get(b); d === void 0 && (d = this._idPool.pop(), d === void 0 && (d = this._values.length), this._values[d] = b, this._goRefCounts[d] = 0, this._ids.set(b, d)), this._goRefCounts[d]++; let e = 1; switch (typeof b) { case "string": e = 2; break; case "symbol": e = 3; break; case "function": e = 4; }this.mem.setUint32(a + 4, 2146959360 | e, !0), this.mem.setUint32(a, d, !0) }, e = a => { const c = b(a + 0), d = b(a + 8); return new Uint8Array(this._inst.exports.mem.buffer, c, d) }, f = d => { const e = b(d + 0), f = b(d + 8), g = Array(f); for (let a = 0; a < f; a++)g[a] = c(e + 8 * a); return g }, g = a => { const c = b(a + 0), d = b(a + 8); return decoder.decode(new DataView(this._inst.exports.mem.buffer, c, d)) }, h = Date.now() - performance.now(); this.importObject = { go: { "runtime.wasmExit": a => { const b = this.mem.getInt32(a + 8, !0); this.exited = !0, delete this._inst, delete this._values, delete this._goRefCounts, delete this._ids, delete this._idPool, this.exit(b) }, "runtime.wasmWrite": a => { const c = b(a + 8), d = b(a + 16), e = this.mem.getInt32(a + 24, !0); fs.writeSync(c, new Uint8Array(this._inst.exports.mem.buffer, d, e)) }, "runtime.resetMemoryDataView": () => { this.mem = new DataView(this._inst.exports.mem.buffer) }, "runtime.nanotime1": b => { a(b + 8, 1e6 * (h + performance.now())) }, "runtime.walltime1": b => { const c = new Date().getTime(); a(b + 8, c / 1e3), this.mem.setInt32(b + 16, 1e6 * (c % 1e3), !0) }, "runtime.scheduleTimeoutEvent": a => { const c = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++, this._scheduledTimeouts.set(c, setTimeout(() => { for (this._resume(); this._scheduledTimeouts.has(c);)console.warn("scheduleTimeoutEvent: missed timeout event"), this._resume() }, b(a + 8) + 1)), this.mem.setInt32(a + 16, c, !0) }, "runtime.clearTimeoutEvent": a => { const b = this.mem.getInt32(a + 8, !0); clearTimeout(this._scheduledTimeouts.get(b)), this._scheduledTimeouts.delete(b) }, "runtime.getRandomData": a => { crypto.getRandomValues(e(a + 8)) }, "syscall/js.finalizeRef": a => { const b = this.mem.getUint32(a + 8, !0); if (this._goRefCounts[b]--, 0 === this._goRefCounts[b]) { const a = this._values[b]; this._values[b] = null, this._ids.delete(a), this._idPool.push(b) } }, "syscall/js.stringVal": a => { d(a + 24, g(a + 8)) }, "syscall/js.valueGet": a => { const b = Reflect.get(c(a + 8), g(a + 16)); a = this._inst.exports.getsp(), d(a + 32, b) }, "syscall/js.valueSet": a => { Reflect.set(c(a + 8), g(a + 16), c(a + 32)) }, "syscall/js.valueDelete": a => { Reflect.deleteProperty(c(a + 8), g(a + 16)) }, "syscall/js.valueIndex": a => { d(a + 24, Reflect.get(c(a + 8), b(a + 16))) }, "syscall/js.valueSetIndex": a => { Reflect.set(c(a + 8), b(a + 16), c(a + 24)) }, "syscall/js.valueCall": a => { try { const b = c(a + 8), e = Reflect.get(b, g(a + 16)), h = f(a + 32), i = Reflect.apply(e, b, h); a = this._inst.exports.getsp(), d(a + 56, i), this.mem.setUint8(a + 64, 1) } catch (b) { d(a + 56, b), this.mem.setUint8(a + 64, 0) } }, "syscall/js.valueInvoke": a => { try { const b = c(a + 8), e = f(a + 16), g = Reflect.apply(b, void 0, e); a = this._inst.exports.getsp(), d(a + 40, g), this.mem.setUint8(a + 48, 1) } catch (b) { d(a + 40, b), this.mem.setUint8(a + 48, 0) } }, "syscall/js.valueNew": a => { try { const b = c(a + 8), e = f(a + 16), g = Reflect.construct(b, e); a = this._inst.exports.getsp(), d(a + 40, g), this.mem.setUint8(a + 48, 1) } catch (b) { d(a + 40, b), this.mem.setUint8(a + 48, 0) } }, "syscall/js.valueLength": b => { a(b + 16, parseInt(c(b + 8).length)) }, "syscall/js.valuePrepareString": b => { const e = encoder.encode(c(b + 8) + ""); d(b + 16, e), a(b + 24, e.length) }, "syscall/js.valueLoadString": a => { const b = c(a + 8); e(a + 16).set(b) }, "syscall/js.valueInstanceOf": a => { this.mem.setUint8(a + 24, c(a + 8) instanceof c(a + 16)) }, "syscall/js.copyBytesToGo": b => { const d = e(b + 8), f = c(b + 32); if (!(f instanceof Uint8Array)) return void this.mem.setUint8(b + 48, 0); const g = f.subarray(0, d.length); d.set(g), a(b + 40, g.length), this.mem.setUint8(b + 48, 1) }, "syscall/js.copyBytesToJS": b => { const d = c(b + 8), f = e(b + 16); if (!(d instanceof Uint8Array)) return void this.mem.setUint8(b + 48, 0); const g = f.subarray(0, d.length); d.set(g), a(b + 40, g.length), this.mem.setUint8(b + 48, 1) }, debug: a => { console.log(a) } } } } async run(a) { this._inst = a, this.mem = new DataView(this._inst.exports.mem.buffer), this._values = [NaN, 0, null, !0, !1, window, this], this._goRefCounts = [], this._ids = new Map, this._idPool = [], this.exited = !1; let b = 4096; const c = a => { const c = b, d = encoder.encode(a + "\0"); return new Uint8Array(this.mem.buffer, b, d.length).set(d), b += d.length, 0 != b % 8 && (b += 8 - b % 8), c }, d = this.argv.length, e = []; this.argv.forEach(a => { e.push(c(a)) }), e.push(0); const f = Object.keys(this.env).sort(); f.forEach(a => { e.push(c(a + "=" + this.env[a])) }), e.push(0); const g = b; e.forEach(a => { this.mem.setUint32(b, a, !0), this.mem.setUint32(b + 4, 0, !0), b += 8 }), this._inst.exports.run(d, g), this.exited && this._resolveExitPromise(), await this._exitPromise } _resume() { if (this.exited) throw new Error("Go program has already exited"); this._inst.exports.resume(), this.exited && this._resolveExitPromise() } _makeFuncWrapper(a) { const b = this; return function () { const c = { id: a, this: this, args: arguments }; return b._pendingEvent = c, b._resume(), c.result } } } const go = new Go; WebAssembly.instantiateStreaming(fetch("`+basePath+`main.wasm`+`"), go.importObject).then(a => go.run(a.instance)).catch(a => console.error("could not load wasm", a));`),
|
|
94
93
|
),
|
|
95
|
-
Body(
|
|
94
|
+
Body(ui),
|
|
96
95
|
).Html(page)
|
|
97
96
|
return page
|
|
98
97
|
}
|
attributes.go
CHANGED
|
@@ -42,7 +42,7 @@ type HelmetAuthor string
|
|
|
42
42
|
type HelmetKeywords string
|
|
43
43
|
|
|
44
44
|
func mergeAttributes(parent *elem, uis ...interface{}) {
|
|
45
|
-
elems := []
|
|
45
|
+
elems := []UI{}
|
|
46
46
|
for _, v := range uis {
|
|
47
47
|
switch c := v.(type) {
|
|
48
48
|
case CssAttribute:
|
|
@@ -73,5 +73,5 @@ func mergeAttributes(parent *elem, uis ...interface{}) {
|
|
|
73
73
|
panic("unknown type in render")
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
-
parent.setBody(elems
|
|
76
|
+
parent.setBody(elems)
|
|
77
77
|
}
|
attributes_test.go
CHANGED
|
@@ -44,5 +44,5 @@ func TestCreatePage(t *testing.T) {
|
|
|
44
44
|
),
|
|
45
45
|
Body(HomeRoute(NewRenderContext())),
|
|
46
46
|
).Html(page)
|
|
47
|
-
assert.Equal(t, "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\n <meta http-equiv=\"encoding\" content=\"utf-8\">\n <title>\n Title\n </title>\n </head>\n <body>\n <div>\n <div></div>\n <div class=\"flex flex-col justify-center
|
|
47
|
+
assert.Equal(t, "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"UTF-8\">\n <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\n <meta http-equiv=\"encoding\" content=\"utf-8\">\n <title>\n Title\n </title>\n </head>\n <body>\n <div>\n <div></div>\n <div class=\"flex flex-col justify-center items-center\">\n <div class=\"flex flex-row justify-center items-center yellow\">\n Counter\n </div>\n <div class=\"flex flex-row justify-center items-center\">\n <div>\n -\n </div>\n <div>\n 0\n </div>\n <div>\n +\n </div>\n </div>\n </div>\n </div>\n </body>\n</html>", page.String())
|
|
48
48
|
}
|
component.go
CHANGED
|
@@ -32,8 +32,7 @@ func (r RenderFunc) Render() UI {
|
|
|
32
32
|
c.index = 0
|
|
33
33
|
c.eindex = 0
|
|
34
34
|
println("render")
|
|
35
|
-
elems := FilterUIElems(r(c))
|
|
36
|
-
return
|
|
35
|
+
return r(c)
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
func (r RenderFunc) Update() {
|
|
@@ -42,7 +41,6 @@ func (r RenderFunc) Update() {
|
|
|
42
41
|
return
|
|
43
42
|
}
|
|
44
43
|
println("update")
|
|
45
|
-
|
|
46
44
|
if err := r.updateRoot(); err != nil {
|
|
47
45
|
panic(err)
|
|
48
46
|
}
|
element.go
CHANGED
|
@@ -318,12 +318,11 @@ func (e *elem) delJsEventHandler(k string, h js.EventHandler) {
|
|
|
318
318
|
delete(e.events, k)
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
func (e *elem) setBody(body
|
|
321
|
+
func (e *elem) setBody(body []UI) {
|
|
322
322
|
if e.selfClosing {
|
|
323
323
|
panic("setting html element body failed: self closing element can't have children" + e.name())
|
|
324
324
|
}
|
|
325
|
-
|
|
326
|
-
e.body =
|
|
325
|
+
e.body = body
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
func (e *elem) Html(w io.Writer) {
|
html.go
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package app
|
|
2
2
|
|
|
3
|
+
import "reflect"
|
|
4
|
+
|
|
3
5
|
func Html(elems ...UI) *elem {
|
|
4
6
|
return &elem{tag: "html", body: elems}
|
|
5
7
|
}
|
|
@@ -55,9 +57,92 @@ func Div(uis ...interface{}) *elem {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
func Row(uis ...interface{}) UI {
|
|
58
|
-
return Div(append([]interface{}{Css("flex flex-row justify-center
|
|
60
|
+
return Div(append([]interface{}{Css("flex flex-row justify-center items-center")}, uis...)...)
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
func Col(uis ...interface{}) UI {
|
|
62
|
-
return Div(append([]interface{}{Css("flex flex-col justify-center
|
|
64
|
+
return Div(append([]interface{}{Css("flex flex-col justify-center items-center")}, uis...)...)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func If(expr bool, a UI, b UI) UI {
|
|
68
|
+
if expr {
|
|
69
|
+
return a
|
|
70
|
+
}
|
|
71
|
+
return nil
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func IfElse(expr bool, a UI, b UI) UI {
|
|
75
|
+
if expr {
|
|
76
|
+
return a
|
|
77
|
+
}
|
|
78
|
+
return b
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func Map(source interface{}, f func(i int) UI) []UI {
|
|
82
|
+
src := reflect.ValueOf(source)
|
|
83
|
+
if src.Kind() != reflect.Slice {
|
|
84
|
+
panic("range loop source is not a slice: " + src.Type().String())
|
|
85
|
+
}
|
|
86
|
+
body := make([]UI, 0, src.Len())
|
|
87
|
+
for i := 0; i < src.Len(); i++ {
|
|
88
|
+
body = append(body, f(i))
|
|
89
|
+
}
|
|
90
|
+
return body
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
func Map2(source interface{}, f func(v interface{}, i int) UI) []UI {
|
|
94
|
+
src := reflect.ValueOf(source)
|
|
95
|
+
if src.Kind() != reflect.Slice {
|
|
96
|
+
panic("range loop source is not a slice: " + src.Type().String())
|
|
97
|
+
}
|
|
98
|
+
body := make([]UI, 0, src.Len())
|
|
99
|
+
for i := 0; i < src.Len(); i++ {
|
|
100
|
+
body = append(body, f(src.Index(i), i))
|
|
101
|
+
}
|
|
102
|
+
return body
|
|
63
103
|
}
|
|
104
|
+
|
|
105
|
+
// func (r RangeLoop) Slice(f func(int) UI) RangeLoop {
|
|
106
|
+
// src := reflect.ValueOf(r.source)
|
|
107
|
+
// if src.Kind() != reflect.Slice && src.Kind() != reflect.Array {
|
|
108
|
+
// panic("range loop source is not a slice or array: " + src.Type().String())
|
|
109
|
+
// }
|
|
110
|
+
|
|
111
|
+
// body := make([]UI, 0, src.Len())
|
|
112
|
+
// for i := 0; i < src.Len(); i++ {
|
|
113
|
+
// body = append(body, FilterUIElems(f(i))...)
|
|
114
|
+
// }
|
|
115
|
+
|
|
116
|
+
// r.body = body
|
|
117
|
+
// return r
|
|
118
|
+
// }
|
|
119
|
+
|
|
120
|
+
// // Map sets the loop content by repeating the given function for the number
|
|
121
|
+
// // of elements in the source. Elements are ordered by keys.
|
|
122
|
+
// //
|
|
123
|
+
// // It panics if the range source is not a map or if map keys are not strings.
|
|
124
|
+
// func (r RangeLoop) Map(f func(string) UI) RangeLoop {
|
|
125
|
+
// src := reflect.ValueOf(r.source)
|
|
126
|
+
// if src.Kind() != reflect.Map {
|
|
127
|
+
// panic("range loop source is not a map: " + src.Type().String())
|
|
128
|
+
// }
|
|
129
|
+
|
|
130
|
+
// if keyType := src.Type().Key(); keyType.Kind() != reflect.String {
|
|
131
|
+
// panic("range loop source keys are not strings: " + src.Type().String() + keyType.String())
|
|
132
|
+
// }
|
|
133
|
+
|
|
134
|
+
// body := make([]UI, 0, src.Len())
|
|
135
|
+
// keys := make([]string, 0, src.Len())
|
|
136
|
+
|
|
137
|
+
// for _, k := range src.MapKeys() {
|
|
138
|
+
// keys = append(keys, k.String())
|
|
139
|
+
// }
|
|
140
|
+
// sort.Strings(keys)
|
|
141
|
+
|
|
142
|
+
// for _, k := range keys {
|
|
143
|
+
// body = append(body, FilterUIElems(f(k))...)
|
|
144
|
+
// }
|
|
145
|
+
|
|
146
|
+
// r.body = body
|
|
147
|
+
// return r
|
|
148
|
+
// }
|
node.go
CHANGED
|
@@ -2,7 +2,6 @@ package app
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"io"
|
|
5
|
-
"reflect"
|
|
6
5
|
|
|
7
6
|
"github.com/pyros2097/wapp/errors"
|
|
8
7
|
"github.com/pyros2097/wapp/js"
|
|
@@ -30,37 +29,6 @@ type UI interface {
|
|
|
30
29
|
update(UI) error
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
// FilterUIElems returns a filtered version of the given UI elements where
|
|
34
|
-
// selector elements such as If and Range are interpreted and removed. It also
|
|
35
|
-
// remove nil elements.
|
|
36
|
-
//
|
|
37
|
-
// It should be used only when implementing components that can accept content
|
|
38
|
-
// with variadic arguments like HTML elements Body method.
|
|
39
|
-
func FilterUIElems(uis ...interface{}) []UI {
|
|
40
|
-
if len(uis) == 0 {
|
|
41
|
-
return nil
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
elems := make([]UI, 0, len(uis))
|
|
45
|
-
|
|
46
|
-
for _, n := range uis {
|
|
47
|
-
if v := reflect.ValueOf(n); n == nil ||
|
|
48
|
-
v.Kind() == reflect.Ptr && v.IsNil() {
|
|
49
|
-
continue
|
|
50
|
-
}
|
|
51
|
-
switch c := n.(type) {
|
|
52
|
-
case RangeLoop:
|
|
53
|
-
elems = append(elems, c.body...)
|
|
54
|
-
case Condition:
|
|
55
|
-
elems = append(elems, c.body...)
|
|
56
|
-
default:
|
|
57
|
-
elems = append(elems, c.(UI))
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return elems
|
|
62
|
-
}
|
|
63
|
-
|
|
64
32
|
func trackMousePosition(e js.Event) {
|
|
65
33
|
x := e.Get("clientX")
|
|
66
34
|
if !x.Truthy() {
|
node_test.go
CHANGED
|
@@ -8,19 +8,6 @@ import (
|
|
|
8
8
|
"github.com/stretchr/testify/require"
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
func TestFilterUIElems(t *testing.T) {
|
|
12
|
-
var nilText *text
|
|
13
|
-
|
|
14
|
-
simpleText := Text("hello")
|
|
15
|
-
|
|
16
|
-
expectedResult := []UI{
|
|
17
|
-
simpleText,
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
res := FilterUIElems(nil, nilText, simpleText)
|
|
21
|
-
require.Equal(t, expectedResult, res)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
11
|
func TestIsErrReplace(t *testing.T) {
|
|
25
12
|
utests := []struct {
|
|
26
13
|
scenario string
|
selectors.go
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
package app
|
|
2
|
-
|
|
3
|
-
import (
|
|
4
|
-
"reflect"
|
|
5
|
-
"sort"
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
// Range returns a range loop that iterates within the given source. Source must
|
|
9
|
-
// be a slice, an array or a map with strings as keys.
|
|
10
|
-
func Range(src interface{}) RangeLoop {
|
|
11
|
-
return RangeLoop{source: src}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// RangeLoop represents a control structure that iterates within a slice, an
|
|
15
|
-
// array or a map.
|
|
16
|
-
type RangeLoop struct {
|
|
17
|
-
body []UI
|
|
18
|
-
source interface{}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Slice sets the loop content by repeating the given function for the
|
|
22
|
-
// number of elements in the source.
|
|
23
|
-
//
|
|
24
|
-
// It panics if the range source is not a slice or an array.
|
|
25
|
-
func (r RangeLoop) Slice(f func(int) UI) RangeLoop {
|
|
26
|
-
src := reflect.ValueOf(r.source)
|
|
27
|
-
if src.Kind() != reflect.Slice && src.Kind() != reflect.Array {
|
|
28
|
-
panic("range loop source is not a slice or array: " + src.Type().String())
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
body := make([]UI, 0, src.Len())
|
|
32
|
-
for i := 0; i < src.Len(); i++ {
|
|
33
|
-
body = append(body, FilterUIElems(f(i))...)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
r.body = body
|
|
37
|
-
return r
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Map sets the loop content by repeating the given function for the number
|
|
41
|
-
// of elements in the source. Elements are ordered by keys.
|
|
42
|
-
//
|
|
43
|
-
// It panics if the range source is not a map or if map keys are not strings.
|
|
44
|
-
func (r RangeLoop) Map(f func(string) UI) RangeLoop {
|
|
45
|
-
src := reflect.ValueOf(r.source)
|
|
46
|
-
if src.Kind() != reflect.Map {
|
|
47
|
-
panic("range loop source is not a map: " + src.Type().String())
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if keyType := src.Type().Key(); keyType.Kind() != reflect.String {
|
|
51
|
-
panic("range loop source keys are not strings: " + src.Type().String() + keyType.String())
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
body := make([]UI, 0, src.Len())
|
|
55
|
-
keys := make([]string, 0, src.Len())
|
|
56
|
-
|
|
57
|
-
for _, k := range src.MapKeys() {
|
|
58
|
-
keys = append(keys, k.String())
|
|
59
|
-
}
|
|
60
|
-
sort.Strings(keys)
|
|
61
|
-
|
|
62
|
-
for _, k := range keys {
|
|
63
|
-
body = append(body, FilterUIElems(f(k))...)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
r.body = body
|
|
67
|
-
return r
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// If returns a condition that filters the given elements according to the given
|
|
71
|
-
// expression.
|
|
72
|
-
func If(expr bool, elems ...interface{}) Condition {
|
|
73
|
-
if !expr {
|
|
74
|
-
elems = nil
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return Condition{
|
|
78
|
-
body: FilterUIElems(elems...),
|
|
79
|
-
satisfied: expr,
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Condition represents a control structure that displays nodes depending on a
|
|
84
|
-
// given expression.
|
|
85
|
-
type Condition struct {
|
|
86
|
-
body []UI
|
|
87
|
-
satisfied bool
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ElseIf sets the condition with the given nodes if previous expressions
|
|
91
|
-
// were not met and given expression is true.
|
|
92
|
-
func (c Condition) ElseIf(expr bool, elems ...interface{}) Condition {
|
|
93
|
-
if c.satisfied {
|
|
94
|
-
return c
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if expr {
|
|
98
|
-
c.body = FilterUIElems(elems...)
|
|
99
|
-
c.satisfied = expr
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return c
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Else sets the condition with the given UI elements if previous
|
|
106
|
-
// expressions were not met.
|
|
107
|
-
func (c Condition) Else(elems ...interface{}) Condition {
|
|
108
|
-
return c.ElseIf(true, elems...)
|
|
109
|
-
}
|
selectors_test.go
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
package app
|
|
2
|
-
|
|
3
|
-
// import "testing"
|
|
4
|
-
|
|
5
|
-
// func TestRange(t *testing.T) {
|
|
6
|
-
// testUpdate(t, []updateTest{
|
|
7
|
-
// {
|
|
8
|
-
// scenario: "range slice is updated",
|
|
9
|
-
// a: Div().Body(
|
|
10
|
-
// Range([]string{"hello", "world"}).Slice(func(i int) UI {
|
|
11
|
-
// src := []string{"hello", "world"}
|
|
12
|
-
// return Text(src[i])
|
|
13
|
-
// }),
|
|
14
|
-
// ),
|
|
15
|
-
// b: Div().Body(
|
|
16
|
-
// Range([]string{"hello", "maxoo"}).Slice(func(i int) UI {
|
|
17
|
-
// src := []string{"hello", "maxoo"}
|
|
18
|
-
// return Text(src[i])
|
|
19
|
-
// }),
|
|
20
|
-
// ),
|
|
21
|
-
// matches: []TestUIDescriptor{
|
|
22
|
-
// {
|
|
23
|
-
// Path: TestPath(),
|
|
24
|
-
// Expected: Div(),
|
|
25
|
-
// },
|
|
26
|
-
// {
|
|
27
|
-
// Path: TestPath(0),
|
|
28
|
-
// Expected: Text("hello"),
|
|
29
|
-
// },
|
|
30
|
-
// {
|
|
31
|
-
// Path: TestPath(1),
|
|
32
|
-
// Expected: Text("maxoo"),
|
|
33
|
-
// },
|
|
34
|
-
// },
|
|
35
|
-
// },
|
|
36
|
-
// {
|
|
37
|
-
// scenario: "range slice is updated to be empty",
|
|
38
|
-
// a: Div().Body(
|
|
39
|
-
// Range([]string{"hello", "world"}).Slice(func(i int) UI {
|
|
40
|
-
// src := []string{"hello", "world"}
|
|
41
|
-
// return Text(src[i])
|
|
42
|
-
// }),
|
|
43
|
-
// ),
|
|
44
|
-
// b: Div().Body(
|
|
45
|
-
// Range([]string{}).Slice(func(i int) UI {
|
|
46
|
-
// src := []string{"hello", "maxoo"}
|
|
47
|
-
// return Text(src[i])
|
|
48
|
-
// }),
|
|
49
|
-
// ),
|
|
50
|
-
// matches: []TestUIDescriptor{
|
|
51
|
-
// {
|
|
52
|
-
// Path: TestPath(),
|
|
53
|
-
// Expected: Div(),
|
|
54
|
-
// },
|
|
55
|
-
// {
|
|
56
|
-
// Path: TestPath(0),
|
|
57
|
-
// Expected: nil,
|
|
58
|
-
// },
|
|
59
|
-
// {
|
|
60
|
-
// Path: TestPath(1),
|
|
61
|
-
// Expected: nil,
|
|
62
|
-
// },
|
|
63
|
-
// },
|
|
64
|
-
// },
|
|
65
|
-
// {
|
|
66
|
-
// scenario: "range map is updated",
|
|
67
|
-
// a: Div().Body(
|
|
68
|
-
// Range(map[string]string{"key": "value"}).Map(func(k string) UI {
|
|
69
|
-
// src := map[string]string{"key": "value"}
|
|
70
|
-
// return Text(src[k])
|
|
71
|
-
// }),
|
|
72
|
-
// ),
|
|
73
|
-
// b: Div().Body(
|
|
74
|
-
// Range(map[string]string{"key": "value"}).Map(func(k string) UI {
|
|
75
|
-
// src := map[string]string{"key": "maxoo"}
|
|
76
|
-
// return Text(src[k])
|
|
77
|
-
// }),
|
|
78
|
-
// ),
|
|
79
|
-
// matches: []TestUIDescriptor{
|
|
80
|
-
// {
|
|
81
|
-
// Path: TestPath(),
|
|
82
|
-
// Expected: Div(),
|
|
83
|
-
// },
|
|
84
|
-
// {
|
|
85
|
-
// Path: TestPath(0),
|
|
86
|
-
// Expected: Text("maxoo"),
|
|
87
|
-
// },
|
|
88
|
-
// },
|
|
89
|
-
// },
|
|
90
|
-
// })
|
|
91
|
-
// }
|
|
92
|
-
|
|
93
|
-
// func TestCondition(t *testing.T) {
|
|
94
|
-
// testUpdate(t, []updateTest{
|
|
95
|
-
// {
|
|
96
|
-
// scenario: "if is interpreted",
|
|
97
|
-
// a: Div().Body(
|
|
98
|
-
// If(false,
|
|
99
|
-
// H1(),
|
|
100
|
-
// ),
|
|
101
|
-
// ),
|
|
102
|
-
// b: Div().Body(
|
|
103
|
-
// If(true,
|
|
104
|
-
// H1(),
|
|
105
|
-
// ),
|
|
106
|
-
// ),
|
|
107
|
-
// matches: []TestUIDescriptor{
|
|
108
|
-
// {
|
|
109
|
-
// Path: TestPath(),
|
|
110
|
-
// Expected: Div(),
|
|
111
|
-
// },
|
|
112
|
-
|
|
113
|
-
// {
|
|
114
|
-
// Path: TestPath(0),
|
|
115
|
-
// Expected: H1(),
|
|
116
|
-
// },
|
|
117
|
-
// },
|
|
118
|
-
// },
|
|
119
|
-
// {
|
|
120
|
-
// scenario: "if is not interpreted",
|
|
121
|
-
// a: Div().Body(
|
|
122
|
-
// If(true,
|
|
123
|
-
// H1(),
|
|
124
|
-
// ),
|
|
125
|
-
// ),
|
|
126
|
-
// b: Div().Body(
|
|
127
|
-
// If(false,
|
|
128
|
-
// H1(),
|
|
129
|
-
// ),
|
|
130
|
-
// ),
|
|
131
|
-
// matches: []TestUIDescriptor{
|
|
132
|
-
// {
|
|
133
|
-
// Path: TestPath(),
|
|
134
|
-
// Expected: Div(),
|
|
135
|
-
// },
|
|
136
|
-
// {
|
|
137
|
-
// Path: TestPath(0),
|
|
138
|
-
// Expected: nil,
|
|
139
|
-
// },
|
|
140
|
-
// },
|
|
141
|
-
// },
|
|
142
|
-
// {
|
|
143
|
-
// scenario: "else if is interpreted",
|
|
144
|
-
// a: Div().Body(
|
|
145
|
-
// If(true,
|
|
146
|
-
// H1(),
|
|
147
|
-
// ).ElseIf(false,
|
|
148
|
-
// H2(),
|
|
149
|
-
// ),
|
|
150
|
-
// ),
|
|
151
|
-
// b: Div().Body(
|
|
152
|
-
// If(false,
|
|
153
|
-
// H1(),
|
|
154
|
-
// ).ElseIf(true,
|
|
155
|
-
// H2(),
|
|
156
|
-
// ),
|
|
157
|
-
// ),
|
|
158
|
-
// matches: []TestUIDescriptor{
|
|
159
|
-
// {
|
|
160
|
-
// Path: TestPath(),
|
|
161
|
-
// Expected: Div(),
|
|
162
|
-
// },
|
|
163
|
-
|
|
164
|
-
// {
|
|
165
|
-
// Path: TestPath(0),
|
|
166
|
-
// Expected: H2(),
|
|
167
|
-
// },
|
|
168
|
-
// },
|
|
169
|
-
// },
|
|
170
|
-
// {
|
|
171
|
-
// scenario: "else if is not interpreted",
|
|
172
|
-
// a: Div().Body(
|
|
173
|
-
// If(false,
|
|
174
|
-
// H1(),
|
|
175
|
-
// ).ElseIf(true,
|
|
176
|
-
// H2(),
|
|
177
|
-
// ),
|
|
178
|
-
// ),
|
|
179
|
-
// b: Div().Body(
|
|
180
|
-
// If(false,
|
|
181
|
-
// H1(),
|
|
182
|
-
// ).ElseIf(false,
|
|
183
|
-
// H2(),
|
|
184
|
-
// ),
|
|
185
|
-
// ),
|
|
186
|
-
// matches: []TestUIDescriptor{
|
|
187
|
-
// {
|
|
188
|
-
// Path: TestPath(),
|
|
189
|
-
// Expected: Div(),
|
|
190
|
-
// },
|
|
191
|
-
|
|
192
|
-
// {
|
|
193
|
-
// Path: TestPath(0),
|
|
194
|
-
// Expected: nil,
|
|
195
|
-
// },
|
|
196
|
-
// },
|
|
197
|
-
// },
|
|
198
|
-
// {
|
|
199
|
-
// scenario: "else is interpreted",
|
|
200
|
-
// a: Div().Body(
|
|
201
|
-
// If(false,
|
|
202
|
-
// H1(),
|
|
203
|
-
// ).ElseIf(true,
|
|
204
|
-
// H2(),
|
|
205
|
-
// ).Else(
|
|
206
|
-
// H3(),
|
|
207
|
-
// ),
|
|
208
|
-
// ),
|
|
209
|
-
// b: Div().Body(
|
|
210
|
-
// If(false,
|
|
211
|
-
// H1(),
|
|
212
|
-
// ).ElseIf(false,
|
|
213
|
-
// H2(),
|
|
214
|
-
// ).Else(
|
|
215
|
-
// H3(),
|
|
216
|
-
// ),
|
|
217
|
-
// ),
|
|
218
|
-
// matches: []TestUIDescriptor{
|
|
219
|
-
// {
|
|
220
|
-
// Path: TestPath(),
|
|
221
|
-
// Expected: Div(),
|
|
222
|
-
// },
|
|
223
|
-
|
|
224
|
-
// {
|
|
225
|
-
// Path: TestPath(0),
|
|
226
|
-
// Expected: H3(),
|
|
227
|
-
// },
|
|
228
|
-
// },
|
|
229
|
-
// },
|
|
230
|
-
// {
|
|
231
|
-
// scenario: "else is not interpreted",
|
|
232
|
-
// a: Div().Body(
|
|
233
|
-
// If(false,
|
|
234
|
-
// H1(),
|
|
235
|
-
// ).ElseIf(true,
|
|
236
|
-
// H2(),
|
|
237
|
-
// ).Else(
|
|
238
|
-
// H3(),
|
|
239
|
-
// ),
|
|
240
|
-
// ),
|
|
241
|
-
// b: Div().Body(
|
|
242
|
-
// If(true,
|
|
243
|
-
// H1(),
|
|
244
|
-
// ).ElseIf(false,
|
|
245
|
-
// H2(),
|
|
246
|
-
// ).Else(
|
|
247
|
-
// H3(),
|
|
248
|
-
// ),
|
|
249
|
-
// ),
|
|
250
|
-
// matches: []TestUIDescriptor{
|
|
251
|
-
// {
|
|
252
|
-
// Path: TestPath(),
|
|
253
|
-
// Expected: Div(),
|
|
254
|
-
// },
|
|
255
|
-
|
|
256
|
-
// {
|
|
257
|
-
// Path: TestPath(0),
|
|
258
|
-
// Expected: H1(),
|
|
259
|
-
// },
|
|
260
|
-
// },
|
|
261
|
-
// },
|
|
262
|
-
// })
|
|
263
|
-
// }
|