-
Notifications
You must be signed in to change notification settings - Fork 0
/
virtualdom.go
221 lines (178 loc) · 5.39 KB
/
virtualdom.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//go:build js && wasm
package lander
import (
"fmt"
"strings"
"sync"
"syscall/js"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/minivera/go-lander/context"
"github.com/minivera/go-lander/diffing"
"github.com/minivera/go-lander/events"
"github.com/minivera/go-lander/internal"
"github.com/minivera/go-lander/nodes"
)
var document js.Value
func init() {
document = js.Global().Get("document")
}
// DomEnvironment is the lander DOM environment that stores the necessary information to allow mounting
// and rendering a lander app. Keep this environment in the main method or in global memory to avoid any
// memory loss.
type DomEnvironment struct {
sync.RWMutex
root string
tree *nodes.FuncNode
prevContext context.Context
}
// RenderInto renders the provided root component node into the given DOM root. The root selector must
// lead to a valid node, otherwise the mounting will error. The tree is only mounted in this method,
// no diffing will happen. Returns the mounted DOM environment, which can be used to trigger updates.
//
// This function is thread safe and will not allow any updates while the first mount is in progress. Event
// listeners or effects triggered during the mount process will have to wait.
func RenderInto(rootNode *nodes.FuncNode, root string) (*DomEnvironment, error) {
env := &DomEnvironment{
root: root,
tree: rootNode,
}
env.Lock()
defer env.Unlock()
err := env.renderIntoRoot()
if err != nil {
return nil, err
}
return env, nil
}
// Update triggers the diffing process and updates the virtual and real DOM tree. The app provided to
// RenderInto will rerender fully and be diffed against the previously store tree. The diffing process
// generates a set of patches, which are executed in sequence against both the real and virtual DOM trees
// to update the stored tree with the new changes.
//
// This function is NOT thread safe and many allow other updates while another is in progress. Trigger an
// Update in an event listener to use the thread safe features of Lander.
func (e *DomEnvironment) Update() error {
err := e.patchDom()
if err != nil {
return err
}
return nil
}
func (e *DomEnvironment) renderIntoRoot() error {
rootElem := document.Call("querySelector", e.root)
if !rootElem.Truthy() {
return fmt.Errorf("failed to find mount parent using query selector %q", e.root)
}
var styles []string
err := context.WithNewContext(e.Update, nil, func() error {
styles = diffing.RecursivelyMount(e.handleDOMEvent, document, rootElem, e.tree)
e.prevContext = context.CurrentContext
return nil
})
if err != nil {
return err
}
e.printTree(e.tree, 0)
m := minify.New()
m.AddFunc("text/css", css.Minify)
stylesString, err := m.String("text/css", strings.Join(styles, " "))
if err != nil {
return fmt.Errorf("could not minify CSS styles from HTML nodes. %w", err)
}
head := document.Call("querySelector", "head")
if !head.Truthy() {
return fmt.Errorf("failed to find head using query selector")
}
styleTag := document.Call("createElement", "style")
styleTag.Set("id", "lander-style-tag")
styleTag.Set("innerHTML", stylesString)
head.Call("appendChild", styleTag)
return nil
}
func (e *DomEnvironment) patchDom() error {
rootElem := document.Call("querySelector", e.root)
if !rootElem.Truthy() {
return fmt.Errorf("failed to find mount parent using query selector %q", e.root)
}
var styles []string
err := context.WithNewContext(e.Update, e.prevContext, func() error {
baseIndex := 0
patches, renderedStyles, err := diffing.GeneratePatches(
e.handleDOMEvent,
nil,
rootElem,
&baseIndex,
e.tree,
e.tree.Clone(),
)
if err != nil {
return err
}
for _, patch := range patches {
err := patch.Execute(document, &renderedStyles)
if err != nil {
return err
}
}
styles = renderedStyles
e.prevContext = context.CurrentContext
return nil
})
if err != nil {
return err
}
e.printTree(e.tree, 0)
styleTag := document.Call("querySelector", "#lander-style-tag")
if !styleTag.Truthy() {
return fmt.Errorf("failed to find the style selector, failing %s", "#lander-style-tag")
}
m := minify.New()
m.AddFunc("text/css", css.Minify)
stylesString, err := m.String("text/css", strings.Join(styles, " "))
if err != nil {
return err
}
styleTag.Set("innerHTML", stylesString)
return nil
}
func (e *DomEnvironment) handleDOMEvent(listener events.EventListenerFunc, this js.Value, args []js.Value) interface{} {
if len(args) < 1 {
panic(fmt.Errorf("args should be at least 1 element, instead was: %#v", args))
}
jsEvent := args[0]
event := events.NewDOMEvent(jsEvent, this)
// acquire exclusive lock before we actually process event
e.Lock()
defer e.Unlock()
err := listener(event)
if err != nil {
// Return the error message
return err.Error()
}
return true
}
func (e *DomEnvironment) printTree(currentNode nodes.Node, layers int) {
prefix := ""
for i := 0; i < layers; i++ {
prefix += "|--"
}
internal.Debugf("%s Node %p %T (%v)\n", prefix, currentNode, currentNode, currentNode)
var children nodes.Children
switch typedNode := currentNode.(type) {
case *nodes.FuncNode:
children = []nodes.Node{typedNode.RenderResult}
case *nodes.FragmentNode:
children = typedNode.Children
case *nodes.HTMLNode:
children = typedNode.Children
default:
return
}
for _, child := range children {
if child == nil {
continue
}
e.printTree(child, layers+1)
}
}