-
-
Notifications
You must be signed in to change notification settings - Fork 355
/
app.go
177 lines (156 loc) · 4.78 KB
/
app.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
//go:generate go run gen/html.go
//go:generate go run gen/scripts.go
//go:generate go fmt
// Package app is a package to build progressive web apps (PWA) with Go
// programming language and WebAssembly.
// It uses a declarative syntax that allows creating and dealing with HTML
// elements only by using Go, and without writing any HTML markup.
// The package also provides an http.Handler ready to serve all the required
// resources to run Go-based progressive web apps.
package app
import (
"context"
"fmt"
"os"
"reflect"
"runtime"
)
const (
// IsClient reports whether the code is running as a client in the
// WebAssembly binary (app.wasm).
IsClient = runtime.GOARCH == "wasm" && runtime.GOOS == "js"
// IsServer reports whether the code is running on a server for
// pre-rendering purposes.
IsServer = runtime.GOARCH != "wasm" || runtime.GOOS != "js"
)
var (
routes = makeRouter()
window = newBrowserWindow()
)
// Getenv retrieves the value of the environment variable named by the key. It
// returns the value, which will be empty if the variable is not present.
func Getenv(k string) string {
if IsServer || !Window().Get("goappGetenv").Truthy() {
return os.Getenv(k)
}
env := Window().Call("goappGetenv", k)
if !env.Truthy() {
return ""
}
return env.String()
}
// KeepBodyClean prevents third-party Javascript libraries to add nodes to the
// body element.
func KeepBodyClean() (close func()) {
if IsServer {
return func() {}
}
release := Window().Call("goappKeepBodyClean")
return func() {
release.Invoke()
}
}
// Window returns the JavaScript "window" object.
func Window() BrowserWindow {
return window
}
// RunWhenOnBrowser starts the app, displaying the component associated with the
// current URL path.
//
// This call is skipped when the program is not run on a web browser. This
// allows writing client and server-side code without separation or
// pre-compilation flags.
//
// Eg:
//
// func main() {
// // Define app routes.
// app.Route("/", myComponent{})
// app.Route("/other-page", myOtherComponent{})
//
// // Run the application when on a web browser (only executed on client side).
// app.RunWhenOnBrowser()
//
// // Launch the server that serves the app (only executed on server side):
// http.Handle("/", &app.Handler{Name: "My app"})
// http.ListenAndServe(":8080", nil)
// }
func RunWhenOnBrowser() {
if IsServer {
return
}
defer func() {
err := recover()
displayLoadError(err)
panic(err)
}()
resolveURL := clientResourceResolver(Getenv("GOAPP_STATIC_RESOURCES_URL"))
originPage := makeRequestPage(Window().URL(), resolveURL)
engine := newEngine(context.Background(),
&routes,
resolveURL,
&originPage,
actionHandlers,
)
engine.Navigate(window.URL(), false)
engine.Start(120)
}
func displayLoadError(err any) {
loadingLabel := Window().
Get("document").
Call("getElementById", "app-wasm-loader-label")
if !loadingLabel.Truthy() {
return
}
loadingLabel.setInnerText(fmt.Sprint(err))
}
// Route associates a given path with a function that generates a new Composer
// component. When a user navigates to the specified path, the function
// newComponent is invoked to create and mount the associated component.
//
// Example:
//
// Route("/home", func() Composer {
// return NewHomeComponent()
// })
func Route(path string, newComponent func() Composer) {
routes.route(path, newComponent)
}
// RouteWithRegexp associates a URL path pattern with a function that generates
// a new Composer component. When a user navigates to a URL path that matches
// the given regular expression pattern, the function newComponent is invoked to
// create and mount the associated component.
//
// Example:
//
// RouteWithRegexp("^/users/[0-9]+$", func() Composer {
// return NewUserComponent()
// })
func RouteWithRegexp(pattern string, newComponent func() Composer) {
routes.routeWithRegexp(pattern, newComponent)
}
// NewZeroComponentFactory returns a function that, when invoked, creates and
// returns a new instance of the same type as the provided component. The new
// instance is initialized with zero values for all its fields.
//
// The function uses reflection to determine the type of the provided Composer
// and to create new instances of that type.
//
// Example:
//
// componentFunc := NewZeroComponentFactory(MyComponent{})
// newComponent := componentFunc()
func NewZeroComponentFactory(c Composer) func() Composer {
componentType := reflect.TypeOf(c)
return func() Composer {
return reflect.New(componentType.Elem()).Interface().(Composer)
}
}
// TryUpdate attempts to update the application in the browser. On success, it
// notifies components implementing the AppUpdater interface that an update is
// ready.
func TryUpdate() {
if tryUpdate := Window().Get("goappTryUpdate"); IsClient && tryUpdate.Truthy() {
tryUpdate.Invoke()
}
}