Skip to content

Refactor Notes Sep 2019

Brad Peabody edited this page Sep 23, 2019 · 34 revisions

This page is temporary documentation following this big refactor. It will take me at least a week or two to port vugu.org over to the new code base (if I can use the opportunity to hack together a prototype URL router I will) and test it and fix what comes up, and update and extend all of the documentation, upgrade the playground, etc. But this document is a quick summary in the meanwhile.

I've tagged the work prior to these changes as v0.1.0 and what follows is on master and can be referenced using the appropriate require line in go.mod (see below). I will tag this new refactored code as v0.2.0 once the vugu.org docs are updated and a bit more testing has been done.

Working Example

Here's a new example that includes use of a component. Drop these files into a directory and then do the same go run devserver.go:

go.mod

module example.org/someone/testapp

// FYI: Go build tooling will automatically replace "master" with the latest revision from that branch
// (but it will also cache it :/ )
require github.com/vugu/vugu master

IMPORTANT NOTE: If go build is caching "master" as an older version, simply go to https://github.com/vugu/vugu and copy and paste in the latest revision hash (8439884690d279f1cef31abf7d793af76f807322 at the time of this writing), to ensure you get the most recent version.

root.vugu

<html>
    <head>
        <title>Test page</title>
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    </head>
    <body>

        <div class="test-div" id="test_div_id">
            <div>Let's see how this goes:</div>
            <ul>
                <main:DemoLine vg-for="i := 0; i < c.ItemCount; i++" vg-key="i" :Num="i"></main:DemoLine>
            </ul>
            <button @click="c.OnAdd()">Add</button>
        </div>

    </body>
</html>

<style>
#test_div_id { background: #ddd; }
</style>

<script type="application/x-go">

type Root struct {
    ItemCount int `vugu:"data"`
}

func (c *Root) BeforeBuild() {
    if c.ItemCount == 0 {
        c.ItemCount = 5
    }
}

func (c *Root) OnAdd() {
    c.ItemCount++
}

</script>

demo-line.vugu

<li class="demo-line">
    <strong vg-html="c.Num"></strong> a line is here
</li>

<style>
.demo-line { color: green; }
</style>

<script type="application/x-go">
type DemoLine struct {
    Num int `vugu:"data"`
}
</script>

devserver.go

// +build ignore

package main

import (
	"log"
	"net/http"
	"os"

	"github.com/vugu/vugu/simplehttp"
)

func main() {
	wd, _ := os.Getwd()
	l := "127.0.0.1:7944"
	log.Printf("Starting HTTP Server at %q", l)
	h := simplehttp.New(wd, true)
	log.Fatal(http.ListenAndServe(l, h))
}

Updates to main_wasm.go

