Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements needed for using templ inside a Wasm environment #1001

Open
shaban opened this issue Nov 19, 2024 · 7 comments
Open

Enhancements needed for using templ inside a Wasm environment #1001

shaban opened this issue Nov 19, 2024 · 7 comments

Comments

@shaban
Copy link

shaban commented Nov 19, 2024

Hello, a while back I promised to check back with findings on trying out templ inside a Wasm environment. So here are my findings:

Preface

Use Cases

Most of us coming from Go have the notion of Go as a backend language but when using Wasm things are totally different.
We now have a backend and platform agnostic frontend language and as such we can in theory use Go as a type safe compiled language in all browser environments with a few considerations. So the most viable use cases are as a web frontend for a fully fledged computer application, since in a localhost environment bundle sizes of 10MB+ do not matter. Also if your Go Wasm Application brings something powerful to the table you might get away with a load screen of a few seconds.

Examples

So I mentioned Wasm being agnostic of the backend means that you can use it inside a QT web view, Wails, possibly even Tauri or Electron if you wanted since with the net/http package and encoding/json you can communicate with all backends amicably. This opens up a wide array of possibilities, since the only real integration with the backend that your Wasm frontend needs is the same definition of the data model.

Workflow

While experimenting I used wails as a backend but found that while developing your frontend this is not the best approach since you don't want the extra overhead on the turnaround times that wails introduces, but simply using vite or any other capable development server is preferable, since the only thing we need is something that will give us access to our assets and data.

Problems

  • Event Handling
  • Build tags

Event Handling

In wasm you basically have two choices for event handling:

  • You can wrap a go function call so it can be called from Javascript
js.Global().Set("myGoFunction", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
	myGoFunction(parameters...)
	return nil
}))
  • or you can call javascript to handle events there.

templ

Assumptions

in templ it is assumed that the html is created by the server which means that all the scripts that we create and special constructs like the script directive in templ templates are correctly evaluated and executed.

Reality

Component wide the html gets delivered in a string builder and then set to the components html element with a snippet like this:

document := js.Global().Get("document")
element := document.Call("getElementById", "result")
component := app("John")
sb := new(strings.Builder)
component.Render(context.Background(), sb)
element.Set("innerHTML", sb.String())

This way the containing script will not be evaluated which renders most JS examples in templ.guide useless.

So the only way to make it work is by applying the script separately like so:

htmlString := stringBuilder.String()

// 1. Get the container element (assuming it has id "myContainer")
container := js.Global().Get("document").Call("getElementById", "myContainer")

// 2. Set the main HTML structure using innerHTML
container.Set("innerHTML", htmlString)

// 3. Create and append <script> elements
for _, script := range scripts {
  scriptElem := js.Global().Get("document").Call("createElement", "script")
  scriptElem.Set("textContent", script) // The script content
  container.Call("appendChild", scriptElem)
}

Issue

I have not found a way to construct a javascript call dynamically like

<button onclick="myClick(this,{name})">Click me</button>

The quotation marks keep the variable from being evaluated and single quotes don't work either. I could be wrong but I don't see any way how to dynamically create parameters to give context to an event besides looking for custom attributes in the event target element.

If that is by design and intended forgive my ignorance but since JS conversions are costly I thought a string based approach would be much more practical especially when you consider that the receiver might be a fantastic javascript library that does not know anything about your custom data attributes.

Build Tags

To be able to use syscall/js package one has to specify build tags like so.

//go:build js && wasm

In a wasm environment it is totally acceptable and even a crucial feature to use other go files in the same package space as the generated template files to host utility functions and generally not pollute the template with lengthy code.

But this is only possible when they both share the same build tags also imports won't work if they don't share compatible build tags.

So when generating the go file build tags must either must be respected or if that is a problem at least there should be a command line flag like

templ generate -magiccomment "//go:build js && wasm"

that prepends the build tag separated with a newline from the "Do not edit" comment.

tldr;

