WIP: V2.16.0 add state control with wasm#19
Draft
felipegenef wants to merge 26 commits into
Draft
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v2.16.0 - Client-Side State with WASM
This release adds a full reactive client-side state system to Gothic, built on TinyGo WASM. Pages and components can now declare local reactive state, observe changes, and manipulate the DOM — all written in Go, compiled to a per-page WASM binary that is served, cached, and decompressed automatically by the framework. No JavaScript needed.
Highlights
ClientSideState func()field onRouteConfig— declare WASM state inline in your.templfileCreateObservable,Observe,ObserveWithCleanup— fine-grained reactivity without a virtual DOMSetText,SetHTML,SetValue,GetValue,SetAttr,SetStyle,AddClass,RemoveClass,ToggleClassFetchandFetchBytes— call any API from WASM with Go-style(value, error)returnsGetFileBytes— read a file input as[]bytefor uploads or local processingCreateWasmFunc,CreateWasmStringFunc,CreateWasmBoolFunc— expose Go functions as global JS callablesClientSideStategets its own compiled binary with dead-code eliminationWasmCompressiononRouteConfigStatefulComponentOffor lazy-loaded HTMX component wrappersClient-Side State
Add a
ClientSideStatefunction to anyRouteConfigto opt a page into WASM:ClientSideStatebody is extracted by the CLI, tree-shaken, and compiled with TinyGo to a.wasmbinary.Content-Typeand compression headers.Reactive Observables
CreateObservable[T](initial T) *Observable[T]obs.Get() Tobs.Set(v T)Observe(fn func(), deps ...any) *Subscriptionfnimmediately and re-runs whenever any dep changesObserveWithCleanup(fn func() func(), deps ...any) *SubscriptionObservebutfnreturns a cleanup function called before each re-runsub.Stop()Shared Context (Cross-Component State)
Pages and components on the same page can share typed state through a centralized context system. State is encoded in a compact binary format and broadcast per-field — only components subscribed to a specific field receive updates.
Defining a context
Create a struct that embeds
GothicSharedContextand tags each field with its wire name and codec hint. The CLI generates aPageContext()function from this struct automatically.Reading and writing from a page or component
Call
PageContext()insideClientSideStateto get a handle to the shared state. Each field is an*Observable[T]— observe it withObserve, read it with.Get(), and write it with.Set(). Local state (not shared) lives alongside it as a regularCreateObservable.Any page or component on the same page that calls
PageContext()shares the same state. Writing topageCtx.Pingsfrom one component immediately updates all other subscribers — only the fields that changed are transmitted over the wire.Supported field types
int,int8–int64,uint–uint64,float32,float64,bool,string[]string,[]int,[]*MyStruct*MyStructmap[string]int,map[string]MyStruct,map[string]map[string]boolThe codec is fully generated by the CLI from your struct definition using Go AST — no
encoding/json, no reflection at runtime.DOM Helpers
All helpers are available via dot-import and compile as no-ops server-side:
Text and HTML
SetText(id, value string)element.textContentSetHTML(id, html string)element.innerHTMLSetValue(id, value string)element.value(inputs)GetValue(id string) stringelement.valueCSS Classes
AddClass(id, className string)RemoveClass(id, className string)ToggleClass(id, className string)Attributes and Styles
SetAttr(id, attr, value string)SetStyle(id, property, value string)All helpers are scope-aware: when the same component is rendered multiple times on a page, each instance operates only on its own DOM subtree.
HTTP Helpers
Fetch(url, ...FetchConfig) (string, error)stringFetchBytes(url, ...FetchConfig) ([]byte, error)[]bytearrayBuffer()File Helpers
GetFileBytes(id string) []bytereads the first selected file from a<input type="file">using the browser'sFileReaderAPI. Blocks until reading completes. Returnsnilif no file is selected.JS Function Bridge
Expose Go functions as globally callable JS functions for use in
onclick,oninput, and similar HTML event handlers:CreateWasmFunc(name, fn func())name()CreateWasmStringFunc(name, fn func(string))name(str)CreateWasmBoolFunc(name, fn func(bool))name(bool)Per-Page WASM Compilation
The
gothicframework wasmcommand (run automatically duringgothicframework dev) scans all pages withClientSideStateand compiles a separate TinyGo binary for each:WasmCompression: routes.BROTLIorroutes.GZIP.wasm_exec.jsare injected automatically into the page's<head>.ClientSideStateis included.Stateful Components
Components with their own WASM state can be lazy-loaded into any page:
The children slot is shown as a loading placeholder while the component binary loads.
StatefulComponentOfreads the route path fromRouteConfig.Path— no magic strings.AST-Based Code Generation
The entire WASM pipeline now uses proper Go AST parsing instead of regex and string slicing:
pkg/helpers/wasm/astx: loads user packages, extractsClientSideStatebodies, and performs tree-shaking usinggo/packagesandgo/ast.typeRefinterface (Named,SliceOf,MapOf,PointerOf,PointerOf): structural type representation replaces string-sliced type names throughout the codec generator.reflect.StructTagparsing replaces manualstrings.Fields/strings.Trimfor struct tag extraction.bufio.Scannerreplacesstrings.Split(string(body), "\n")in checksum parsing.rewriteAutoKeys,rewriteContextCalls) return(string, error)with positioned errors instead of silently falling back to regex.This eliminates an entire class of subtle bugs where type names containing generics, pointers, or nested brackets would confuse the old string-based parser.
What Changed
New Packages
pkg/wasmpkg/wasm/wasm-runtime/runtimepkg/helpers/wasmpkg/helpers/wasm/astxClientSideStateextractorpkg/helpers/routes/wasm_bootstrap.goNew
RouteConfigFieldsClientSideStatefunc()WasmCompressionCompressionMethodroutes.GZIP(default) orroutes.BROTLINew Commands
gothicframework wasmClientSideStateNew Dependencies
tinygo(external)golang.org/x/sync/errgroupcmd/init.go