When updating existing Vugu code, you will need to delete your main_wasm.go and let it regenerate (or make the needed modifications by hand) in order for to work with the new code base. (If you've hacked on main_wasm.go manually, I suggest you rename it, let Vugu create a new one for you, and then manually merge over your changes.) The updated one isn't terribly different but there are various different function args, the render loop is now in two steps - build and render, and a few other things.

Component Changes

The method receiver for the component you are "currently in" is c.

There is no more Comp and CompData, there's just one struct per component now, so root.go corresponds to type Root struct and that's it.

Embedding other components is done and references created at code-generation-time now with the following syntax:

<pkg:Comp 
    StringField="some string"
    :DynamicField='/* some go expression */ 123.0'
    stringAttrMapValue="some string"
    :dynamicAttrMapValue='/* some go expression */ 123.0'
    ></pkg:Comp>
  • pkg is the package name - it must correspond to an import statement or be the same as the current package.
  • Comp is the name of the component struct. No mangling is performed on it (or any of the fields mentioned below).
  • StringField is assigned as a regular Go struct field using the string "some string"
  • DynamicField is assigned as a regular Go struct field but /* some go expression */ 123.0 is emitted directly into the code generated file and thus evaluated as Go code.
  • stringAttrMapValue (lower-case first letter) is assigned as a key of to a field that you must declare as AttrMap map[string]interface{} - this allows you to accept arbitrary values as component input, or not
  • dynamicAttrMapValue (lower-case first letter) works like stringAttrMapValue but is evaluated as Go code instead of a static string.

BeforeBuild Callback

For a component, after any field assignments are done and the actual virtual DOM building occurs, the BeforeBuild() method is called on a component if it is exists.

This is essentially for use in implementing "computed fields" (to use Vue's terminology), and doing any precomputation prior to building.

Modification Tracking

In order to implement caching and other related concerns, we needed an exact definition of what "modified" means for a component, and we need it in a way that is generally useful to developers building an app with Vugu. This problem was what lead to an entire subsystem in Vue entailing an event system which tracks properties that have changed. Having prototyped an event-based modification tracking system in Go and discarded as much too unwieldy, we instead now have the following:

Modification tracking is done with ModTracker, it is used internally, you won't need to instantiate it yourself.

ModTracker determines "changed" from one render pass to the next based on these rules:

  • Component (and other structs) fields are traversed by calling ModCheckAll on each with the tag vugu:"data". (Any struct fields used as component input should be tagged with vugu:"data", or you must implement the ModChecker interface. "Computed fields", written by BeforeBuild() should not be tagged like this)
  • Then modification checking continues to traverse based on these rules:
  • For values implementing the ModChecker interface, the ModCheck method will be called.
  • All values passed should be pointers to the types described below.
  • Single-value primitive types are supported.
  • Arrays and slices of supported types are supported, their length is compared as well as a pointer to each member.
  • As a special case []byte is treated like a string.
  • Maps are not supported at this time.
  • Other weird and wonderful things like channels and funcs are not supported.
  • Passing an unsupported type will result in a panic.

The point of all this is that ModTracker will scan your component and follow the graph of objects to determine if it is "modified" before rendering. The ModChecker interface (and if you're looking at that, have a look at ModCounter as a simple implementation) allows developers to customize how modification track is done, for when the graph of objects gets too large and things get slow.

There is a whole lot more to say about this and the design, etc. I hope to have a chance to write out a better explanation when I update the Vugu docs.

JS Property Assignment Syntax

A syntax has been added which assigns a JavaScript property to an element (this is different from an attribute!).

Example:

<input type="checkbox" .checked="false"/>

Values are evaluated as a Go expression (false in this case) and then run through json.Marshal and the resulting value is assigned in JS.

This was needed because for forms and possibly other elements they have JS properties which just don't have a corresponding attribute (this is an annoying inconsistency, but is also reality).

Full HTML Support

Your root.vugu can now start with an <html> tag.

This is an important step toward being able to produce full sites with Vugu and not have 99% of your application being in one format and when you want to change the title tag invent some strange solution for that. This way Vugu app page layout can be 100% .vugu files.

Certain features (such as title tag and other head tag elements) are not yet implemented, but should be easy to do with the structure now in place.

syscall/js Wrapper

This is primarily for easing the development of server-side page rendering along side client-side Vugu page rendering. The package github.com/vugu/vugu/js is a lightweight wrapper around Go 1.13's syscall/js package, but with the distinct difference than it will compile on non-wasm platforms and all operations will return appropriate zero or error values (or in some cases panic). The point is that you don't have to separate out your files with a build tag every time you need to declare a variable of type js.Value or something like that.

All code generated Vugu code uses github.com/vugu/vugu/js instead of syscall/js and you are encouraged to do this as well. It's very lightweight and there are no known bugs.

New and Improved DOM Event Handling

You can now put statements directly in an event handler, e.g.:

<div @click="c.Toggled=!c.Toggled"> ...

DOMEvent now has an "event summary", which is basically a blob of JSON collected in JS when the event fired and marshalled over to Go so you can use it and get, for example, a form value without having to reach back into JS and ask for it. The EventSummary itself is a map[string]interface{} populated by json.Unmarshal, but the Prop method is useful for extracting data from it, i.e. event.Prop("target", "value") gives you the same value as event.target.value would in JS (and won't panic if it's not there). Variations for different types exist also, e.g. event.PropString("target", "value") returns a string.

Example:

<div><form>
<input type="text" @changed="c.HandleChanged(event)"> ... 
</form></div>


<script type="application/x-go">
import "log"

func (c *Root) HandleChanged(event *vugu.DOMEvent) {
    log.Print(event.PropString("target", "value"))
}

</script>

Go 1.13

This new code requires Go 1.13, there will be no support for prior versions at this time.

Import Deduplication

Vugu will now automatically remove duplicate import lines, all fancy-like. (This happens when you import a package in your <script> that the code generator has already imported.)

Render Pipeline

This is mostly an internal thing, but what was just a "render" before is now two steps - a "build" and a "render".

  • Build creates the virtual DOM for each component.
  • Render synchronizes this DOM with the browser.

This allows for better code re-use and organization and also makes it clear where optimization and caching can be performed.

When server-side rendering is available, the Build step will remain the same and the Render step will be implemented by a different struct which simply outputs static HTML (with things like DOM event handlers elided).

Next Steps

Slots and component events have not yet been implemented but the pattern is worked out (see Component Related Features Design), and I expect these to be implemented in the coming weeks as this new code base stabilizes.

You can’t perform that action at this time.