Go, traditionally a backend language, takes on a new role in the WASM environment. It becomes a platform-agnostic frontend language capable of powering rich web applications. This shift opens possibilities for using Go in various contexts, including:

  • Web frontends: For complex applications where performance and type safety are crucial.
  • Desktop applications: Integration with frameworks like QT Webview, Wails, Tauri, and Electron.
  • Hybrid environments: Combining Go's strengths with existing JavaScript ecosystems.

Development Workflow

While frameworks like Wails offer integrated development, a simpler setup with tools like vite can be more efficient for frontend development. This approach focuses on rapid iteration and asset management.

Dynamic JavaScript Calls

Constructing dynamic JavaScript calls with templ is problematic due to quotation mark escaping issues. This limits the ability to pass context to event handlers, potentially hindering integration with JavaScript libraries.

Finding a way to manage javascript in a centralized way would be beneficial for everyone using templ since that would solve innerHTML issues and improve JS Code organization. I will not propose a solution since this is out of scope of this writeup.

Build Tags

Using the syscall/js package requires specific build tags: //go:build js && wasm

This poses challenges when:

  • Sharing utility functions: Go files containing utility functions used by templates need identical build tags.
  • Managing imports: Imports require compatible build tags. An added benefit is future proofing for tags not yet introduced by the Go Team.

Proposed Solution

Introduce a command-line flag to templ for specifying build tags in generated files:

templ generate -magiccomment "//go:build js && wasm"

This would prepend the specified build tag to the generated Go files, ensuring compatibility and code organization.

Conclusion

Addressing these issues will enable templ's usability in WASM environments, allow developers to leverage its strengths for building complex and interactive applications, with the added benefit of not having to deal with javascript focused build steps (node modules, dependency issues)

P.S. Apologies you were right about the watch flag causing the problems in my last issue :)

@a-h
Copy link
Owner

a-h commented Nov 20, 2024

Thanks for compiling your research into such a well written report. I'll take a read through, but want to give it the proper thinking time rather than rushing it, so won't be in the next couple of days.

@gedw99
Copy link

gedw99 commented Jan 2, 2025

Is the intent here related to building a Templ Playground Editor with Monaco and LSP off the files on the server .?

That can be done by passing the files up from the server and then back down to the server , with the templ generator in watch mode .

The LSP can be sent over SSS , like Datastar . So can the file tree and even file change diffs . Datastar is kind of perfect for this .

this is really not doing any compilation in the browser but just allowing a collaborative editor , and a decent playground .

—-

Also , the Monaco Editor is designed for the LSP to be pushed via Websockets , which is not so aligned with Datastar .

So a little server side converter is the best way perhaps . Not sure as I have not delved into the templ LSP enough .

am happy to dig into it, but need some clarification of the above things

@a-h
Copy link
Owner

a-h commented Jan 3, 2025

I'm struggling to understand this, probably because I've never tried to run Go in Web Assembly.

I think that some examples would be useful!

FWIW, there's a WASM based templ playground at https://play.templ.guide/

You can see the code at https://github.com/templ-go/playground

Not sure if that helps...

@FrancescoLuzzi
Copy link

FrancescoLuzzi commented Jan 8, 2025

I've also stumbled upon this, i'll make another example:

templ MyExample(user string, show bool) {
    <script>
        function writeUserName(el,name,show){
            if (!show){
                return;
            }
            el.innerHTML=name
        }
    </script>
    <div
      onClick="writeUserName(this,'test',true)"
      // I'd like to inject the values directly
     // onClick="writeUserName(this,{user},{show})"
      style="width: 200px; height: 200px; background-color: #BEBEBE; color: black;"
    >
      placeholder
    </div>
}

A workaround could be:

templ MyExample(user string, show bool) {
    <script>
      function proxy(el) {
        const user = el.getAttribute('arg-user');
        const show = el.getAttribute('arg-show') !== null;
        writeUserName(el, user, show);
      }
      function writeUserName(el, name, show) {
        if (!show) {
          return;
        }
        el.innerHTML = name;
      }
    </script>
    <div
      onClick="proxy(this)"
      arg-user={ user }
      arg-show?={ show }
      style="
        width: 200px;
        height: 200px;
        background-color: #bebebe;
        color: black;
      "
    >
      placeholder
    </div>
}

