Vugu takes a number of steps to make the development process easy to rapidly get started and prototype user interfaces. That said, it's important to understand the overall preogram structure and how a Vugu program works (at least by default), so you can customize it to your needs when building a sophisticated application. The basics are fairly simple.

When you use the HTTP handler in development mode (as shown on the Getting Started page), upon page load Vugu will convert your .vugu files into .go and also by default attempt to generate a main_wasm.go if it does not exist. (You can edit main_wasm.go as needed. It is only a template to get your started. Vugu will not overwrite it.)

WebAssembly main()

Like any Go program, it starts with main(). The build constraint at the top (see Dual Build below) indicates this is the WebAssembly entry point:

  1. // +build wasm
  2.  
  3. package main
  4.  
  5. import (
  6. // ...
  7. "github.com/vugu/vugu"
  8. )
  9.  
  10. func main() {

Root Component

To render a page to HTML, you need to have "root" component. This is the top level component that houses everything else. By default this component lives in root.vugu, gets code generated to root.go, which has a Root struct (the component type) and RootData struct (the component's instance data). vugu.New accepts a ComponentType (Root) and a vugu.Props (nil in this case) and returns an instance that we can use. Like so:

  1. rootInst, err := vugu.New(&Root{}, nil)
  2. if err != nil {
  3. log.Fatal(err)
  4. }

JSEnv

Once we have a root component instance, we need an environment. There are two environments currently implemented: JSEnv for use in WebAssembly applications, and StaticHTMLEnv which can be used for server-side rendering and tests. JSEnv is what performs the syncing of the virtual DOM from by our root component and any nested components to the browser DOM. But before that happens, we need to create an instance:

  1. env := vugu.NewJSEnv("#root_mount_parent",
  2. rootInst,
  3. vugu.RegisteredComponentTypes())
  • The first argument is the location in the page (CSS selector, readable by querySelector) of where the component is "mounted". By default #root_mount_parent corresponds to a <div> immediately inside the <body> tag.
  • The second argument is the component instance we created above.
  • And the last argument is a map of which component types are available under which names. The function vugu.RegisteredComponentTypes will retreive all of the components that have registered themselves with vugu.RegisterComponentType (which is the case for all components by default). This makes it so you can simply import _ "path/to/other/components" and they will be available automatically. Components defined directly in your main package are also always available by default.

If you need more control over which component types are available during rendering and the names they use, or where your application is output on the page, customizing what you pass to NewJSEnv is the way to do it.

Render Loop

The render loop is where the magic happens. Your components' virtual DOM output (see ComponentType.BuildVDOM) is generated and synchronized with the browser's DOM to give you a matching HTML page. This involves various optimizations including keeping hashes of various pieces of information during rendering so it can cache the result and reduce both compution and the number of calls into the browser to synchronize DOM.

  1. for ok := true; ok; ok = env.EventWait() {
  2. err = env.Render()
  3. if err != nil {
  4. panic(err)
  5. }
  6. }

This will call env.Render() immediately the first time and then wait for env.EventWait() to return and render again.

Important Note

When DOM Events are handled, a (write) lock is acquired against the environment automatically and then released when your event handler returns. When things that would block (like fetching data from the server over HTTP) need to be done, this must be run in a goroutine which uses event.EventEnv() to acquire their own lock before modifying any component data, to ensure they don't interfere with the render loop or other code. Locking should only be done during data modification and then unlocked immediately afterward. Do not put a Lock() before http.Get() or other such blocking) calls. Instead Lock() after you have your data and before updating the state of your component. EventEnv.UnlockRender() will cause the env.EventWait() call above to return and update the page. (Whereas EventEnv.UnlockOnly() will release the lock but not cause the page update. This is useful if you need to do several updates to data at different times but only care to refresh the page after they are all done.) See Code for a correct example.

EventWait will return false if it detects something wrong with the environment and the program should exit. This should release any resources and be a clean exit from the program when the page goes away.

The Dual-Build Approach

The discussion above is only about the WebAssembly side of your application.

Using Go's build constraints it is easy to output two different executables from your same package directory. The common case is that you want a client-side build that compiles to WebAssembly (as discussed above), as well as a server-side executable to act as a web server. These each need different main() and likely other functions, but your components and other functionality should be available both in your WebAssembly output and in your server program.

This is why the main() function for your client-side application lives in main_wasm.go. The "_wasm" part indicates that the file should be included during a WebAssembly build. You can and should include a server-side main() in another file and use // +build !wasm at the top as the inverse build constraint. See Building and Distribution for more info.