| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- //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"
- "encoding/json"
- "fmt"
- "net/url"
- "os"
- "runtime"
- "strings"
- "time"
- "github.com/maxence-charriere/go-app/v9/pkg/errors"
- )
- 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"
- orientationChangeDelay = time.Millisecond * 500
- engineUpdateRate = 120
- resizeInterval = time.Millisecond * 250
- )
- var (
- rootPrefix string
- isInternalURL func(string) bool
- appUpdateAvailable bool
- lastURLVisited *url.URL
- resizeTimer *time.Timer
- )
- // 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 {
- 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)
- }()
- rootPrefix = Getenv("GOAPP_ROOT_PREFIX")
- isInternalURL = internalURLChecker()
- staticResourcesResolver := newClientStaticResourceResolver(Getenv("GOAPP_STATIC_RESOURCES_URL"))
- disp := engine{
- FrameRate: engineUpdateRate,
- LocalStorage: newJSStorage("localStorage"),
- SessionStorage: newJSStorage("sessionStorage"),
- StaticResourceResolver: staticResourcesResolver,
- ActionHandlers: actionHandlers,
- }
- disp.Page = browserPage{dispatcher: &disp}
- disp.Body = newClientBody(&disp)
- disp.init()
- defer disp.Close()
- window.setBody(disp.Body)
- onAchorClick := FuncOf(onAchorClick(&disp))
- defer onAchorClick.Release()
- Window().Set("onclick", onAchorClick)
- onPopState := FuncOf(onPopState(&disp))
- defer onPopState.Release()
- Window().Set("onpopstate", onPopState)
- goappNav := FuncOf(goappNav(&disp))
- defer goappNav.Release()
- Window().Set("goappNav", goappNav)
- onAppUpdate := FuncOf(onAppUpdate(&disp))
- defer onAppUpdate.Release()
- Window().Set("goappOnUpdate", onAppUpdate)
- onAppInstallChange := FuncOf(onAppInstallChange(&disp))
- defer onAppInstallChange.Release()
- Window().Set("goappOnAppInstallChange", onAppInstallChange)
- closeAppResize := Window().AddEventListener("resize", onResize)
- defer closeAppResize()
- closeAppOrientationChange := Window().AddEventListener("orientationchange", onAppOrientationChange)
- defer closeAppOrientationChange()
- performNavigate(&disp, Window().URL(), false)
- disp.start(context.Background())
- }
- func displayLoadError(err any) {
- loadingLabel := Window().
- Get("document").
- Call("getElementById", "app-wasm-loader-label")
- if !loadingLabel.Truthy() {
- return
- }
- loadingLabel.setInnerText(fmt.Sprint(err))
- }
- func newClientStaticResourceResolver(staticResourceURL string) func(string) string {
- return func(path string) string {
- if isRemoteLocation(path) || !isStaticResourcePath(path) {
- return path
- }
- var b strings.Builder
- b.WriteString(staticResourceURL)
- b.WriteByte('/')
- b.WriteString(strings.TrimPrefix(path, "/"))
- return b.String()
- }
- }
- func internalURLChecker() func(string) bool {
- var urls []string
- json.Unmarshal([]byte(Getenv("GOAPP_INTERNAL_URLS")), &urls)
- return func(url string) bool {
- for _, u := range urls {
- if strings.HasPrefix(url, u) {
- return true
- }
- }
- return false
- }
- }
- func newClientBody(d Dispatcher) *htmlBody {
- ctx, cancel := context.WithCancel(context.Background())
- body := &htmlBody{
- htmlElement: htmlElement{
- tag: "body",
- context: ctx,
- contextCancel: cancel,
- dispatcher: d,
- jsElement: Window().Get("document").Get("body"),
- },
- }
- body.setSelf(body)
- ctx, cancel = context.WithCancel(context.Background())
- content := &htmlDiv{
- htmlElement: htmlElement{
- tag: "div",
- context: ctx,
- contextCancel: cancel,
- dispatcher: d,
- jsElement: body.JSValue().firstElementChild(),
- },
- }
- content.setSelf(content)
- content.setParent(body)
- body.children = append(body.children, content)
- return body
- }
- func onAchorClick(d Dispatcher) func(Value, []Value) any {
- return func(this Value, args []Value) any {
- event := Event{Value: args[0]}
- elem := event.Get("target")
- for {
- switch elem.Get("tagName").String() {
- case "A":
- if meta := event.Get("metaKey"); meta.Truthy() && meta.Bool() {
- return nil
- }
- if ctrl := event.Get("ctrlKey"); ctrl.Truthy() && ctrl.Bool() {
- return nil
- }
- if download := elem.Call("getAttribute", "download"); !download.IsNull() {
- return nil
- }
- event.PreventDefault()
- if href := elem.Get("href"); href.Truthy() {
- navigate(d, elem.Get("href").String())
- }
- return nil
- case "BODY":
- return nil
- default:
- elem = elem.Get("parentElement")
- if !elem.Truthy() {
- return nil
- }
- }
- }
- }
- }
- func onPopState(d Dispatcher) func(this Value, args []Value) any {
- return func(this Value, args []Value) any {
- d.Dispatch(Dispatch{
- Mode: Update,
- Function: func(ctx Context) {
- navigateTo(d, Window().URL(), false)
- },
- })
- return nil
- }
- }
- func goappNav(d Dispatcher) func(this Value, args []Value) any {
- return func(this Value, args []Value) any {
- navigate(d, args[0].String())
- return nil
- }
- }
- func navigate(d Dispatcher, rawURL string) {
- u, err := url.Parse(rawURL)
- if err != nil {
- Log(errors.New("navigating to URL failed").
- Tag("url", rawURL).
- Wrap(err))
- return
- }
- navigateTo(d, u, true)
- }
- func navigateTo(d Dispatcher, u *url.URL, updateHistory bool) {
- if IsServer {
- return
- }
- if isExternalNavigation(u) {
- if rawurl := u.String(); isInternalURL(rawurl) || isMailTo(u) {
- Window().Get("location").Set("href", u.String())
- } else {
- Window().Call("open", rawurl)
- }
- return
- }
- luv := lastURLVisited
- if u.String() == luv.String() {
- return
- }
- if u.Path == luv.Path && u.Fragment != luv.Fragment {
- if updateHistory {
- Window().addHistory(u)
- } else {
- lastURLVisited = u
- }
- d, ok := d.(ClientDispatcher)
- if !ok {
- return
- }
- d.Nav(u)
- if isFragmentNavigation(u) {
- d.Dispatch(Dispatch{
- Mode: Defer,
- Function: func(ctx Context) {
- Window().ScrollToID(u.Fragment)
- },
- })
- }
- return
- }
- performNavigate(d, u, updateHistory)
- }
- func performNavigate(d Dispatcher, u *url.URL, updateHistory bool) {
- if IsServer {
- return
- }
- path := strings.TrimPrefix(u.Path, rootPrefix)
- if path == "" {
- path = "/"
- }
- compo, ok := routes.createComponent(path)
- if !ok {
- compo = ¬Found{}
- }
- disp, ok := d.(ClientDispatcher)
- if !ok {
- return
- }
- disp.Mount(compo)
- if updateHistory {
- Window().addHistory(u)
- } else {
- lastURLVisited = u
- }
- disp.Nav(u)
- if isFragmentNavigation(u) {
- d.Dispatch(Dispatch{
- Mode: Defer,
- Function: func(ctx Context) {
- Window().ScrollToID(u.Fragment)
- },
- })
- }
- }
- func isExternalNavigation(u *url.URL) bool {
- switch {
- case u.Host != "" && u.Host != Window().URL().Host,
- isMailTo(u):
- return true
- default:
- return false
- }
- }
- func isMailTo(u *url.URL) bool {
- return u.Scheme == "mailto"
- }
- func isFragmentNavigation(u *url.URL) bool {
- return u.Fragment != ""
- }
- func onAppUpdate(d ClientDispatcher) func(this Value, args []Value) any {
- return func(this Value, args []Value) any {
- d.Dispatch(Dispatch{
- Mode: Update,
- Function: func(ctx Context) {
- appUpdateAvailable = true
- d.AppUpdate()
- ctx.Defer(func(Context) {
- Log("app has been updated, reload to see changes")
- })
- },
- })
- return nil
- }
- }
- func onAppInstallChange(d ClientDispatcher) func(this Value, args []Value) any {
- return func(this Value, args []Value) any {
- d.AppInstallChange()
- return nil
- }
- }
- func onResize(ctx Context, e Event) {
- if resizeTimer != nil {
- resizeTimer.Stop()
- resizeTimer.Reset(resizeInterval)
- return
- }
- resizeTimer = time.AfterFunc(resizeInterval, func() {
- if d, ok := ctx.Dispatcher().(ClientDispatcher); ok {
- d.AppResize()
- }
- })
- }
- func onAppOrientationChange(ctx Context, e Event) {
- if d, ok := ctx.Dispatcher().(ClientDispatcher); ok {
- go func() {
- time.Sleep(orientationChangeDelay)
- d.AppResize()
- }()
- }
- }
|