Skip to content

YOUR FIRST APP

Firstly, we must create the app itself:

import (
    "fmt"
    "os"

    "github.com/loom-go/loom"
    "github.com/loom-go/term"
)

// define your root component.
// this is what we're going to provide to the renderer.
func App() loom.Node {
    return nil
}

func main() {
    // create a new terminal app
	app := term.NewApp()

    // tell the renderer to render in fullscreen,
    // and give it our root `App` component.
	for err := range app.Run(term.RenderFullscreen, App) {
        // in case of error, close the app and exit with status 1
		app.Close()
		fmt.Printf("Error: %v\n", err)
		os.Exit(1)
	}
}
import (
    "github.com/loom-go/loom"
    "github.com/loom-go/web"
)

// define your root component.
// this is what we're going to provide to the renderer.
func App() loom.Node {
    return nil
}

func main() {
    // create a new web app
	app := web.NewApp()

    // tell the renderer to render in `#root`,
    // and give it our root `App` component.
	for err := range app.Run("#root", App) {
        // in case of error, log it to the console
        web.ConsoleError(fmt.Sprintf("Error: %v\n", err)
	}
}

With the app setup, let’s create our first component:

import (
    . "github.com/loom-go/term/components"
)

func Title() loom.Node {
    return P(Text("beep boop"))
}

// update the root component to use `Title`
func App() loom.Node {
    return Title()
}
import (
    . "github.com/loom-go/web/components"
)

func Title() loom.Node {
    return P(Text("beep boop"))
}

// update the root component to use `Title`
func App() loom.Node {
    return Title()
}

Now try running the app:

go run main.go

Create an index.html file:

<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>App</title>
  </head>

  <body>
    <div id="app"></div>

    <script src="https://cdn.jsdelivr.net/gh/golang/[email protected]/lib/wasm/wasm_exec.js"></script>
    <script>
      const go = new Go();
      WebAssembly.instantiateStreaming(
        fetch("main.wasm"),
        go.importObject,
      ).then((r) => go.run(r.instance));
    </script>
  </body>
</html>

Build your main.go in wasm:

GOOS=js GOARCH=wasm go build -o main.wasm main.go

And serve your files with your favorite http server! Here with serve:

npx serve

✦‧₊˚⋅ And its live! ✦‧₊˚⋅

Styling

Styling works via appliers. How it works will vary depending on the renderer you are using, but it stays mostly the same. Make sure to read your renderer’s styling guide to understand more!

Let’s try to style our component:

import (
    . "github.com/loom-go/term/components"
)

// create a textStyle variable containing a Style{} applier
var titleStyle = Style{
    Color:      "darkred", // set the text color to darkred
    FontWeight: "Bold", // and the font bold
}

func Title() loom.Node {
    // apply textStyle to the P Node (which will be inherited by the Text Node)
    return P(
        Text("beep boop"),
        Apply(textStyle),
    )
}
import (
    . "github.com/loom-go/web/components"
)

// create a textStyle variable containing a Style{} applier
var titleStyle = Style{
    "color":       "darkred", // set the text color to darkred
    "font-weight": "bold", // and the font bold
}

func Title() loom.Node {
    // apply textStyle to the P Node (which will be inherited by the Text Node)
    return P(
        Text("beep boop"),
        Apply(textStyle),
    )
}

We can go a bit further and make it centered on the screen by styling our App component:

var rootStyle = Style{
    Width:          "100%",
    Height:         "100%",
	PaddingVertical: 6,
    FlexDirection:  "column",
    AlignItems:     "center",
}

func App() loom.Node {
    // wrap our component in a box, and apply our root style on that box
    return Box(
        Title(),
        Apply(rootStyle),
    )
}
var rootStyle = Style{
    "width":           "100vw",
    "height":          "100vh",
    "display":         "flex",
    "padding":         "1rem 0",
    "flex-direction":  "column",
    "align-items":     "center",
}

func App() loom.Node {
    // wrap our component in a div, and apply our root style on that box
    return Div(
        Title(),
        Apply(rootStyle),
    )
}

If you run the app again, you should see the title is now centered!

Attributes

Using attributes is very similar to styling. It works with appliers too, and will vary depending on the renderer you’re using.

The official renderers each provide an Attribute{} applier (and Attr{} as an alias).

Let’s try adding a new component that uses attributes:

var titleStyle = Style{
    // ...
    MarginBottom: 3, // add a bit of space for the title
}

var formStyle = Style{
	MaxWidth:      40,
	FlexDirection: "column",
	GapRow:        1,
}

var styleInput = Style{
    Width:                30,
	AlignSelf:            "center",
    BackgroundColor:      "darkgray",
    PlaceholderFontStyle: "italic",
}

func Form() loom.Node {
    return Input(Apply(
        styleInput, // apply our style for this input
        Attr{Placeholder: "Your value..."}, // define a placeholder via attributes
    ))
}

func App() loom.Node {
    return Box(
        Title(),
        Form(), // add our new component to the `App`
        Apply(rootStyle),
    )
}
var titleStyle = Style{
    // ...
    "margin-bottom": "1.5rem", // add a bit of space for the title
}

var formStyle = Style{
	"max-width":      "200px",
	"flex-direction": "column",
	"row-gap":        "10px",
}

var styleInput = Style{
    "width":      "100px",
    "align-self": "center",
}

func Form() loom.Node {
    return Box(
        Input(Apply(
            styleInput, // apply our style for this input
            Attr{"placeholder": "Your value..."}, // define a placeholder via attributes
        )),

        Apply(formStyle),
    )
}

func App() loom.Node {
    return Div(
        Title(),
        Form(), // add our new component to the `App`
        Apply(rootStyle),
    )
}

Run the app again to see the changes!

Events

Events is no exception, it also works through appliers.

The official renderers provide the On{} applier. It registers callbacks on the Node for spesific events, and removed them when the Node is disposed.

Let’s keep going with our Form() component by using the On{} applier:

import (
    // ...

    "encoding/base64"

    . "github.com/loom-go/loom/components"
)

func Form() loom.Node {
    result, setResult := Signal("")

    onInput := func(evt *term.EventInput) {
        // encode the input's value in base64
        raw := []byte(evt.Value)
        encoded := base64.StdEncoding.EncodeToString(raw)

        // set result with the encoded value
        setResult(encoded)
    }

    return Box(
        Input(Apply(
            styleInput,
            On{Input: onInput}, // register our callback with On{}
            Attr{Placeholder: "Your value..."},
        )),

        // display the encoded result
        P(Text("Result: "), BindText(result)),

        Apply(formStyle),
    )
}
import (
    // ...

    "encoding/base64"

    . "github.com/loom-go/loom/components"
)

func Form() loom.Node {
    result, setResult := Signal("")

    onInput := func(evt *web.EventInput) {
        // encode the input's value in base64
        raw := []byte(evt.Value)
        encoded := base64.StdEncoding.EncodeToString(raw)

        // set result with the encoded value
        setResult(encoded)
    }

    return Div(
        Input(Apply(
            styleInput,
            On{"input": onInput}, // register our callback with On{}
            Attr{"placeholer": "Your value..."},
        )),

        // display the encoded result
        P(Text("Result: "), BindText(result)),

        Apply(formStyle),
    )
}

And run the app to see our little base64 encoder!


Feel free to keep exploring the docs and the examples to see the various possibilities and features of loom!