To me this is not very intuitive and pretty distant from the general approach: "open a pair of curly brackets and dump some text into some html"
(this is my interpretation and an over simplification of the "templ" philosophy)

Anyways, thanks for your work and please correct me if i'm wrong 😃

@shaban
Copy link
Author

shaban commented Jan 8, 2025

Thanks for the input I will try out the suggestions.
At the moment I am busy though finalizing my marriage.

@gedw99
Copy link

gedw99 commented Jan 8, 2025

Hey @shaban

I just want to frame this issue, because the context was not explained. I think some of the confusion is related to the context.

You want to run templ on your laptop ( or ci ) so that you can import the output of templ into a golang package that is then compiled ( again on your laptop ) to WASM and then run that WASM in a web browser ?

To jump ahead , assuming this context is correct, it seems that it would require that the templ generator is told that it’s compiling for web WASM, and constructs the js sys calls correctly.

https://github.com/inkeliz/go_inkwasm Generates these js sys calls is such a way as to give a performance increase. It’s relevant to this use case , not just for performance , but also because a generator of the js sys calls. Thus it can be used as a reference for how templ could generate these js sys calls.

looking forward to see where this goes . I think optimising the developer experience for using templ is a web WASM environment is a worthy use case these days. I my self use a lot of golang inside Service Workers and Web workers , and the easier it can be to allow templ to be used in these environments the better.

To do this a proper test harness would be needed because there are varying ways a web WASM can be setup.

there are other runtime contexts , like Cloudflare workers too in which templ can be used . So the way the inputs into the developers templ code is constructed varies , and this the generator would vary. See: https://github.com/syumai/workers/tree/main/_examples/hello where the querystring is the context of the inputs that would be sent into temp.

This all begs the question of , should templ generate this shim code to gather the inputs into the templ code . I think it’s worth exploring and reflecting on it. My own WASM code when compiled with tinygo is half the size ( in general) then golang compiled WASM, and is often about 1 mb for small projects . It’s usable for some developers projects imho. You can also break the project down into a WASM per web page ( which I do actually , and what is done for golang that will run in a browser or cloudflare workers ) and the service worker shim can async load them when needed. Once loaded he next call is super quick of course . I mention all this to help layout the wider context for how this can work in reality with decent performance for the users.

@gedw99
Copy link

gedw99 commented Jan 8, 2025

I also want to draw your attention to this article.

https://blog.boot.dev/golang/running-go-in-the-browser-wasm-web-workers/

It shows how to load the WASM into browser based web workers so that your web WASM app is multi threaded .

Any attempt to use templ should probably take this course.

It’s breaking the web WASM into smaller chunks and so lowers the initial load times for users . It’s also speeding up the apps user experience by multi threading the app.

It’s very similar to the approach used with Cloudflare workers.

I use this approach for golang WASM on the Server, Cloudflare workers and the Browser, so that I have reasonable load times .

The runtime context, impacts how you approach the compile time , in that each has a different shim of how you pass inputs into the golang code later.

So, it again begs the question of if a generator is needed that runs before templ, so that the shim can be constructed for the context ? It’s what I do for most golang that will be run in different WASM runtime contexts. The code generated by this shim is then used to pass the variables into templ.
It’s a delicate dance - I know :)

here is a golang generator that does exactly that , that is designed for go templates , that does web workers and share web workers :

https://github.com/magodo/go-wasmww/tree/main

What I like about it is that it’s using stdio as the “ transport “, which makes it highly flexible , in that the actual worker code can be used in the browser, or on a server or Cloudflare workers, etc. it’s essentially using a conduit that’s isomorphic / agnostic . I write all my golang that will be used in any WASM context to use stdio. It also makes it very easy to write tests and run the code outside a WASM context . I generate a golang based cli, that can call the WASM just for this .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants