This repository has been archived by the owner on Jul 11, 2019. It is now read-only.
/
wrap.go
166 lines (143 loc) · 5.35 KB
/
wrap.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
package react
import (
"fmt"
"github.com/gopherjs/gopherjs/js"
"github.com/leeola/go-react/reactjs"
)
// wrapComponent creates a React class for the given Component by
// creating a plain js.Object and mapping functions to it, and back.
//
// For example, the React `render()` method is assigned to the object.
// When React calls this method, the given Component's Render() method is
// called. The result of Component.Render() is recursively run through
// Wrap() as well, with the final result being a series of objects given
// to React that map to their respective components.
//
// This means that when a component calls the embedded *React methods,
// such as React.SetState() end up calling the functions in this closure
// which then call the normal React `this.setState()` methods.
//
// A lot of wiring, but the goal is that complexity in a Component is
// isolated from the complexity given to React. So all of the tomfoolery
// that React does to method Binding's are not actually done to the
// methods of your component.
//
// TL;DR: The overhead caused by Wrap() is to avoid React tomfoolery
// multiplying by GopherJS tomfoolery. Resulting in a (in theory)
// net tomfoolery reduction. Science.
//
// TODO: Create the *React pipeline to communicate from the Component
// to the jsObject wrapper.
//
// TODO: Write errors to console.error
//
// TODO: Drop error return. Don't think it's possible to have an error
// outside of the callbacks.
//
// TODO: Handle the following React warning:
//
// Warning: Each child in an array or iterator should have a
// unique "key" prop. Check the React.render call using <div>.
// See https://fb.me/react-warning-keys for more information.
//
// Though, the responsibility of handling this may be better off on the
// Component.
func wrapComponent(c Component) (jsComp *js.Object, err error) {
fmt.Printf("wrapComponent(<%s>)\n", c.Tag())
jsObject := js.Global.Get("Object").New()
// This is a reference to the final, valid React object instance.
//
// React does a lot of binding/this tomfoolery, and it heavily
// screws with GopherJS. As a result, custom functions won't have
// valid `this` objects.
//
// React API functions can get a valid `this`, such as `render` and
// `setInitialState`, but custom functions fail.
//
// Commented out, as it's likely being removed soon (no need for
// the reference, Components have SetThis()
//var this *js.Object
// TODO: Find a way to support sane error handling. It's difficult
// because the render() method is a callback from JS, and the return
// value is consumed by React.js itself. Furthermore, even if we could
// return an error value, we can't get access to it.
render := func() *js.Object {
fmt.Printf("<%s>.render\n", c.Tag())
rendComp := c.Render()
var jsClass *js.Object
var content string
// If a component is Rendered, wrap it and return.
if rendComp != nil {
// If this child has Content (String), return it directly.
content = rendComp.Content()
if content != "" {
return reactjs.CreateElement(c.Tag(), c.Props(), content)
}
jsClass, _ = wrapComponent(rendComp)
return reactjs.CreateElement(jsClass)
}
// The comp did not render anything, so try to create a Tag with
// it's children.
//
// If it has no tag, we can't do that. Error out.
if c.Tag() == "" {
fmt.Println("Error: Component has no Children and no Tag")
return nil
}
children := c.Children()
childrenCount := len(children)
// If this comp has no children, create an empty jsComp (eg: <foo />)
if childrenCount == 0 {
return reactjs.CreateElement(c.Tag(), c.Props())
}
// The == 1 check is done to avoid creating the slice, looping,
// and (most importantly) avoid returning an array. By doing it
// here, we can save a bit of overhead.. in theory.
//
// Premature optimization without benchmarks? heh
var childComp Component
if childrenCount == 1 {
childComp = children[0]
// If this child has Content (String), return it directly.
content = childComp.Content()
if content != "" {
return reactjs.CreateElement(c.Tag(), c.Props(), content)
}
jsClass, _ = wrapComponent(childComp)
return reactjs.CreateElement(c.Tag(), c.Props(),
reactjs.CreateElement(jsClass))
}
// A slice of this Element's Childen. Note that in the event of
// a Content child, string is added to this slice instead of a
// jsObject.
jsChildren := make([]interface{}, childrenCount)
var i int
for i, childComp = range children {
// If this child has Content (String), push it onto Children
// directly.
content = childComp.Content()
if content != "" {
jsChildren[i] = content
continue
}
// Wrap the component, returning it's class
jsClass, _ = wrapComponent(childComp)
// And create an element from the class, adding it to the slice.
jsChildren[i] = reactjs.CreateElement(jsClass)
}
return reactjs.CreateElement(c.Tag(), c.Props(), jsChildren)
}
jsObject.Set("render", render)
jsObject.Set("componentDidMount", c.ComponentDidMount)
jsObject.Set("componentWillUnmount", c.ComponentWillUnmount)
// Temporary implementation for early testing.
jsObject.Set("getInitialState", js.MakeFunc(
func(reactThis *js.Object, _ []*js.Object) interface{} {
if err := c.SetThis(reactThis); err != nil {
fmt.Println("Error: ", err.Error())
}
return nil
},
))
return reactjs.CreateClass(jsObject), nil
}