Using Components

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 as AttrMap vugu.AttrMap (an alias for 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.
  • @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, but you can override what is used for caching purposes by specifying vg-key on your component reference.

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>

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>
is shorthand for:
<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 exists to automatically emit the appropriate types and interfaces if you do not declare them yourself. Let's look at a complete example:

<!-- 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.

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 during BeforeBuild() should generally 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 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.