Skip to content

Latest commit

 

History

History
296 lines (198 loc) · 8.42 KB

File metadata and controls

296 lines (198 loc) · 8.42 KB

Create Your First Counter

Learn the fundamentals of nojs by building a simple, reactive counter component. In just 10 minutes, you'll understand how components, state, data binding, and event handling work.

What you'll build: A button-based counter that increments and displays a live count. What you'll learn: Components, reactive state, data binding, event binding, AOT compilation. No routing, no complexity — just the essentials.


Prerequisites

  • Go 1.25+https://go.dev/dl/
  • Make — pre-installed on Linux/macOS; use WSL on Windows
  • Python 3 — Required to serve the app locally. This tutorial uses python3 -m http.server for simplicity; any static file server works, but we assume Python 3 is available so you can focus on learning nojs, not configuring servers.

Estimated time: 10 minutes


Project Setup

We've provided a starter project with all the boilerplate already in place. Clone it and you're ready to start:

git clone https://github.com/ForgeLogic/nojs-tutorials.git
cd nojs-tutorials
git checkout counter-tutorial

File Structure

Here's what's already in the project:

counter-tutorial/
├── internal/app/components/counter/
│   ├── counter.go              ← You'll fill this in (Step 1-2)
│   └── Counter.gt.html         ← You'll fill this in (Step 3)
├── wwwroot/
│   ├── index.html              ← Ready to go
│   ├── core.js                 ← Ready to go
│   └── (wasm_exec.js — copy it in Step 0)
├── main.go                     ← You'll fill this in (Step 5)
├── go.mod                      ← Already configured
├── Makefile                    ← Ready to use
└── README.md                   ← Quick reference

Step 0: Copy wasm_exec.js

The wasm_exec.js file is the Go runtime bridge for WebAssembly. It must match your installed Go version.

cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" wwwroot/

This copies it from your Go installation to the project.


Step 1: Create the Component Struct

Open internal/app/components/counter/counter.go and fill in the struct.

Currently, it looks like this:

//go:build js || wasm

package counter

import "github.com/ForgeLogic/nojs/runtime"

type Counter struct {
    runtime.ComponentBase
    // TODO: Add Count field here
}

Replace the TODO with:

//go:build js || wasm

package counter

import "github.com/ForgeLogic/nojs/runtime"

type Counter struct {
    runtime.ComponentBase
    Count int
}

What's happening?

  • //go:build js || wasm — Build tag that tells Go to only compile this file for WebAssembly targets, not during test builds.
  • runtime.ComponentBase — Embedded struct that gives your component lifecycle methods and the StateHasChanged() function.
  • Count int — Your component's state. Must be exported (capitalized) — templates can only access public fields.

Step 2: Add the Increment Method

Still in counter.go, add a method that mutates the state:

func (c *Counter) Increment() {
    c.Count++
    c.StateHasChanged()
}

What's happening?

  • c.Count++ — Increment the counter value.
  • c.StateHasChanged() — Tell the framework "My state changed, please re-render me." This triggers the virtual DOM diffing and patching process.

Step 3: Create the Template

Open internal/app/components/counter/Counter.gt.html and replace the TODO with:

<div>
    <p>Count: {Count}</p>
    <button @onclick="Increment">Click me</button>
</div>

What's happening?

  • {Count}Data binding. Inserts the Count field value into the HTML. The receiver is inferred by the compiler — you never write {c.Count}, just {Count}. The compiler knows the receiver is implicit.
  • @onclick="Increment"Event binding. When the button is clicked, calls the Increment() method on your component.

Step 4: Compile the Component

Run the compiler to generate the Render() method from your template:

make install

Then:

make full

What's happening?

  1. make install downloads framework dependencies and installs the nojsc compiler.
  2. make full runs the compiler:
    • nojsc parses Counter.gt.html
    • Generates Counter.generated.go with the auto-generated Render() method
    • Compiles Go to WASM (main.wasm)
  3. You'll see the new Counter.generated.go file in your project directory.
  4. All template validation happens at build time, not runtime. If there are errors, the compiler will report them.

Step 5: Mount the Component to the DOM

Open main.go and replace the TODO with:

//go:build js || wasm

package main

import (
    "github.com/ForgeLogic/nojs/runtime"
    "nojs-counter/internal/app/components/counter"
)

func init() {
    runtime.Mount("#app", &counter.Counter{})
}

func main() {
    select {}
}

Note: Update the import path to match your module name in go.mod. If your module is github.com/myuser/my-counter-app, change the import to "github.com/myuser/my-counter-app/internal/app/components/counter".

What's happening?

  • runtime.Mount("#app", &counter.Counter{}) — Finds the DOM element with id app and renders your component there.
  • select {} — Keeps the Go runtime running and listening for events. Don't remove this.

Step 6: Rebuild and Run

Recompile with your updated main.go:

make full

Then serve the app:

make serve

You should see:

Serving HTTP on 0.0.0.0 port 9090 (http://0.0.0.0:9090/) ...

Step 7: See It Work

Open your browser to http://localhost:9090

You should see:

  • A paragraph: Count: 0
  • A button: Click me

Click the button. The count increments. Click again. It goes up. No page refresh, no manual DOM queries — just pure reactive updates.

Open the browser DevTools (F12 → Console). You should see:

WebAssembly module loaded.

Congratulations! You've built your first nojs component. 🎉


What Happened Behind the Scenes

Here's the data flow:

  1. User clicks button → Browser's event listener triggers
  2. Increment() method called → You wrote this in Step 2
  3. c.Count++ → State is mutated
  4. StateHasChanged() → Framework notified
  5. Render() called → Compiler-generated method creates new virtual DOM
  6. Virtual DOM diffed → New tree compared against old tree
  7. Minimal changes calculated → Only {Count} changed
  8. Real DOM patched → Browser updates only the <p> text
  9. Result: Count reflects the new value, instantly

This is the virtual DOM cycle — the core of nojs's performance. No full page reload, no querySelector, no manual textContent assignments. Just: mutate state → call StateHasChanged() → framework handles the rest.


Next Steps

🎯 Exercise: Add a Decrement Button

Try this on your own:

  1. Add a Decrement() method to your component (copy Increment, change ++ to --)
  2. Update the template to add a decrement button
  3. Rebuild (make full) and test

📚 Learn More


Troubleshooting

command not found: nojsc

Make sure you ran make install first. This installs the compiler.

Template doesn't update when I click

Make sure you called StateHasChanged() in your event handler. Without it, the framework doesn't know to re-render.

wasm_exec.js: No such file

You need to copy it (see Step 0). It's version-specific to your Go installation.

I see compilation error from the compiler

Write the full error message from the terminal. Common issues:

  • Typo in field name (remember: must be capitalized like Count, not count)
  • Typo in method name in @onclick="..."
  • Invalid HTML syntax in the template