Components are individual files which are used to organize your user interface code. Each component lives in a .vugu file. Each .vugu file is processed to produce a .go file. Like all Go code, each directory is a package and you may add additional .go files to it and use them as part of your component. Vugu does some code generation, but otherwise does not interfere with the regular Go build process at all. Code-generated files end with _vgen.go.
By default, the component named root (and thus living in root.vugu) is the top
level component and is rendered just inside the <body>
tag on your page.
In this case, there is only one instance of your root component, which is created in your main()
function.
(If you've followed the
Getting Started
instructions you can find this code in main_wasm.go)
Things get interesting when we introduce the idea of multiple components into an application. Each component goes in its own .vugu file.
It's important to remember that under the hood, components are just Go structs.
All component references are resolved at compile-time. There is intentionally
as little magic involved as possible. In fact, components are really just implementations
of the Builder
interface and simply provide a Build
method with HTML nodes they output.
Static Component References
One component can include a tag that indicates the name of a component struct to use.. When this happens, it says that an instance of this other component should be created. Let's look at an example:
<!-- root.vugu --> <div class="root"> <ul> <main:MyLine FileName="example.txt" :LineNumber="rand.Int63n(100)" ></main:MyLine> </ul> </div> <script type="application/x-go"> import "math/rand" </script>
<!-- my-line.vugu --> <li class="my-line"> <strong vg-content='c.FileName'></strong>:<span vg-content='c.LineNumber'></span> </li> <script type="application/x-go"> type MyLine struct { FileName string `vugu:"data"` LineNumber int `vugu:"data"` } </script>
In this case the <main:MyLine>
tag gets replaced with the <li>
tag and its contents as
rendered by the MyLine component. The reference is emitted directly into the code generated file and resolved at Go compile time.
main
is the name of the package. The name of the current package ("main" or otherwise) is removed
in the generated code, but otherwise all package names are regular Go references. Import statements are required
in order to access components in other packages.
As shown in the example, HTML attributes starting with a capital letter correspond to struct field assignments.
Attributes with a colon (:
)
will be evaluated as Go code and then the result used as-is (not converted in any way).
This allows you to pass arbitrarily complex data, or pointers, etc. into components when needed.
Components can be instantiated as many times as needed. Each one causes a new instance to be created. Like so:
<!-- root.vugu --> <div class="root"> <ul> <main:MyLine vg-for='i := 0; i < 10; i++' FileName="example.txt" :LineNumber="i" ></main:MyLine> </ul> </div>
Components used in this way also support additional features including dynamic properties, as follows:
<pkg:Comp StringField="some string" :DynamicField='/* some go expression */ 123.0' stringAttrMapValue="some string" :dynamicAttrMapValue='/* some go expression */ 123.0' @Something='/* ... */' ></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. It should be an exported type (start with a capital letter).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 asAttrMap vugu.AttrMap
(an alias formap[string]interface{}
) - this allows you to accept arbitrary values as component input, or not.dynamicAttrMapValue
(lower-case first letter) works likestringAttrMapValue
but is evaluated as Go code instead of a static string.@Something
is used for component events, see below.
The vg-key
attribute can also be specified on a component reference to indicate an additional value which
should be used for caching purposes. Generally this is a key used in a loop iterator. Note that loops will automatically
select the key from the vg-for
,vg-key
The vg-var
attribute, if specified, will cause a Go variable declaration statement to be
made with the value of the component created. I.e. <pkg:Comp vg-var="a" ...
will result in var a = /* component reference */
in the resulting Go code. This allows you to access the component
and its fields and methods directly as a
when needed.
Any component can include any other component. But if components include each other in a loop the behavior is undefined (but I can promise you it won't be good).
Component Lifecycle
At runtime, components are created and destroyed as needed, based on the logic in your application that declares what is supposed to be
displayed at a given moment. For example, a vg-if
expression can be used to turn a section of output on or off. If this section
includes one or more components, the necessary components are created as needed. Other mechanisms such as vg-for
(and in combination with the vg-key
attribute) are used to describe more complex cases of where a component needs to exist multiple
times and indicates the exact key used to identify which component is which.
Regardless of the exact logic for each case, the concept is the same: Components are created when needed, remain as long as they are needed, and destroyed as soon as a render occurs where that component was not needed.
The following methods can be defined on a component and will be called at the appropriate time during the build/render process:
Init(ctx vugu.InitCtx)
is called when a component is created before any other callbacks and gives an opportunity to initialize.Compute(ctx vugu.ComputeCtx)
is called each time before the output of a given component is built. It provides an opportunity to compute any necessary information for each render pass. (NOTE: this was previously called BeforeBuild.)Rendered(ctx vugu.RenderedCtx)
is called after each render pass has completed and all DOM elements have been synchronized with the page.Destroy(ctx vugu.DestroyCtx)
is called when build/render pass occurs and it is discovered that a component is no longer needed and gives an opportunity to destroy any resources no longer used.
For convenience, on each of these the ctx
paramter is optional and if omitted it will be called without it. E.g Init()
can be used if you don't need the context.
The context on each does provide an EventEnv, which is useful if you need to trigger a re-render (see next).
Here's an example of a component that defines each of these lifecycle callback methods and gives an idea of their intended use:
<!-- my-comp.vugu --> <div> <ul vg-for='_, item := range c.Items'> <li vg-content="item"></li> </ul> <div>Short count: <span vg-content='c.shortItemCount'></span></div> </div> <script type="application/x-go"> import ( "net/http" "encoding/json" ) type MyComp struct { Loading bool `vugu:"data"` Items []string `vugu:"data"` shortItemCount int } func (c *MyComp) Init(ctx vugu.InitCtx) { // kick of loading data from an endpoint c.Loading = true go func() { resp, err := http.Get("/some/endpoint") if err != nil { log.Printf("Error fetching: %v", err) return } defer resp.Body.Close() var items []string err =json.NewDecoder(resp.Body).Decode(&items) if err != nil { log.Printf("Error decoding response: %v", err) return } ctx.EventEnv().Lock() c.Loading = false c.Items = items ctx.EventEnv().UnlockRender() }() } func (c *MyComp) Compute() { // recompute each render count := 0 for _, item := range c.Items { if len(item) < 5 { count++ } } c.shortItemCount = count } func (c *MyComp) Rendered(ctx vugu.RenderedCtx) { // if you really need to manipulate DOM directly after it is rendered, you can do it here if ctx.First() { // only after first render el := js.Global().Get("document").Call("getElementById", "some_id_here") _ = el // do something with an element manually after the first render } } func (c *MyComp) Destroy() { // some teardown code here } </script>
Note: main_wasm.go change
With the implementation of these lifecycle events, some minor changes were made to the generated main_wasm.go file. If you are updating an existing project from a prior Vugu version you may need to delete main_wasm.go and let it get regenerated using the latest Vugu version.
Component Events
Unlike DOM events, events for components are really just a means of receiving a simple method call. Vugu provides some facilities to declare and use your own events quickly and easily.
The @
symbol followed by an exported Go field name (starts with an uppper case letter)
is a shorthand for an assignment to this field name as follows:
<pkg:Comp @Something="log.Println(event)"></pkg:Comp>
<pkg:Comp :Something='func(event pkg.SomethingEvent) { log.Println(event) }' ></pkg:Comp>
In this case pkg.Comp
would be a struct with a field called
Something
of type SomethingHandler
. (See more explanation below.)
To faciliate creating component events, the code generation syntax
//vugugen:event Something
<!-- root.vugu --> <div id="top"> <main:Thing @Click='c.ShowText = fmt.Sprintf("%#v",event)' ></main:Thing> <div vg-content="c.ShowText"></div> </div> <script type="application/x-go"> import "fmt" type Root struct { ShowText string `vugu:"data"` } </script>
<!-- thing.vugu --> <div> <button @click="c.HandleButtonClick(event)">A button here</button> </div> <script type="application/x-go"> type Thing struct { Click ClickHandler } func (c *Thing) HandleButtonClick(event vugu.DOMEvent) { if c.Click != nil { c.Click.ClickHandle(ClickEvent{DOMEvent:event}) } } // ClickEvent we are declaring ourselves so we can add whatever fields we want. // If we had not declared it here, the comment below would cause this to have // be created automatically. type ClickEvent struct { vugu.DOMEvent } //vugugen:event Click // The above statement will code generate the following for any of // these that you do not declare yourself: // ClickHandler is the interface for things that can handle ClickEvent. // type ClickHandler interface { // ClickHandle(event ClickEvent) // } // ClickFunc implements ClickHandler as a function. // type ClickFunc func(event ClickEvent) // ClickHandle implements the ClickHandler interface. // func (f ClickFunc) ClickHandle(event ClickEvent) { f(event) } </script>
In this case our Thing
has a field called Click which is of type
ClickHandler
. Each of the related types:
ClickEvent,
ClickHandler,
ClickFunc
will be generated automatically by the vugugen comment if they don't
exist. This field can then be easily assigned with
<main:Thing @Click='...'>
as shown.
Tip: Component Event Naming
The suggested approach for naming events is to use the name of the action in the present tense. i.e. 'Save' rather than 'Saved' or another variation.
If the component event name corresponds directly to a DOM event, use the camel case version, i.e.
DOM event "click" becomes ClickEvent defined with DOMEvent embedded into it
type ClickEvent struct { vugu.DOMEvent }
. (The code generator for //vugugen:event Click
will do this for you if you do not otherwise declare ClickEvent
.)
This makes less hassle when writing code that involves similar DOM and components events
and they can be used in similar ways. For example
<pkg:Comp @Click='event.PreventDefault()'>
,<button @click='event.PreventDefault()'>
Also, your event types may be declared as either structs or interfaces. Use whichever is most appropriate for your situation. For simple one-off events structs are probably more appropriate. For complex events that have deep and meaningful implications throughout a large app, and interface might be better.
Again, this system of naming is intended to make it simple to create custom events for your components
that are both type-safe and easy to use. If you need to do something more sophisticated you can use
the :Something=
syntax to assign an arbitrary Go expression to a field and this works
perfectly well for component events as well. Remember, components events are just regular struct
field assignements, with some tooling to make them easier to use with the @ syntax.
Dynamic Component References
Component references can also be done dynamically based on an arbitrary Go expression. This is done with
a <vg-comp expr=""><vg-comp>
tag, and the expression must
resolve to a type compatible with vugu.Builder.
This allows you to instantiate a component in your own Go code and reference it directly during render. Example:
<!-- root.vugu --> <div> <vg-comp expr="c.MyChildComp"></vg-comp> </div> <script type="application/x-go"> type Root struct { MyChildComp vugu.Builder } func (c *Root) Init() { c.MyChildComp = &someother.ComponentHere{} } </script>
Slots
Slots are a mechanism that allow you to pass content (or logic that generates content) into another component, so it can output it in the appropriate place. Slots are implemented by emitting a Go anonymous function which outputs the markup you specify, converts it to a vugu.Builder, and assigns it to a field on a component struct. Example:
<!-- root.vugu --> <div> <!-- content placed directly inside a component is "DefaultSlot"--> <main:MySlottedComp> <div>This goes in the DefaultSlot</div> </main:MySlottedComp> <!-- or we can specify mutliple slots with names--> <main:MySlottedComp vg-var='myParentComp'> <vg-slot name="DefaultSlot"> <div>This goes in the DefaultSlot</div> </vg-slot> <vg-slot name="AfterSlot"> <div>This goes in the AfterSlot: <span vg-content='fmt.Sprintf(myParentComp)'></span></div> </vg-slot> </main:MySlottedComp> </div>
<!-- my-slotted-comp.vugu --> <div> <!-- will render DefaultSlot here if not-nil--> <vg-comp expr='c.DefaultSlot'></vg-comp> <!-- will render AfterSlot here if not-nil--> <vg-comp expr='c.AfterSlot'></vg-comp> </div> <script type="application/x-go"> type MySlottedComp struct { DefaultSlot vugu.Builder AfterSlot vugu.Builder } </script>
As you can see above, content directly inside a component tag is assigned to the "DefaultSlot".
Or the <vg-slot>
tag can be used to assign to an explicitly named slot.
Inside in the implementation of the slotted component, <vg-comp>
is used to output the slot content (see above for more info on vg-comp). You can also see
in the example that slots can access the component they are "inside" via a variable name
declared with vg-var
.
Dynamic slot names are also available using the syntax Field[IndexExpr]
as the
slot name. In this case, Field
must be of type map[string]vugu.Builder
and IndexExpr
must be a valid expression to be used for assignment into the map.
TODO: Once a "data table" component is created this will provide a good example for
how dynamic slots are handled and this documentation should be updated accordingly.
But the core idea is that slots are just functions, and since they can access the parent
component from the scope they are declared in (via the vg-var attr),
a data table can for example have a field
like CurrentIndex int
and a slot that handles an individual row can read
that field upon each iteration to determine which record it's supposed to output.
This is in contrast to how Vue deals with "scoped slots".
In Vugu, you don't "pass" things into slots,
instead they just read from the scope they are declared in. This sounds strange
initially but it allows the various references to be type-safe and avoids passing
around interface{}s with type assertions. Slots can understand the context in which
they are called and the input data, the component that has the slot itself should
understand as little about its input as possible. A similar approach is used
for things like sort.Slice in the Go standard library - where the caller understands
what it is sorting, and the code in the sort package knows only how to sort.
It works well there. Likewise, components with slots only know where and in what sequence
they output slot content, not all the details about what data they are dealing with.
(This even applies if there is a bunch of crazy reflection code to provide nice
pretty output by default by inspecting, e.g. a slice that is passed to a data table -
it still doesn't change the basic idea.)
This whole concept should be explained clearly and in detail with examples
once some more experience is gained.
Modification Tracking
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 struct tag
`vugu:"data"`
. Any struct fields used as component input should be tagged as such, or you must implement the ModChecker interface. "Computed fields", populated duringBeforeBuild()
should generally not be tagged like this. - Then modification checking continues to traverse based on these rules:
- For values implementing the
ModChecker
interface, theModCheck
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 re-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 tracking is done, for when the graph of objects gets too large and things get slow.