ErrorBoundary struct

ErrorBoundary wraps a child component and renders a fallback UI when the

child panics during Render or Mount. Once a panic occurs, the fallback UI is

displayed for subsequent renders.

Fields:

  • Child (Component)
  • Fallback (string)
  • err (any)

Methods:

fallbackHTML


Returns:
  • string

Show/Hide Method Body
{
	return "<root data-component-id=\"" + e.Child.GetID() + "\">" + e.Fallback + "</root>"
}

Render

Render renders the child component, returning the fallback HTML if the child

panics or if a previous panic was recorded.


Returns:
  • out string

Show/Hide Method Body
{
	if e.err != nil {
		return e.fallbackHTML()
	}
	defer func() {
		if r := recover(); r != nil {
			e.err = r
			out = e.fallbackHTML()
		}
	}()
	return e.Child.Render()
}

Mount

Mount mounts the child component, updating the DOM with the fallback HTML if

the child panics during mounting.


Show/Hide Method Body
{
	if e.err != nil {
		return
	}
	defer func() {
		if r := recover(); r != nil {
			e.err = r
			dom.UpdateDOM(e.Child.GetID(), e.Fallback)
		}
	}()
	e.Child.Mount()
}

Unmount

Unmount delegates to the child component's Unmount method.


Show/Hide Method Body
{ e.Child.Unmount() }

OnMount

OnMount is a no-op for ErrorBoundary.


Show/Hide Method Body
{}

OnUnmount

OnUnmount is a no-op for ErrorBoundary.


Show/Hide Method Body
{}

GetName

GetName returns the name of the component.


Returns:
  • string

Show/Hide Method Body
{ return "ErrorBoundary" }

GetID

GetID returns the wrapped child's ID.


Returns:
  • string

Show/Hide Method Body
{ return e.Child.GetID() }

SetSlots

SetSlots delegates slot assignment to the child component.


Parameters:
  • slots map[string]any

Show/Hide Method Body
{
	if e.Child != nil {
		e.Child.SetSlots(slots)
	}
}

NewErrorBoundary function

NewErrorBoundary creates a new ErrorBoundary around the provided child

component. If the child panics during Render or Mount, the provided fallback

HTML will be rendered instead.

Parameters:

  • child Component
  • fallback string

Returns:

  • *ErrorBoundary

References:

Show/Hide Function Body
{
	return &ErrorBoundary{Child: child, Fallback: fallback}
}

ComponentStats struct

ComponentStats is a stub for non-wasm builds.

Fields:

  • RenderCount (int)
  • TotalRender (time.Duration)
  • LastRender (time.Duration)
  • AverageRender (time.Duration)
  • Timeline ([]ComponentTimelineEntry)

ComponentTimelineEntry struct

ComponentTimelineEntry is a stub for non-wasm builds.

Fields:

  • Kind (string)
  • Timestamp (time.Time)
  • Duration (time.Duration)

Plugin interface

Plugin defines interface for plugins to register hooks on the App. Plugins can

provide a build step which is executed by the CLI before the application is

run and may also attach runtime hooks through Install.

Methods:

Build


Parameters:
  • json.RawMessage

Returns:
  • error

Install


Parameters:
  • *App

Named interface

Named plugins expose a unique identifier used for deduplication.

Implementing this interface is optional.

Methods:

Name


Returns:
  • string

Requires interface

Requires allows plugins to declare mandatory dependencies.

Implementing this interface is optional.

Methods:

Requires


Returns:
  • []Plugin

Optional interface

Optional allows plugins to declare optional dependencies.

Implementing this interface is optional.

Methods:

Optional


Returns:
  • []Plugin

PreBuilder interface

PreBuilder allows plugins to execute logic before the CLI build step.

Implementing this interface is optional.

Methods:

PreBuild


Parameters:
  • json.RawMessage

Returns:
  • error

PostBuilder interface

PostBuilder allows plugins to execute logic after the CLI build step.

Implementing this interface is optional.

Methods:

PostBuild


Parameters:
  • json.RawMessage

Returns:
  • error

Uninstaller interface

Uninstaller allows plugins to clean up previously registered hooks.

Implementing this interface is optional.

Methods:

Uninstall


Parameters:
  • *App

App struct

App maintains registered hooks and exposes helper methods for plugins

to attach to framework events.

Fields:

  • pluginVars (map[string]map[string]any)
  • plugins (map[string]Plugin)

Methods:

RegisterRouter

RegisterRouter adds a router navigation hook.


Parameters:
  • fn func(string)

Show/Hide Method Body
{
	a.routerHooks = append(a.routerHooks, fn)
}

RegisterStore

RegisterStore adds a store mutation hook.


Parameters:
  • fn func(module, store, key string, value any)

Show/Hide Method Body
{
	a.storeHooks = append(a.storeHooks, fn)
}

RegisterTemplate

RegisterTemplate adds a template render hook.


Parameters:
  • fn func(componentID, html string)

Show/Hide Method Body
{
	a.templateHooks = append(a.templateHooks, fn)
}

RegisterLifecycle

RegisterLifecycle adds hooks for component mount and unmount.


Parameters:
  • mount func(Component)
  • unmount func(Component)

Show/Hide Method Body
{
	if mount != nil {
		a.mountHooks = append(a.mountHooks, mount)
	}
	if unmount != nil {
		a.unmountHooks = append(a.unmountHooks, unmount)
	}
}

RegisterRTMLVar

RegisterRTMLVar registers a value that can be referenced from RTML as

{plugin:NAME.VAR}.


Parameters:
  • plugin string
  • name string
  • val any

Show/Hide Method Body
{
	if a.pluginVars == nil {
		a.pluginVars = make(map[string]map[string]any)
	}
	if _, ok := a.pluginVars[plugin]; !ok {
		a.pluginVars[plugin] = make(map[string]any)
	}
	a.pluginVars[plugin][name] = val
}

HasPlugin

HasPlugin reports whether a plugin with the given name is installed.


Parameters:
  • name string

Returns:
  • bool

Show/Hide Method Body
{
	if a.plugins == nil {
		return false
	}
	_, ok := a.plugins[name]
	return ok
}

RegisterRouter


Parameters:
  • fn func(string)

Show/Hide Method Body
{}

RegisterStore


Parameters:
  • fn func(module, store, key string, value any)

Show/Hide Method Body
{}

RegisterLifecycle


Parameters:
  • mount func(Component)
  • unmount func(Component)

Show/Hide Method Body
{}

RegisterTemplate


Parameters:
  • fn func(componentID, html string)

Show/Hide Method Body
{}

RegisterRTMLVar


Parameters:
  • plugin string
  • name string
  • val any

Show/Hide Method Body
{}

HasPlugin


Parameters:
  • name string

Returns:
  • bool

Show/Hide Method Body
{ return false }

hooks struct

Fields:

  • routerHooks ([]func(string))
  • storeHooks ([]func(module, store, key string, value any))
  • templateHooks ([]func(componentID, html string))
  • mountHooks ([]func(Component))
  • unmountHooks ([]func(Component))

newApp function

newApp creates an App with initialized hook storage.

Returns:

  • *App
Show/Hide Function Body
{
	return &App{hooks: &hooks{}, pluginVars: make(map[string]map[string]any), plugins: make(map[string]Plugin)}
}

getRTMLVar function

getRTMLVar retrieves a registered plugin variable.

Parameters:

  • plugin string
  • name string

Returns:

  • any
  • bool
Show/Hide Function Body
{
	if app.pluginVars == nil {
		return nil, false
	}
	if vars, ok := app.pluginVars[plugin]; ok {
		v, ok := vars[name]
		return v, ok
	}
	return nil, false
}

RegisterPluginVar function

RegisterPluginVar is a convenience wrapper for plugins to expose variables.

Parameters:

  • plugin string
  • name string
  • val any
Show/Hide Function Body
{
	app.RegisterRTMLVar(plugin, name, val)
}

RegisterPlugin function

RegisterPlugin registers a plugin and allows it to add hooks. If the plugin

implements Named and has already been installed, it is skipped.

Parameters:

  • p Plugin

References:

Show/Hide Function Body
{
	if n, ok := p.(Named); ok {
		if app.HasPlugin(n.Name()) {
			return
		}
		if app.plugins == nil {
			app.plugins = make(map[string]Plugin)
		}
		app.plugins[n.Name()] = p
	}
	if r, ok := p.(Requires); ok {
		for _, dep := range r.Requires() {
			if dn, ok := dep.(Named); ok {
				if app.HasPlugin(dn.Name()) {
					continue
				}
			}
			RegisterPlugin(dep)
		}
	}
	if o, ok := p.(Optional); ok {
		for _, dep := range o.Optional() {
			if dn, ok := dep.(Named); ok {
				if app.HasPlugin(dn.Name()) {
					continue
				}
			}
			RegisterPlugin(dep)
		}
	}
	p.Install(app)
}

TriggerRouter function

TriggerRouter invokes router hooks with the given path.

Parameters:

  • path string
Show/Hide Function Body
{
	for _, h := range app.routerHooks {
		h(path)
	}
}

TriggerStore function

TriggerStore invokes store hooks for a mutation.

Parameters:

  • module string
  • store string
  • key string
  • value any
Show/Hide Function Body
{
	for _, h := range app.storeHooks {
		h(module, store, key, value)
	}
}

TriggerTemplate function

TriggerTemplate invokes template hooks with rendered HTML for a component.

Parameters:

  • componentID string
  • html string
Show/Hide Function Body
{
	for _, h := range app.templateHooks {
		h(componentID, html)
	}
}

TriggerMount function

TriggerMount invokes mount lifecycle hooks.

Parameters:

  • c Component

References:

Show/Hide Function Body
{
	for _, h := range app.mountHooks {
		h(c)
	}
}

TriggerUnmount function

TriggerUnmount invokes unmount lifecycle hooks.

Parameters:

  • c Component

References:

Show/Hide Function Body
{
	for _, h := range app.unmountHooks {
		h(c)
	}
}

init function

Show/Hide Function Body
{
	state.StoreHook = TriggerStore
	dom.TemplateHook = TriggerTemplate
}

App struct

App is a stub holder for callbacks.

Methods:

RegisterRouter

RegisterRouter adds a router navigation hook.


Parameters:
  • fn func(string)

Show/Hide Method Body
{
	a.routerHooks = append(a.routerHooks, fn)
}

RegisterStore

RegisterStore adds a store mutation hook.


Parameters:
  • fn func(module, store, key string, value any)

Show/Hide Method Body
{
	a.storeHooks = append(a.storeHooks, fn)
}

RegisterTemplate

RegisterTemplate adds a template render hook.


Parameters:
  • fn func(componentID, html string)

Show/Hide Method Body
{
	a.templateHooks = append(a.templateHooks, fn)
}

RegisterLifecycle

RegisterLifecycle adds hooks for component mount and unmount.


Parameters:
  • mount func(Component)
  • unmount func(Component)

Show/Hide Method Body
{
	if mount != nil {
		a.mountHooks = append(a.mountHooks, mount)
	}
	if unmount != nil {
		a.unmountHooks = append(a.unmountHooks, unmount)
	}
}

RegisterRTMLVar

RegisterRTMLVar registers a value that can be referenced from RTML as

{plugin:NAME.VAR}.


Parameters:
  • plugin string
  • name string
  • val any

Show/Hide Method Body
{
	if a.pluginVars == nil {
		a.pluginVars = make(map[string]map[string]any)
	}
	if _, ok := a.pluginVars[plugin]; !ok {
		a.pluginVars[plugin] = make(map[string]any)
	}
	a.pluginVars[plugin][name] = val
}

HasPlugin

HasPlugin reports whether a plugin with the given name is installed.


Parameters:
  • name string

Returns:
  • bool

Show/Hide Method Body
{
	if a.plugins == nil {
		return false
	}
	_, ok := a.plugins[name]
	return ok
}

RegisterRouter


Parameters:
  • fn func(string)

Show/Hide Method Body
{}

RegisterStore


Parameters:
  • fn func(module, store, key string, value any)

Show/Hide Method Body
{}

RegisterLifecycle


Parameters:
  • mount func(Component)
  • unmount func(Component)

Show/Hide Method Body
{}

RegisterTemplate


Parameters:
  • fn func(componentID, html string)

Show/Hide Method Body
{}

RegisterRTMLVar


Parameters:
  • plugin string
  • name string
  • val any

Show/Hide Method Body
{}

HasPlugin


Parameters:
  • name string

Returns:
  • bool

Show/Hide Method Body
{ return false }

RegisterPlugin function

Parameters:

  • p Plugin

References:

Show/Hide Function Body
{}

TriggerRouter function

Parameters:

  • path string
Show/Hide Function Body
{}

TriggerStore function

Parameters:

  • module string
  • store string
  • key string
  • value any
Show/Hide Function Body
{}

TriggerMount function

Parameters:

  • c Component

References:

Show/Hide Function Body
{}

TriggerUnmount function

Parameters:

  • c Component

References:

Show/Hide Function Body
{}

TriggerTemplate function

Parameters:

  • componentID string
  • html string
Show/Hide Function Body
{}

RegisterPluginVar function

Parameters:

  • plugin string
  • name string
  • val any
Show/Hide Function Body
{}

noopComponent struct

Implements:

  • Component from core

Methods:

Render


Returns:
  • string

Show/Hide Method Body
{ return "" }

Mount


Show/Hide Method Body
{}

Unmount


Show/Hide Method Body
{}

OnMount


Show/Hide Method Body
{}

OnUnmount


Show/Hide Method Body
{}

GetName


Returns:
  • string

Show/Hide Method Body
{ return "noop" }

GetID


Returns:
  • string

Show/Hide Method Body
{ return "noop" }

SetSlots


Parameters:
  • map[string]any

Show/Hide Method Body
{}

TestRegisterComponentDuplicate function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	// reset registry
	ComponentRegistry = map[string]func() Component{}
	if err := RegisterComponent("dup", func() Component { return noopComponent{} }); err != nil {
		t.Fatalf("unexpected error registering component: %v", err)
	}
	if err := RegisterComponent("dup", func() Component { return noopComponent{} }); err == nil {
		t.Fatalf("expected error on duplicate registration")
	}
}

TestMustRegisterComponentPanic function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	ComponentRegistry = map[string]func() Component{}
	MustRegisterComponent("dup", func() Component { return noopComponent{} })
	defer func() {
		if r := recover(); r == nil {
			t.Fatalf("expected panic on duplicate registration")
		}
	}()
	MustRegisterComponent("dup", func() Component { return noopComponent{} })
}

devPayload struct

Fields:

  • Type (string) - json:"type"
  • Component (string) - json:"component,omitempty"
  • Markup (string) - json:"markup,omitempty"

startDevTemplateWatcher function

Show/Hide Function Body
{
	devWatcherOnce.Do(func() {
		if js.Get("EventSource").Type() != js.TypeFunction {
			log.Printf("rfw: EventSource not available, auto reload disabled")
			return
		}
		devMsgHandler = js.FuncOf(func(this js.Value, args []js.Value) any {
			if len(args) == 0 {
				return nil
			}
			data := args[0].Get("data").String()
			devHandleDevMessage(data)
			return nil
		})
		devErrHandler = js.FuncOf(func(this js.Value, args []js.Value) any {
			params := make([]any, 0, len(args)+1)
			params = append(params, "rfw dev watcher error")
			for _, a := range args {
				params = append(params, a)
			}
			js.Console().Call("warn", params...)
			return nil
		})
		devEventSource = js.Get("EventSource").New("/__rfw/hmr")
		if !devEventSource.Truthy() {
			return
		}
		if devMsgHandler.Truthy() {
			devEventSource.Set("onmessage", devMsgHandler.Value)
			devEventSource.Call("addEventListener", "message", devMsgHandler.Value)
		}
		if devErrHandler.Truthy() {
			devEventSource.Set("onerror", devErrHandler.Value)
			devEventSource.Call("addEventListener", "error", devErrHandler.Value)
		}
	})
}

devHandleDevMessage function

Parameters:

  • raw string
Show/Hide Function Body
{
	var msg devPayload
	if err := json.Unmarshal([]byte(raw), &msg); err != nil {
		log.Printf("rfw: failed to parse dev message: %v", err)
		return
	}
	switch msg.Type {
	case "reload":
		js.Location().Call("reload")
	case "rtml":
		devApplyTemplateUpdate(msg.Component, msg.Markup)
	}
}

devApplyTemplateUpdate function

Parameters:

  • name string
  • markup string
Show/Hide Function Body
{
	if !DevMode || name == "" || markup == "" {
		return
	}
	devMu.Lock()
	devTemplateOverrides[name] = markup
	components := make([]*HTMLComponent, 0, len(devComponents[name]))
	for _, cmp := range devComponents[name] {
		components = append(components, cmp)
	}
	devMu.Unlock()

	dom.OverrideBindings(name, markup)
	for _, cmp := range components {
		cmp.Template = markup
		cmp.TemplateFS = []byte(markup)
		cmp.cache = nil
		cmp.lastCacheKey = ""
		dom.RegisterBindings(cmp.ID, cmp.Name, markup)
		html := cmp.Render()
		dom.UpdateDOM(cmp.ID, html)
	}
}

devOverrideTemplate function

Parameters:

  • c *HTMLComponent
  • template string

Returns:

  • string
Show/Hide Function Body
{
	if !DevMode {
		return template
	}
	devMu.RLock()
	override, ok := devTemplateOverrides[c.Name]
	devMu.RUnlock()
	if !ok || override == "" {
		return template
	}
	dom.OverrideBindings(c.Name, override)
	c.TemplateFS = []byte(override)
	return override
}

devRegisterComponent function

Parameters:

  • c *HTMLComponent
Show/Hide Function Body
{
	if !DevMode {
		return
	}
	devMu.Lock()
	defer devMu.Unlock()
	bucket, ok := devComponents[c.Name]
	if !ok {
		bucket = make(map[string]*HTMLComponent)
		devComponents[c.Name] = bucket
	}
	bucket[c.ID] = c
}

devUnregisterComponent function

Parameters:

  • c *HTMLComponent
Show/Hide Function Body
{
	if !DevMode {
		return
	}
	devMu.Lock()
	defer devMu.Unlock()
	if bucket, ok := devComponents[c.Name]; ok {
		delete(bucket, c.ID)
		if len(bucket) == 0 {
			delete(devComponents, c.Name)
		}
	}
}

namedTestPlugin struct

Fields:

  • installed (int)

Implements:

  • Named from core

Methods:

Build


Parameters:
  • json.RawMessage

Returns:
  • error

Show/Hide Method Body
{ return nil }

Install


Parameters:
  • a *App

Show/Hide Method Body
{ p.installed++ }

Name


Returns:
  • string

Show/Hide Method Body
{ return "named-test" }

TestRegisterPlugin_dedup function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	app = newApp()
	p1 := &namedTestPlugin{}
	RegisterPlugin(p1)
	if p1.installed != 1 {
		t.Fatalf("expected first plugin to install once, got %d", p1.installed)
	}
	p2 := &namedTestPlugin{}
	RegisterPlugin(p2)
	if p2.installed != 0 {
		t.Fatalf("expected second plugin not to install, got %d", p2.installed)
	}
	if !app.HasPlugin("named-test") {
		t.Fatalf("expected HasPlugin to return true")
	}
}

depPlugin struct

Fields:

  • installed (int)

Implements:

  • Named from core

Methods:

Build


Parameters:
  • json.RawMessage

Returns:
  • error

Show/Hide Method Body
{ return nil }

Install


Parameters:
  • a *App

Show/Hide Method Body
{ p.installed++ }

Name


Returns:
  • string

Show/Hide Method Body
{ return "dep" }

requiresPlugin struct

Fields:

  • dep (*depPlugin)

Implements:

  • Requires from core
  • Named from core

Methods:

Build


Parameters:
  • json.RawMessage

Returns:
  • error

Show/Hide Method Body
{ return nil }

Install


Parameters:
  • a *App

Show/Hide Method Body
{}

Name


Returns:
  • string

Show/Hide Method Body
{ return "requires" }

Requires


Returns:
  • []Plugin

Show/Hide Method Body
{ return []Plugin{p.dep} }

TestRegisterPlugin_requires function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	app = newApp()
	dep := &depPlugin{}
	req := &requiresPlugin{dep: dep}
	RegisterPlugin(req)
	if dep.installed != 1 {
		t.Fatalf("expected dependency to install, got %d", dep.installed)
	}
	if !app.HasPlugin("dep") || !app.HasPlugin("requires") {
		t.Fatalf("expected both plugins to be registered")
	}
}

optionalPlugin struct

Fields:

  • dep (*depPlugin)
  • enable (bool)

Implements:

  • Named from core
  • Optional from core

Methods:

Build


Parameters:
  • json.RawMessage

Returns:
  • error

Show/Hide Method Body
{ return nil }

Install


Parameters:
  • a *App

Show/Hide Method Body
{}

Name


Returns:
  • string

Show/Hide Method Body
{ return "optional" }

Optional


Returns:
  • []Plugin

Show/Hide Method Body
{
	if !p.enable {
		return nil
	}
	return []Plugin{p.dep}
}

TestRegisterPlugin_optional function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	app = newApp()
	dep := &depPlugin{}
	opt := &optionalPlugin{dep: dep, enable: true}
	RegisterPlugin(opt)
	if dep.installed != 1 {
		t.Fatalf("expected optional dependency to install")
	}

	app = newApp()
	dep2 := &depPlugin{}
	opt2 := &optionalPlugin{dep: dep2, enable: false}
	RegisterPlugin(opt2)
	if dep2.installed != 0 {
		t.Fatalf("expected disabled optional dependency not to install")
	}
}

TestElseIfRendering function

Tests complex conditional scenarios including @else-if and nested blocks.

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	c := &HTMLComponent{Props: map[string]any{"val": "2"}, conditionContents: make(map[string]ConditionContent)}
	template := `
@if:prop:val=="1"
One
@else-if:prop:val=="2"
Two
@else
Other
@endif`

	out := replaceConditionals(template, c)
	if strings.Contains(out, "One") || strings.Contains(out, "Other") {
		t.Fatalf("unexpected branches rendered: %s", out)
	}
	if !strings.Contains(out, "Two") {
		t.Fatalf("expected 'Two' branch, got %s", out)
	}
}

TestNestedConditionals function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	props := map[string]any{"outer": "yes", "inner": "maybe"}
	c := &HTMLComponent{Props: props, conditionContents: make(map[string]ConditionContent)}
	template := `
@if:prop:outer=="yes"
Start
    @if:prop:inner=="yes"
        InnerYes
    @else-if:prop:inner=="maybe"
        InnerMaybe
    @else
        InnerNo
    @endif
@else
    OuterNo
@endif`

	out := replaceConditionals(template, c)
	if !strings.Contains(out, "Start") || !strings.Contains(out, "InnerMaybe") {
		t.Fatalf("nested conditions not rendered as expected: %s", out)
	}
	if strings.Contains(out, "InnerYes") || strings.Contains(out, "InnerNo") || strings.Contains(out, "OuterNo") {
		t.Fatalf("unexpected branches present: %s", out)
	}
}

TestReplaceConstructors function

Tests constructor decorators for refs and keyed lists.

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	tpl := `<div [header] class="box"></div>`
	out := replaceConstructors(tpl)
	if out != `<div class="box" data-ref="header"></div>` {
		t.Fatalf("unexpected constructor replacement: %s", out)
	}

	tpl = `<li [key {item.ID}]></li>`
	out = replaceConstructors(tpl)
	if out != `<li data-key="{item.ID}"></li>` {
		t.Fatalf("expected data-key constructor, got %s", out)
	}
}

TestPluginPlaceholders function

Tests plugin placeholders for variables, commands and constructors.

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	RegisterPluginVar("soccer", "team", "lions")
	tpl := `<div @plugin:soccer.init>{plugin:soccer.team}</div>`
	out := replacePluginPlaceholders(tpl)
	if !strings.Contains(out, "lions") {
		t.Fatalf("plugin variable not replaced: %s", out)
	}
	if !strings.Contains(out, `data-plugin-cmd="soccer.init"`) {
		t.Fatalf("plugin command not replaced: %s", out)
	}

	tpl = `<span [plugin:soccer.badge]></span>`
	out = replaceConstructors(tpl)
	if !strings.Contains(out, `data-plugin="soccer.badge"`) {
		t.Fatalf("plugin constructor not replaced: %s", out)
	}
}

SetDevMode function

SetDevMode toggles development mode features.

Parameters:

  • enabled bool
Show/Hide Function Body
{
        DevMode = enabled
        if enabled {
                startDevTemplateWatcher()
        }
}

Component interface

Methods:

Render


Returns:
  • string

Mount

Unmount

OnMount

OnUnmount

GetName


Returns:
  • string

GetID


Returns:
  • string

SetSlots


Parameters:
  • map[string]any

RegisterComponent function

RegisterComponent registers a component constructor under the provided name.

When a template references the name via `rt-is`, the constructor will be

invoked to create a new component instance at render time. It returns an

error if a component with the same name is already registered and logs a

warning.

Parameters:

  • name string
  • constructor func() Component

Returns:

  • error

References:

Show/Hide Function Body
{
	componentRegistryMu.Lock()
	defer componentRegistryMu.Unlock()
	if _, exists := ComponentRegistry[name]; exists {
		Log().Warn("component %s already registered", name)
		return fmt.Errorf("component %s already registered", name)
	}
	ComponentRegistry[name] = constructor
	return nil
}

LoadComponent function

LoadComponent retrieves a component by name using the registry. If no

component is registered under that name, nil is returned.

Parameters:

  • name string

Returns:

  • Component

References:

Show/Hide Function Body
{
	componentRegistryMu.RLock()
	ctor, ok := ComponentRegistry[name]
	componentRegistryMu.RUnlock()
	if ok {
		return ctor()
	}
	return nil
}

NewComponent function

NewComponent creates an HTMLComponent initialized with the provided

template and props. It sets itself as the underlying component and

performs initialization with the default store.

Parameters:

  • name string
  • templateFS []byte
  • props map[string]any

Returns:

  • *HTMLComponent
Show/Hide Function Body
{
	c := NewHTMLComponent(name, templateFS, props)
	c.SetComponent(c)
	c.Init(nil)
	return c
}

NewComponentWith function

NewComponentWith creates an HTMLComponent and binds it to the given

component implementation. This is useful when embedding HTMLComponent

inside another struct to override lifecycle hooks.

Parameters:

  • name string
  • templateFS []byte
  • props map[string]any
  • self T

Returns:

  • *HTMLComponent
Show/Hide Function Body
{
	c := NewHTMLComponent(name, templateFS, props)
	if any(self) != nil {
		c.SetComponent(self)
	} else {
		c.SetComponent(c)
	}
	c.Init(nil)
	return c
}

HTMLComponent struct

Methods:

Stats

Stats returns zeroed metrics on non-wasm builds.


Returns:
  • ComponentStats

References:


Show/Hide Method Body
{ return ComponentStats{} }

Init


Parameters:
  • store *state.Store

Show/Hide Method Body
{
	template, err := LoadComponentTemplate(c.TemplateFS)
	if err != nil {
		panic(fmt.Sprintf("Error loading template for component %s: %v", c.Name, err))
	}
	template = devOverrideTemplate(c, template)
	c.Template = template
	dom.RegisterBindings(c.ID, c.Name, template)
	devRegisterComponent(c)

	if store != nil {
		c.Store = store
	} else {
		c.Store = state.GlobalStoreManager.GetStore("app", "default")
		if c.Store == nil {
			panic(fmt.Sprintf("No store provided and no default store found for component %s", c.Name))
		}
	}
}

Render


Returns:
  • renderedTemplate string

Show/Hide Method Body
{
	start := time.Now()
	defer c.recordRender(time.Since(start))
	key := c.cacheKey()
	if c.cache != nil {
		if val, ok := c.cache[key]; ok {
			renderedTemplate = val
			return
		}
		if c.lastCacheKey != "" && c.lastCacheKey != key {
			delete(c.cache, c.lastCacheKey)
		}
	} else {
		c.cache = make(map[string]string)
	}
	defer func() {
		if r := recover(); r != nil {
			jsStack := js.Error().New().Get("stack").String()
			goStack := string(debug.Stack())
			panic(fmt.Sprintf("%v\nGo stack:\n%s\nJS stack:\n%s", r, goStack, jsStack))
		}
	}()

	c.unsubscribes.Run()

	renderedTemplate = c.Template
	renderedTemplate = strings.Replace(renderedTemplate, "<root", fmt.Sprintf("<root data-component-id=\"%s\"", c.ID), 1)

	// Extract slot contents destined for child components
	renderedTemplate = extractSlotContents(renderedTemplate, c)

	// Replace this component's slot placeholders with provided content or fallbacks
	renderedTemplate = replaceSlotPlaceholders(renderedTemplate, c)

	for key, value := range c.Props {
		placeholder := fmt.Sprintf("{{%s}}", key)
		renderedTemplate = strings.ReplaceAll(renderedTemplate, placeholder, fmt.Sprintf("%v", value))
	}

	// Register @include directives that supply inline props
	renderedTemplate = replaceComponentIncludes(renderedTemplate, c)

	// Handle @include:componentName syntax for dependencies
	renderedTemplate = replaceIncludePlaceholders(c, renderedTemplate)

	// Handle @for loops and legacy @foreach syntax
	renderedTemplate = replaceForPlaceholders(renderedTemplate, c)
	renderedTemplate = replaceForeachPlaceholders(renderedTemplate, c)

	// Handle @store:module.storeName.varName syntax.
	// Append :w for writable inputs; read-only inputs omit the suffix (:r is not supported).
	renderedTemplate = replaceStorePlaceholders(renderedTemplate, c)

	// Handle @signal:name syntax for local signals
	renderedTemplate = replaceSignalPlaceholders(renderedTemplate, c)

	// Handle @prop:propName syntax for props
	renderedTemplate = replacePropPlaceholders(renderedTemplate, c)

	// Handle plugin variable and command placeholders
	renderedTemplate = replacePluginPlaceholders(renderedTemplate)

	// Handle host variable and command placeholders
	if c.HostComponent != "" {
		renderedTemplate = replaceHostPlaceholders(renderedTemplate, c)
	}

	// Handle @if:condition syntax for conditional rendering
	renderedTemplate = replaceConditionals(renderedTemplate, c)

	// Handle @on:event:handler and @event:handler syntax for event binding
	renderedTemplate = replaceEventHandlers(renderedTemplate)

	// Handle rt-is="ComponentName" for dynamic component loading
	renderedTemplate = replaceRtIsAttributes(renderedTemplate, c)

	// Render any components introduced via rt-is placeholders
	renderedTemplate = replaceIncludePlaceholders(c, renderedTemplate)

	// Handle constructor decorators like [ref] and [key expr]
	renderedTemplate = replaceConstructors(renderedTemplate)

	if c.HostComponent != "" {
		hostclient.RegisterComponent(c.ID, c.HostComponent, c.hostVars)
	}

	renderedTemplate = minifyInline(renderedTemplate)

	c.cache[key] = renderedTemplate
	c.lastCacheKey = key
	return renderedTemplate
}

recordRender


Parameters:
  • duration time.Duration

Show/Hide Method Body
{
	if c == nil {
		return
	}
	c.metricsMu.Lock()
	c.renderCount++
	c.totalRender += duration
	c.lastRender = duration
	c.appendTimelineLocked(ComponentTimelineEntry{
		Kind:      "render",
		Timestamp: time.Now(),
		Duration:  duration,
	})
	c.metricsMu.Unlock()
}

appendTimelineLocked


Parameters:
  • entry ComponentTimelineEntry

References:


Show/Hide Method Body
{
	if entry.Kind == "" {
		return
	}
	if c.timeline == nil {
		c.timeline = make([]ComponentTimelineEntry, 0, 8)
	}
	c.timeline = append(c.timeline, entry)
	if len(c.timeline) > componentTimelineLimit {
		c.timeline = append([]ComponentTimelineEntry(nil), c.timeline[len(c.timeline)-componentTimelineLimit:]...)
	}
}

Stats

Stats returns a snapshot of the component's render metrics.


Returns:
  • ComponentStats

References:


Show/Hide Method Body
{
	c.metricsMu.Lock()
	defer c.metricsMu.Unlock()
	stats := ComponentStats{
		RenderCount: c.renderCount,
		TotalRender: c.totalRender,
		LastRender:  c.lastRender,
	}
	if c.renderCount > 0 {
		stats.AverageRender = c.totalRender / time.Duration(c.renderCount)
	}
	if len(c.timeline) > 0 {
		stats.Timeline = append(stats.Timeline, c.timeline...)
	}
	return stats
}

AddDependency


Parameters:
  • placeholderName string
  • dep Component

References:


Show/Hide Method Body
{
	if c.Dependencies == nil {
		c.Dependencies = make(map[string]Component)
	}
	if depComp, ok := dep.(*HTMLComponent); ok {
		depComp.Init(c.Store)
		depComp.parent = c
	}
	c.Dependencies[placeholderName] = dep
}

Unmount


Show/Hide Method Body
{
	devUnregisterComponent(c)
	if c.component != nil {
		c.component.OnUnmount()
	}

	dom.RemoveEventListeners(c.ID)
	dom.RemoveComponentSignals(c.ID)
	log.Printf("Unsubscribing %s from all stores", c.Name)
	c.unsubscribes.Run()

	for _, dep := range c.Dependencies {
		dep.Unmount()
	}
}

Mount


Show/Hide Method Body
{
	for _, dep := range c.Dependencies {
		dep.Mount()
	}
	if c.component != nil {
		c.component.OnMount()
	}
}

GetName


Returns:
  • string

Show/Hide Method Body
{
	return c.Name
}

GetID


Returns:
  • string

Show/Hide Method Body
{
	return c.ID
}

GetRef

GetRef returns the DOM element annotated with a matching constructor

decorator. It searches within this component's root element using the

data-ref attribute injected during template rendering.


Parameters:
  • name string

Returns:
  • dom.Element

Show/Hide Method Body
{
	doc := dom.Doc()
	var root dom.Element
	if c.ID == "" {
		root = doc.ByID("app")
	} else {
		root = doc.Query(fmt.Sprintf("[data-component-id='%s']", c.ID))
	}
	if root.IsNull() || root.IsUndefined() {
		return dom.Element{}
	}
	return root.Query(fmt.Sprintf(`[data-ref="%s"]`, name))
}

OnMount


Show/Hide Method Body
{
	if c.onMount != nil {
		c.onMount(c)
	}
}

OnUnmount


Show/Hide Method Body
{
	if c.onUnmount != nil {
		c.onUnmount(c)
	}
}

SetOnMount


Parameters:
  • fn func(*HTMLComponent)

Show/Hide Method Body
{
	c.onMount = fn
}

SetOnUnmount


Parameters:
  • fn func(*HTMLComponent)

Show/Hide Method Body
{
	c.onUnmount = fn
}

WithLifecycle


Parameters:
  • onMount func(*HTMLComponent)
  • onUnmount func(*HTMLComponent)

Returns:
  • *HTMLComponent

Show/Hide Method Body
{
	c.onMount = onMount
	c.onUnmount = onUnmount
	return c
}

SetComponent


Parameters:
  • component Component

References:


Show/Hide Method Body
{
	c.component = component
}

SetSlots


Parameters:
  • slots map[string]any

Show/Hide Method Body
{
	if c.Slots == nil {
		c.Slots = make(map[string]any)
	}
	for k, v := range slots {
		c.Slots[k] = v
	}
}

Provide

Provide stores a value on this component so that descendants can

retrieve it with Inject. It creates the map on first use.


Parameters:
  • key string
  • val any

Show/Hide Method Body
{
	if c.provides == nil {
		c.provides = make(map[string]any)
	}
	c.provides[key] = val
}

Inject

Inject searches for a provided value starting from this component and

walking up the parent chain. It returns the value as `any` and whether it

was found. Callers can type-assert the result.


Parameters:
  • key string

Returns:
  • any
  • bool

Show/Hide Method Body
{
	if c.provides != nil {
		if v, ok := c.provides[key]; ok {
			return v, true
		}
	}
	if c.parent != nil {
		return c.parent.Inject(key)
	}
	return nil, false
}

SetRouteParams


Parameters:
  • params map[string]string

Show/Hide Method Body
{
	if c.Props == nil {
		c.Props = make(map[string]any)
	}
	for k, v := range params {
		c.Props[k] = v
	}
}

AddHostComponent

AddHostComponent links this HTML component to a server-side HostComponent

by name. When running in SSC mode, messages from the wasm runtime will be

routed to the corresponding host component on the server.


Parameters:
  • name string

Show/Hide Method Body
{
	c.HostComponent = name
}

cacheKey


Returns:
  • string

Show/Hide Method Body
{
	hasher := sha1.New()
	hasher.Write([]byte(serializeProps(c.Props)))

	if len(c.Dependencies) > 0 {
		deps := make([]string, 0, len(c.Dependencies))
		for name, dep := range c.Dependencies {
			deps = append(deps, name+dep.GetID())
		}
		sort.Strings(deps)
		for _, d := range deps {
			hasher.Write([]byte(d))
		}
	}

	return hex.EncodeToString(hasher.Sum(nil))
}

startDevTemplateWatcher function

Show/Hide Function Body
{}

devApplyTemplateUpdate function

Parameters:

  • string
  • string
Show/Hide Function Body
{}

devOverrideTemplate function

Parameters:

  • c *HTMLComponent
  • template string

Returns:

  • string
Show/Hide Function Body
{ return template }

devRegisterComponent function

Parameters:

  • *HTMLComponent
Show/Hide Function Body
{}

devUnregisterComponent function

Parameters:

  • *HTMLComponent
Show/Hide Function Body
{}

Logger interface

Logger defines logging interface used by the framework.

Methods:

Debug


Parameters:
  • format string
  • v ...any

Info


Parameters:
  • format string
  • v ...any

Warn


Parameters:
  • format string
  • v ...any

Error


Parameters:
  • format string
  • v ...any

init function

Show/Hide Function Body
{ state.SetLogger(logger) }

SetLogger function

SetLogger allows applications to replace the default logger.

Parameters:

  • l Logger

References:

Show/Hide Function Body
{
	if l != nil {
		logger = l
		state.SetLogger(l)
	}
}

Log function

Log returns the active logger implementation.

Returns:

  • Logger

References:

Show/Hide Function Body
{ return logger }

defaultLogger struct

defaultLogger is the fallback logger using the standard log package.

Implements:

  • Logger from core

Methods:

Debug


Parameters:
  • format string
  • v ...any

Show/Hide Method Body
{ log.Printf("DEBUG: "+format, v...) }

Info


Parameters:
  • format string
  • v ...any

Show/Hide Method Body
{ log.Printf("INFO: "+format, v...) }

Warn


Parameters:
  • format string
  • v ...any

Show/Hide Method Body
{ log.Printf("WARN: "+format, v...) }

Error


Parameters:
  • format string
  • v ...any

Show/Hide Method Body
{ log.Printf("ERROR: "+format, v...) }

TestProvideInject function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	state.NewStore("default", state.WithModule("app"))

	parentTpl := []byte("<root></root>")
	childTpl := []byte("<root></root>")

	parent := NewComponent("Parent", parentTpl, nil)
	child := NewComponent("Child", childTpl, nil)

	parent.Provide("answer", 42)
	parent.AddDependency("child", child)

	v, ok := Inject[int](child, "answer")
	if !ok || v != 42 {
		t.Fatalf("expected injected 42, got %v", v)
	}
}

Node interface

AST structures for template parsing

Methods:

Render


Parameters:
  • c *HTMLComponent

Returns:
  • string

TextNode struct

Fields:

  • Text (string)

Implements:

  • Node from core

Methods:

Render


Parameters:
  • c *HTMLComponent

Returns:
  • string

Show/Hide Method Body
{ return t.Text }

ConditionalBranch struct

Fields:

  • Condition (string)
  • Nodes ([]Node)

ConditionalNode struct

Fields:

  • Branches ([]ConditionalBranch)

Implements:

  • Node from core

Methods:

Render

Render evaluates the conditional branches and renders the appropriate content.


Parameters:
  • c *HTMLComponent

Returns:
  • string

Show/Hide Method Body
{
	var conditions []string
	for _, br := range cn.Branches {
		conditions = append(conditions, br.Condition)
	}
	conditionID := fmt.Sprintf("cond-%x", sha1.Sum([]byte(strings.Join(conditions, "|"))))

	var content ConditionContent
	var chosen string
	for _, br := range cn.Branches {
		var sb strings.Builder
		for _, n := range br.Nodes {
			sb.WriteString(n.Render(c))
		}
		branchContent := sb.String()
		content.Branches = append(content.Branches, ConditionalBranchContent{Condition: br.Condition, Content: branchContent})

		if br.Condition != "" {
			result, dependencies := evaluateCondition(br.Condition, c)
			for _, dep := range dependencies {
				if dep.signal != "" {
					if prop, ok := c.Props[dep.signal]; ok {
						if sig, ok := prop.(interface{ Read() any }); ok {
							dom.RegisterSignal(c.ID, dep.signal, sig)
							unsub := state.Effect(func() func() {
								sig.Read()
								updateConditionBindings(c, conditionID)
								return nil
							})
							c.unsubscribes.Add(unsub)
						}
					}
					continue
				}
				module, storeName, key := dep.module, dep.storeName, dep.key
				store := state.GlobalStoreManager.GetStore(module, storeName)
				if store != nil {
					unsubscribe := store.OnChange(key, func(newValue any) {
						updateConditionBindings(c, conditionID)
					})
					c.unsubscribes.Add(unsubscribe)
				}
			}
			if chosen == "" && result {
				chosen = branchContent
			}
		} else if chosen == "" {
			chosen = branchContent
		}
	}

	c.conditionContents[conditionID] = content
	return fmt.Sprintf(`<div data-condition="%s">%s</div>`, conditionID, chosen)
}

ConditionalBranchContent struct

ConditionContent stores rendered content for each branch of a conditional block

Fields:

  • Condition (string)
  • Content (string)

ConditionContent struct

Fields:

  • Branches ([]ConditionalBranchContent)

ConditionDependency struct

Fields:

  • module (string)
  • storeName (string)
  • key (string)
  • signal (string)

ForeachConfig struct

Fields:

  • Expr (string)
  • ItemAlias (string)
  • Content (string)

replaceIncludePlaceholders function

Parameters:

  • c *HTMLComponent
  • renderedTemplate string

Returns:

  • string
Show/Hide Function Body
{
	includeRegex := regexp.MustCompile(`@include:([\w-]+)`)
	return includeRegex.ReplaceAllStringFunc(renderedTemplate, func(match string) string {
		name := includeRegex.FindStringSubmatch(match)[1]
		if dep, ok := c.Dependencies[name]; ok {
			return dep.Render()
		}
		if DevMode {
			Log().Warn("component %s missing dependency '%s'", c.Name, name)
		}
		return match
	})
}

replaceComponentIncludes function

replaceComponentIncludes scans for @include directives that supply inline

props using the syntax @include:Component:{key:"value"}. Matching includes

are replaced with standard @include placeholders after instantiating the

component and registering it as a dependency.

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	idx := 0

	// Handle includes that may be wrapped in <p> tags produced by Markdown
	// renderers as well as bare @include directives.
	patterns := []string{
		`<p>@include:([\w-]+):\{([^}]*)\}</p>`,
		`@include:([\w-]+):\{([^}]*)\}`,
	}

	for _, pat := range patterns {
		re := regexp.MustCompile(pat)
		template = re.ReplaceAllStringFunc(template, func(match string) string {
			parts := re.FindStringSubmatch(match)
			if len(parts) < 3 {
				return match
			}
			name := parts[1]
			propStr := html.UnescapeString(parts[2])
			comp := LoadComponent(name)
			if comp == nil {
				if DevMode {
					Log().Warn("include referenced unknown component '%s'", name)
				}
				return match
			}
			props := map[string]any{}
			propRe := regexp.MustCompile(`(\w+):"([^"]*)"`)
			for _, m := range propRe.FindAllStringSubmatch(propStr, -1) {
				props[m[1]] = m[2]
			}
			if hc, ok := comp.(*HTMLComponent); ok {
				hc.Props = props
				hc.ID = generateComponentID(hc.Name, hc.Props)
			}
			placeholder := fmt.Sprintf("inc-%s-%d", name, idx)
			idx++
			c.AddDependency(placeholder, comp)
			return "@include:" + placeholder
		})
	}

	return template
}

extractSlotContents function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	slotRegex := regexp.MustCompile(`@slot:(\w+)(?:\.(\w+))?([\s\S]*?)@endslot`)
	return slotRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := slotRegex.FindStringSubmatch(match)
		if len(parts) < 4 {
			return match
		}
		depName := parts[1]
		slotName := parts[2]
		if slotName == "" {
			slotName = "default"
		}
		content := parts[3]
		if dep, ok := c.Dependencies[depName]; ok {
			dep.SetSlots(map[string]any{slotName: content})
			return ""
		}
		if DevMode {
			Log().Warn("component %s missing dependency '%s' for slot '%s'", c.Name, depName, slotName)
		}
		return match
	})
}

replaceSlotPlaceholders function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	slotRegex := regexp.MustCompile(`@slot(?::(\w+))?([\s\S]*?)@endslot`)
	idx := 0
	return slotRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := slotRegex.FindStringSubmatch(match)
		if len(parts) < 3 {
			return match
		}
		slotName := parts[1]
		if slotName == "" {
			slotName = "default"
		}
		fallback := parts[2]
		if content, ok := c.Slots[slotName]; ok {
			switch v := content.(type) {
			case string:
				return v
			case Component:
				placeholder := fmt.Sprintf("slot-%s-%d", slotName, idx)
				idx++
				c.AddDependency(placeholder, v)
				return fmt.Sprintf("@include:%s", placeholder)
			default:
				return fallback
			}
		}
		return fallback
	})
}

replaceStorePlaceholders function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	storeRegex := regexp.MustCompile(`@store:(\w+)\.(\w+)\.(\w+)(:w)?`)
	return storeRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := storeRegex.FindStringSubmatch(match)
		if len(parts) < 4 {
			return match
		}

		module := parts[1]
		storeName := parts[2]
		key := parts[3]
		isWriteable := len(parts) == 5 && parts[4] == ":w"

		store := state.GlobalStoreManager.GetStore(module, storeName)
		if store != nil {
			value := store.Get(key)
			if value == nil {
				value = ""
			}

			unsubscribe := store.OnChange(key, func(newValue any) {
				updateStoreBindings(c, module, storeName, key, newValue)
			})
			c.unsubscribes.Add(unsubscribe)

			if isWriteable {
				return match
			} else {
				return fmt.Sprintf(`<span data-store="%s.%s.%s">%v</span>`, module, storeName, key, value)
			}
		}
		if DevMode {
			Log().Warn("store %s.%s not found for key '%s' in component %s", module, storeName, key, c.Name)
		}
		return match
	})
}

replaceSignalPlaceholders function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	sigRegex := regexp.MustCompile(`@signal:(\w+)(:w)?`)
	return sigRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := sigRegex.FindStringSubmatch(match)
		if len(parts) < 2 {
			return match
		}
		name := parts[1]
		isWriteable := len(parts) == 3 && parts[2] == ":w"
		if prop, ok := c.Props[name]; ok {
			if sig, ok := prop.(interface{ Read() any }); ok {
				dom.RegisterSignal(c.ID, name, sig)
				val := sig.Read()
				unsub := state.Effect(func() func() {
					v := sig.Read()
					updateSignalBindings(c, name, v)
					return nil
				})
				c.unsubscribes.Add(unsub)
				if isWriteable {
					return match
				}
				return fmt.Sprintf(`<span data-signal="%s">%v</span>`, name, val)
			}
		}
		if DevMode {
			Log().Warn("signal '%s' not found in component %s", name, c.Name)
		}
		return match
	})
}

replacePropPlaceholders function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	propRegex := regexp.MustCompile(`@prop:(\w+)`)
	idx := 0
	return propRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := propRegex.FindStringSubmatch(match)
		if len(parts) != 2 {
			return match
		}
		propName := parts[1]
		if value, exists := c.Props[propName]; exists {
			switch v := value.(type) {
			case Component:
				placeholder := fmt.Sprintf("prop-%s-%d", propName, idx)
				idx++
				c.AddDependency(placeholder, v)
				return fmt.Sprintf("@include:%s", placeholder)
			default:
				return fmt.Sprintf("%v", v)
			}
		}
		if DevMode {
			Log().Warn("component %s missing prop '%s'", c.Name, propName)
		}
		return match
	})
}

replacePluginPlaceholders function

Parameters:

  • template string

Returns:

  • string
Show/Hide Function Body
{
	varRegex := regexp.MustCompile(`\{plugin:(\w+)\.(\w+)\}`)
	template = varRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := varRegex.FindStringSubmatch(match)
		if len(parts) != 3 {
			return match
		}
		plug, name := parts[1], parts[2]
		if v, ok := getRTMLVar(plug, name); ok {
			return fmt.Sprintf("%v", v)
		}
		if DevMode {
			Log().Warn("plugin variable %s.%s not found", plug, name)
		}
		return match
	})
	cmdRegex := regexp.MustCompile(`@plugin:(\w+)\.(\w+)([\s>/])`)
	template = cmdRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := cmdRegex.FindStringSubmatch(match)
		if len(parts) != 4 {
			return match
		}
		plug, name, suffix := parts[1], parts[2], parts[3]
		return fmt.Sprintf(`data-plugin-cmd="%s.%s"%s`, plug, name, suffix)
	})
	return template
}

replaceHostPlaceholders function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	varRegex := regexp.MustCompile(`\{h:(\w+)\}`)
	template = varRegex.ReplaceAllStringFunc(template, func(match string) string {
		name := varRegex.FindStringSubmatch(match)[1]
		c.hostVars = append(c.hostVars, name)

		expectedVal := ""
		if c.Props != nil {
			if v, ok := c.Props[name]; ok {
				expectedVal = fmt.Sprintf("%v", v)
			} else if v, ok := c.Props["h:"+name]; ok {
				expectedVal = fmt.Sprintf("%v", v)
			}
		}

		hash := sha1.Sum([]byte(expectedVal))
		expectedAttr := fmt.Sprintf("sha1:%x", hash)

		return fmt.Sprintf(`<span data-host-var="%s" data-host-expected="%s">%s</span>`,
			name, expectedAttr, html.EscapeString(expectedVal))
	})
	cmdRegex := regexp.MustCompile(`@h:(\w+)`)
	template = cmdRegex.ReplaceAllStringFunc(template, func(match string) string {
		name := cmdRegex.FindStringSubmatch(match)[1]
		c.hostCmds = append(c.hostCmds, name)
		return fmt.Sprintf(`data-host-cmd="%s"`, name)
	})
	return template
}

replaceEventHandlers function

Parameters:

  • template string

Returns:

  • string
Show/Hide Function Body
{
	// Match event directives ensuring they are terminated by whitespace,
	// a self-closing slash or the end of the tag. The terminating
	// character is captured so it can be preserved in the replacement.
	eventRegex := regexp.MustCompile(`@(on:)?(\w+(?:\.\w+)*):(\w+)([\s>/])`)
	return eventRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := eventRegex.FindStringSubmatch(match)
		if len(parts) != 5 {
			return match
		}
		fullEvent := parts[2]
		handler := parts[3]
		suffix := parts[4]
		eventParts := strings.Split(fullEvent, ".")
		event := eventParts[0]
		modifiers := []string{}
		if len(eventParts) > 1 {
			modifiers = eventParts[1:]
		}
		attr := fmt.Sprintf("data-on-%s=\"%s\"", event, handler)
		if len(modifiers) > 0 {
			attr += fmt.Sprintf(" data-on-%s-modifiers=\"%s\"", event, strings.Join(modifiers, ","))
		}
		return attr + suffix
	})
}

replaceRtIsAttributes function

replaceRtIsAttributes scans the template for elements decorated with the

`rt-is` attribute. The attribute's value identifies a component registered in

the ComponentRegistry. Matching elements are replaced with an @include

placeholder so standard include processing can render the referenced

component and manage its lifecycle.

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	re := regexp.MustCompile(`<([a-zA-Z0-9]+)([^>]*)rt-is="([^"]+)"[^>]*/?>`)
	idx := 0
	return re.ReplaceAllStringFunc(template, func(match string) string {
		parts := re.FindStringSubmatch(match)
		if len(parts) < 4 {
			return match
		}
		name := parts[3]
		comp := LoadComponent(name)
		if comp == nil {
			if DevMode {
				Log().Warn("rt-is referenced unknown component '%s'", name)
			}
			return match
		}
		placeholder := fmt.Sprintf("rtis-%s-%d", name, idx)
		idx++
		c.AddDependency(placeholder, comp)
		return fmt.Sprintf("@include:%s", placeholder)
	})
}

parseTemplate function

parseTemplate parses the template string into an AST of nodes.

Parameters:

  • template string

Returns:

  • []Node
  • error
Show/Hide Function Body
{
	lines := strings.Split(template, "\n")
	idx := 0
	return parseBlock(lines, &idx)
}

parseBlock function

Parameters:

  • lines []string
  • idx *int

Returns:

  • []Node
  • error
Show/Hide Function Body
{
	var nodes []Node
	for *idx < len(lines) {
		line := lines[*idx]
		trimmed := strings.TrimSpace(line)
		switch {
		case strings.HasPrefix(trimmed, "@if:"):
			cond := trimmed
			*idx++
			n, err := parseConditional(lines, idx, cond)
			if err != nil {
				return nil, err
			}
			nodes = append(nodes, n)
		case strings.HasPrefix(trimmed, "@else-if:"), trimmed == "@else", trimmed == "@endif":
			return nodes, nil
		default:
			nodes = append(nodes, &TextNode{Text: line + "\n"})
			*idx++
		}
	}
	return nodes, nil
}

parseConditional function

Parameters:

  • lines []string
  • idx *int
  • firstCond string

Returns:

  • Node
  • error

References:

Show/Hide Function Body
{
	node := &ConditionalNode{}
	children, err := parseBlock(lines, idx)
	if err != nil {
		return nil, err
	}
	node.Branches = append(node.Branches, ConditionalBranch{Condition: firstCond, Nodes: children})

	for *idx < len(lines) {
		trimmed := strings.TrimSpace(lines[*idx])
		switch {
		case strings.HasPrefix(trimmed, "@else-if:"):
			cond := trimmed
			*idx++
			children, err := parseBlock(lines, idx)
			if err != nil {
				return nil, err
			}
			node.Branches = append(node.Branches, ConditionalBranch{Condition: cond, Nodes: children})
		case trimmed == "@else":
			*idx++
			children, err := parseBlock(lines, idx)
			if err != nil {
				return nil, err
			}
			node.Branches = append(node.Branches, ConditionalBranch{Condition: "", Nodes: children})
		case trimmed == "@endif":
			*idx++
			return node, nil
		default:
			*idx++
		}
	}
	return node, nil
}

replaceConditionals function

replaceConditionals parses conditionals using the AST and renders them.

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	nodes, err := parseTemplate(template)
	if err != nil {
		return template
	}
	var sb strings.Builder
	for _, n := range nodes {
		sb.WriteString(n.Render(c))
	}
	return sb.String()
}

evaluateCondition function

Parameters:

  • condition string
  • c *HTMLComponent

Returns:

  • bool
  • []ConditionDependency
Show/Hide Function Body
{
	Log().Debug("Evaluating condition: '%s'", condition)
	conditionParts := strings.Split(condition, "==")
	if len(conditionParts) != 2 {
		Log().Debug("Condition format is invalid. Expected '=='.")
		return false, nil
	}

	leftSide := strings.TrimSpace(conditionParts[0])
	leftSide = strings.Replace(leftSide, "@if:", "", 1)
	leftSide = strings.Replace(leftSide, "@else-if:", "", 1)
	expectedValue := strings.ReplaceAll(conditionParts[1], `"`, "")
	expectedValue = strings.TrimSpace(expectedValue)

	Log().Debug("Left side: '%s', Expected value: '%s'", leftSide, expectedValue)

	dependencies := []ConditionDependency{}

	if strings.HasPrefix(leftSide, "store:") {
		storeParts := strings.Split(strings.TrimPrefix(leftSide, "store:"), ".")
		if len(storeParts) == 3 {
			module, storeName, key := storeParts[0], storeParts[1], storeParts[2]
			Log().Debug("Dependency detected: Module '%s', Store '%s', Key '%s'", module, storeName, key)
			store := state.GlobalStoreManager.GetStore(module, storeName)
			if store != nil {
				dependencies = append(dependencies, ConditionDependency{module: module, storeName: storeName, key: key})
				actualValue := fmt.Sprintf("%v", store.Get(key))
				Log().Debug("Actual value from store: '%s'", actualValue)
				return actualValue == expectedValue, dependencies
			} else {
				Log().Debug("Store '%s' in module '%s' not found.", storeName, module)
			}
		} else {
			Log().Debug("Store parts length is not 3.")
		}
	}

	if strings.HasPrefix(leftSide, "signal:") {
		sigName := strings.TrimPrefix(leftSide, "signal:")
		if prop, ok := c.Props[sigName]; ok {
			if sig, ok := prop.(interface{ Read() any }); ok {
				dependencies = append(dependencies, ConditionDependency{signal: sigName})
				actualValue := fmt.Sprintf("%v", sig.Read())
				return actualValue == expectedValue, dependencies
			}
		}
	}

	if strings.HasPrefix(leftSide, "prop:") {
		propName := strings.TrimPrefix(leftSide, "prop:")
		if value, exists := c.Props[propName]; exists {
			actualValue := fmt.Sprintf("%v", value)
			Log().Debug("Actual value from props: '%s'", actualValue)
			return actualValue == expectedValue, dependencies
		}
	}

	Log().Debug("No dependencies detected.")
	return false, dependencies
}

updateStoreBindings function

Parameters:

  • c *HTMLComponent
  • module string
  • storeName string
  • key string
  • newValue any
Show/Hide Function Body
{
	doc := dom.Doc()
	var element dom.Element
	if c.ID == "" {
		element = doc.ByID("app")
	} else {
		element = doc.Query(fmt.Sprintf("[data-component-id='%s']", c.ID))
	}
	if element.IsNull() || element.IsUndefined() {
		return
	}

	selector := fmt.Sprintf(`[data-store="%s.%s.%s"]`, module, storeName, key)
	nodes := element.Call("querySelectorAll", selector)
	for i := 0; i < nodes.Length(); i++ {
		node := nodes.Index(i)
		node.Set("innerHTML", fmt.Sprintf("%v", newValue))
	}

	placeholder := fmt.Sprintf("@store:%s.%s.%s:w", module, storeName, key)

	// Update value-based inputs and selects
	inputSelector := fmt.Sprintf(`input[value="%s"], select[value="%s"]`, placeholder, placeholder)
	inputs := element.Call("querySelectorAll", inputSelector)
	for i := 0; i < inputs.Length(); i++ {
		input := inputs.Index(i)
		input.Set("value", fmt.Sprintf("%v", newValue))
	}

	// Update checkboxes bound via checked attribute
	checkedSelector := fmt.Sprintf(`input[checked="%s"]`, placeholder)
	checks := element.Call("querySelectorAll", checkedSelector)
	for i := 0; i < checks.Length(); i++ {
		chk := checks.Index(i)
		switch v := newValue.(type) {
		case bool:
			chk.Set("checked", v)
		case string:
			chk.Set("checked", strings.ToLower(v) == "true")
		default:
			chk.Set("checked", newValue != nil)
		}
	}

	// Update textareas where placeholder is in content
	textareas := element.Call("querySelectorAll", "textarea")
	for i := 0; i < textareas.Length(); i++ {
		ta := textareas.Index(i)
		if ta.Get("value").String() == placeholder {
			ta.Set("value", fmt.Sprintf("%v", newValue))
		}
	}

	updateConditionsForStoreVariable(c, module, storeName, key)
}

updateSignalBindings function

Parameters:

  • c *HTMLComponent
  • name string
  • newValue any
Show/Hide Function Body
{
	doc := dom.Doc()
	var element dom.Element
	if c.ID == "" {
		element = doc.ByID("app")
	} else {
		element = doc.Query(fmt.Sprintf("[data-component-id='%s']", c.ID))
	}
	if element.IsNull() || element.IsUndefined() {
		return
	}

	selector := fmt.Sprintf(`[data-signal="%s"]`, name)
	nodes := element.Call("querySelectorAll", selector)
	for i := 0; i < nodes.Length(); i++ {
		node := nodes.Index(i)
		node.Set("innerHTML", fmt.Sprintf("%v", newValue))
	}

	placeholder := fmt.Sprintf("@signal:%s:w", name)

	// Update value-based inputs and selects
	inputSelector := fmt.Sprintf(`input[value="%s"], select[value="%s"]`, placeholder, placeholder)
	inputs := element.Call("querySelectorAll", inputSelector)
	for i := 0; i < inputs.Length(); i++ {
		input := inputs.Index(i)
		input.Set("value", fmt.Sprintf("%v", newValue))
	}

	// Update checkboxes
	checkedSelector := fmt.Sprintf(`input[checked="%s"]`, placeholder)
	checks := element.Call("querySelectorAll", checkedSelector)
	for i := 0; i < checks.Length(); i++ {
		chk := checks.Index(i)
		switch v := newValue.(type) {
		case bool:
			chk.Set("checked", v)
		case string:
			chk.Set("checked", strings.ToLower(v) == "true")
		default:
			chk.Set("checked", newValue != nil)
		}
	}

	// Update textareas with placeholder in content
	textareas := element.Call("querySelectorAll", "textarea")
	for i := 0; i < textareas.Length(); i++ {
		ta := textareas.Index(i)
		if ta.Get("value").String() == placeholder {
			ta.Set("value", fmt.Sprintf("%v", newValue))
		}
	}
}

insertDataKey function

Parameters:

  • content string
  • key any

Returns:

  • string
Show/Hide Function Body
{
	tagRegex := regexp.MustCompile(`<([a-zA-Z][a-zA-Z0-9-]*)`)
	loc := tagRegex.FindStringSubmatchIndex(content)
	if loc == nil {
		return content
	}
	return content[:loc[1]] + fmt.Sprintf(` data-key="%v"`, key) + content[loc[1]:]
}

replaceConstructors function

replaceConstructors scans for inline constructor tokens inside an element's

start tag and injects the corresponding data attribute. Supported

constructors:

[name] -> data-ref="name"

[key expr] -> data-key="expr"

Only a single constructor per element is handled.

Parameters:

  • template string

Returns:

  • string
Show/Hide Function Body
{
	re := regexp.MustCompile(`<([a-zA-Z][\w-]*)([^>]*?)\s\[([^\] ]+)(?:\s+([^\]]+))?\]([^>]*)>`)
	return re.ReplaceAllStringFunc(template, func(match string) string {
		parts := re.FindStringSubmatch(match)
		if len(parts) < 6 {
			return match
		}
		tag := parts[1]
		before := parts[2]
		name := parts[3]
		param := parts[4]
		after := parts[5]
		attr := ""
		if name == "key" && param != "" {
			attr = fmt.Sprintf(` data-key="%s"`, param)
		} else if strings.HasPrefix(name, "plugin:") {
			attr = fmt.Sprintf(` data-plugin="%s"`, strings.TrimPrefix(name, "plugin:"))
		} else {
			attr = fmt.Sprintf(` data-ref="%s"`, name)
		}
		return fmt.Sprintf("<%s%s%s%s>", tag, before, attr, after)
	})
}

resolveNumber function

Parameters:

  • expr string
  • c *HTMLComponent

Returns:

  • int
  • error
Show/Hide Function Body
{
	if n, err := strconv.Atoi(expr); err == nil {
		return n, nil
	}
	if strings.HasPrefix(expr, "store:") {
		parts := strings.Split(strings.TrimPrefix(expr, "store:"), ".")
		if len(parts) == 3 {
			module, storeName, key := parts[0], parts[1], parts[2]
			store := state.GlobalStoreManager.GetStore(module, storeName)
			if store != nil {
				if val := store.Get(key); val != nil {
					unsubscribe := store.OnChange(key, func(newValue any) {
						dom.UpdateDOM(c.ID, c.Render())
					})
					c.unsubscribes.Add(unsubscribe)
					switch v := val.(type) {
					case int:
						return v, nil
					case float64:
						return int(v), nil
					case string:
						return strconv.Atoi(v)
					}
				}
			}
		}
	}
	if val, ok := c.Props[expr]; ok {
		switch v := val.(type) {
		case int:
			return v, nil
		case float64:
			return int(v), nil
		case string:
			return strconv.Atoi(v)
		}
	}
	return 0, fmt.Errorf("invalid number")
}

legacyReplaceForPlaceholders function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	forRegex := regexp.MustCompile(`@for:(\w+(?:,\w+)?)\s+in\s+(\S+)([\s\S]*?)@endfor`)
	return forRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := forRegex.FindStringSubmatch(match)
		if len(parts) < 4 {
			return match
		}

		varsPart := parts[1]
		expr := parts[2]
		loopContent := parts[3]

		aliases := strings.Split(varsPart, ",")
		for i := range aliases {
			aliases[i] = strings.TrimSpace(aliases[i])
		}

		if strings.Contains(expr, "..") {
			rangeParts := strings.Split(expr, "..")
			if len(rangeParts) != 2 {
				return match
			}
			start, err := resolveNumber(rangeParts[0], c)
			if err != nil {
				return match
			}
			end, err := resolveNumber(rangeParts[1], c)
			if err != nil {
				return match
			}
			var result strings.Builder
			for i := start; i <= end; i++ {
				iter := strings.ReplaceAll(loopContent, fmt.Sprintf("@prop:%s", aliases[0]), fmt.Sprintf("%d", i))
				iter = insertDataKey(iter, i)
				result.WriteString(iter)
			}
			return result.String()
		}

		var collection any
		if strings.HasPrefix(expr, "store:") {
			storeParts := strings.Split(strings.TrimPrefix(expr, "store:"), ".")
			if len(storeParts) == 3 {
				module, storeName, key := storeParts[0], storeParts[1], storeParts[2]
				store := state.GlobalStoreManager.GetStore(module, storeName)
				if store != nil {
					collection = store.Get(key)
					unsubscribe := store.OnChange(key, func(newValue any) {
						dom.UpdateDOM(c.ID, c.Render())
					})
					c.unsubscribes.Add(unsubscribe)
				} else {
					return match
				}
			} else {
				return match
			}
		} else if val, ok := c.Props[expr]; ok {
			collection = val
		} else {
			return match
		}

		switch col := collection.(type) {
		case []any:
			var result strings.Builder
			alias := aliases[0]
			for idx, item := range col {
				iterContent := loopContent
				if itemMap, ok := item.(map[string]any); ok {
					fieldRegex := regexp.MustCompile(fmt.Sprintf(`@prop:%s\.(\w+)`, alias))
					iterContent = fieldRegex.ReplaceAllStringFunc(iterContent, func(fieldMatch string) string {
						fieldParts := fieldRegex.FindStringSubmatch(fieldMatch)
						if len(fieldParts) == 2 {
							if fieldValue, exists := itemMap[fieldParts[1]]; exists {
								return fmt.Sprintf("%v", fieldValue)
							}
						}
						return fieldMatch
					})
				} else {
					iterContent = strings.ReplaceAll(iterContent, fmt.Sprintf("@prop:%s", alias), fmt.Sprintf("%v", item))
				}
				iterContent = insertDataKey(iterContent, idx)
				result.WriteString(iterContent)
			}
			return result.String()
		case map[string]any:
			keyAlias := aliases[0]
			valAlias := keyAlias
			if len(aliases) > 1 {
				valAlias = aliases[1]
			}
			keys := make([]string, 0, len(col))
			for k := range col {
				keys = append(keys, k)
			}
			sort.Strings(keys)
			var result strings.Builder
			for _, k := range keys {
				v := col[k]
				iterContent := strings.ReplaceAll(loopContent, fmt.Sprintf("@prop:%s", keyAlias), k)
				if len(aliases) > 1 {
					if vMap, ok := v.(map[string]any); ok {
						fieldRegex := regexp.MustCompile(fmt.Sprintf(`@prop:%s\.(\w+)`, valAlias))
						iterContent = fieldRegex.ReplaceAllStringFunc(iterContent, func(fieldMatch string) string {
							fieldParts := fieldRegex.FindStringSubmatch(fieldMatch)
							if len(fieldParts) == 2 {
								if fieldValue, exists := vMap[fieldParts[1]]; exists {
									return fmt.Sprintf("%v", fieldValue)
								}
							}
							return fieldMatch
						})
					} else {
						iterContent = strings.ReplaceAll(iterContent, fmt.Sprintf("@prop:%s", valAlias), fmt.Sprintf("%v", v))
					}
				}
				iterContent = insertDataKey(iterContent, k)
				result.WriteString(iterContent)
			}
			return result.String()
		default:
			return match
		}
	})
}

replaceForeachPlaceholders function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	foreachRegex := regexp.MustCompile(`@foreach:(\S+)\s+as\s+(\w+)([\s\S]*?)@endforeach`)
	return foreachRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := foreachRegex.FindStringSubmatch(match)
		if len(parts) < 4 {
			return match
		}

		expr := parts[1]
		alias := parts[2]
		content := parts[3]
		foreachID := fmt.Sprintf("foreach-%x", sha1.Sum([]byte(match)))
		c.foreachContents[foreachID] = ForeachConfig{Expr: expr, ItemAlias: alias, Content: content}

		if strings.HasPrefix(expr, "store:") {
			storeParts := strings.Split(strings.TrimPrefix(expr, "store:"), ".")
			if len(storeParts) == 3 {
				module, storeName, key := storeParts[0], storeParts[1], storeParts[2]
				store := state.GlobalStoreManager.GetStore(module, storeName)
				if store != nil {
					unsubscribe := store.OnChange(key, func(newValue any) {
						updateForeachBindings(c, foreachID)
					})
					c.unsubscribes.Add(unsubscribe)
				}
			}
		} else {
			name := strings.TrimPrefix(expr, "signal:")
			if prop, ok := c.Props[name]; ok {
				if sig, ok := prop.(interface{ Read() any }); ok {
					dom.RegisterSignal(c.ID, name, sig)
					unsub := state.Effect(func() func() {
						sig.Read()
						updateForeachBindings(c, foreachID)
						return nil
					})
					c.unsubscribes.Add(unsub)
				}
			}
		}

		rendered := renderForeachLoop(c, expr, alias, content)
		return fmt.Sprintf(`<div data-foreach="%s">%s</div>`, foreachID, rendered)
	})
}

renderForeachLoop function

Parameters:

  • c *HTMLComponent
  • expr string
  • alias string
  • content string

Returns:

  • string
Show/Hide Function Body
{
	var collection any
	if strings.HasPrefix(expr, "store:") {
		parts := strings.Split(strings.TrimPrefix(expr, "store:"), ".")
		if len(parts) == 3 {
			module, storeName, key := parts[0], parts[1], parts[2]
			store := state.GlobalStoreManager.GetStore(module, storeName)
			if store != nil {
				collection = store.Get(key)
			}
		}
	} else {
		name := strings.TrimPrefix(expr, "signal:")
		if val, ok := c.Props[name]; ok {
			if sig, ok := val.(interface{ Read() any }); ok {
				collection = sig.Read()
			} else {
				collection = val
			}
		}
	}

	val := reflect.ValueOf(collection)
	if !val.IsValid() {
		return ""
	}
	switch val.Kind() {
	case reflect.Slice, reflect.Array:
		var result strings.Builder
		for i := 0; i < val.Len(); i++ {
			item := val.Index(i).Interface()
			iter := content
			if itemMap, ok := item.(map[string]any); ok {
				fieldRegex := regexp.MustCompile(fmt.Sprintf(`@%s\\.(\\w+)`, alias))
				iter = fieldRegex.ReplaceAllStringFunc(iter, func(fieldMatch string) string {
					fieldParts := fieldRegex.FindStringSubmatch(fieldMatch)
					if len(fieldParts) == 2 {
						if fieldValue, exists := itemMap[fieldParts[1]]; exists {
							return fmt.Sprintf("%v", fieldValue)
						}
					}
					return fieldMatch
				})
				propFieldRegex := regexp.MustCompile(fmt.Sprintf(`@prop:%s\\.(\\w+)`, alias))
				iter = propFieldRegex.ReplaceAllStringFunc(iter, func(fieldMatch string) string {
					fieldParts := propFieldRegex.FindStringSubmatch(fieldMatch)
					if len(fieldParts) == 2 {
						if fieldValue, exists := itemMap[fieldParts[1]]; exists {
							return fmt.Sprintf("%v", fieldValue)
						}
					}
					return fieldMatch
				})
			}
			iter = strings.ReplaceAll(iter, fmt.Sprintf("@%s", alias), fmt.Sprintf("%v", item))
			iter = strings.ReplaceAll(iter, fmt.Sprintf("@prop:%s", alias), fmt.Sprintf("%v", item))
			result.WriteString(iter)
		}
		return result.String()
	case reflect.Map:
		if val.Type().Key().Kind() != reflect.String {
			return ""
		}
		var result strings.Builder
		keys := val.MapKeys()
		sort.Slice(keys, func(i, j int) bool { return keys[i].String() < keys[j].String() })
		for _, k := range keys {
			v := val.MapIndex(k).Interface()
			iter := content
			if vMap, ok := v.(map[string]any); ok {
				fieldRegex := regexp.MustCompile(fmt.Sprintf(`@%s\\.(\\w+)`, alias))
				iter = fieldRegex.ReplaceAllStringFunc(iter, func(fieldMatch string) string {
					fieldParts := fieldRegex.FindStringSubmatch(fieldMatch)
					if len(fieldParts) == 2 {
						if fieldValue, exists := vMap[fieldParts[1]]; exists {
							return fmt.Sprintf("%v", fieldValue)
						}
					}
					return fieldMatch
				})
				propFieldRegex := regexp.MustCompile(fmt.Sprintf(`@prop:%s\\.(\\w+)`, alias))
				iter = propFieldRegex.ReplaceAllStringFunc(iter, func(fieldMatch string) string {
					fieldParts := propFieldRegex.FindStringSubmatch(fieldMatch)
					if len(fieldParts) == 2 {
						if fieldValue, exists := vMap[fieldParts[1]]; exists {
							return fmt.Sprintf("%v", fieldValue)
						}
					}
					return fieldMatch
				})
			}
			iter = strings.ReplaceAll(iter, fmt.Sprintf("@%s", alias), fmt.Sprintf("%v", v))
			iter = strings.ReplaceAll(iter, fmt.Sprintf("@prop:%s", alias), fmt.Sprintf("%v", v))
			result.WriteString(iter)
		}
		return result.String()
	default:
		return ""
	}
}

updateForeachBindings function

Parameters:

  • c *HTMLComponent
  • foreachID string
Show/Hide Function Body
{
	doc := dom.Doc()
	var element dom.Element
	if c.ID == "" {
		element = doc.ByID("app")
	} else {
		element = doc.Query(fmt.Sprintf("[data-component-id='%s']", c.ID))
	}
	if element.IsNull() || element.IsUndefined() {
		return
	}

	selector := fmt.Sprintf(`[data-foreach="%s"]`, foreachID)
	node := element.Call("querySelector", selector)
	if node.IsNull() || node.IsUndefined() {
		return
	}

	cfg := c.foreachContents[foreachID]
	newContent := renderForeachLoop(c, cfg.Expr, cfg.ItemAlias, cfg.Content)
	node.Set("innerHTML", newContent)
	dom.BindStoreInputsForComponent(c.ID, node)
	dom.BindSignalInputs(c.ID, node)
}

updateConditionBindings function

Parameters:

  • c *HTMLComponent
  • conditionID string
Show/Hide Function Body
{
	doc := dom.Doc()
	var element dom.Element
	if c.ID == "" {
		element = doc.ByID("app")
	} else {
		element = doc.Query(fmt.Sprintf("[data-component-id='%s']", c.ID))
	}
	if element.IsNull() || element.IsUndefined() {
		return
	}

	selector := fmt.Sprintf(`[data-condition="%s"]`, conditionID)
	node := element.Call("querySelector", selector)
	if node.IsNull() || node.IsUndefined() {
		return
	}

	conditionContent := c.conditionContents[conditionID]
	var newContent string
	for _, br := range conditionContent.Branches {
		if br.Condition == "" {
			if newContent == "" {
				newContent = br.Content
			}
			continue
		}
		result, _ := evaluateCondition(br.Condition, c)
		if result {
			newContent = br.Content
			break
		}
	}

	node.Set("innerHTML", newContent)

	dom.BindStoreInputsForComponent(c.ID, node)
	dom.BindSignalInputs(c.ID, node)
}

updateConditionsForStoreVariable function

Parameters:

  • c *HTMLComponent
  • module string
  • storeName string
  • key string
Show/Hide Function Body
{
	for conditionID, content := range c.conditionContents {
		for _, br := range content.Branches {
			if br.Condition == "" {
				continue
			}
			dependencies, _ := getConditionDependencies(br.Condition)
			for _, dep := range dependencies {
				if dep.module == module && dep.storeName == storeName && dep.key == key {
					updateConditionBindings(c, conditionID)
					break
				}
			}
		}
	}
}

getConditionDependencies function

Parameters:

  • condition string

Returns:

  • []ConditionDependency
  • error
Show/Hide Function Body
{
	conditionParts := strings.Split(condition, "==")
	if len(conditionParts) != 2 {
		return nil, fmt.Errorf("Invalid condition format")
	}

	leftSide := strings.TrimSpace(conditionParts[0])
	leftSide = strings.Replace(leftSide, "@if:", "", 1)
	leftSide = strings.Replace(leftSide, "@else-if:", "", 1)

	dependencies := []ConditionDependency{}

	if strings.HasPrefix(leftSide, "store:") {
		storeParts := strings.Split(strings.TrimPrefix(leftSide, "store:"), ".")
		if len(storeParts) == 3 {
			module, storeName, key := storeParts[0], storeParts[1], storeParts[2]
			dependencies = append(dependencies, ConditionDependency{module: module, storeName: storeName, key: key})
		}
	}

	return dependencies, nil
}

replaceForPlaceholders function

Parameters:

  • template string
  • c *HTMLComponent

Returns:

  • string
Show/Hide Function Body
{
	forRegex := regexp.MustCompile(`@for:(\w+(?:,\w+)?)\s+in\s+(\S+)([\s\S]*?)@endfor`)
	return forRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := forRegex.FindStringSubmatch(match)
		if len(parts) < 4 {
			return match
		}

		varsPart := parts[1]
		expr := parts[2]
		loopContent := parts[3]

		aliases := strings.Split(varsPart, ",")
		for i := range aliases {
			aliases[i] = strings.TrimSpace(aliases[i])
		}

		if strings.Contains(expr, "..") {
			rangeParts := strings.Split(expr, "..")
			if len(rangeParts) != 2 {
				return match
			}
			start, err := resolveNumber(rangeParts[0], c)
			if err != nil {
				return match
			}
			end, err := resolveNumber(rangeParts[1], c)
			if err != nil {
				return match
			}
			var result strings.Builder
			for i := start; i <= end; i++ {
				iter := strings.ReplaceAll(loopContent, fmt.Sprintf("@prop:%s", aliases[0]), fmt.Sprintf("%d", i))
				iter = insertDataKey(iter, i)
				result.WriteString(iter)
			}
			return result.String()
		}

		var collection any
		if strings.HasPrefix(expr, "store:") {
			storeParts := strings.Split(strings.TrimPrefix(expr, "store:"), ".")
			if len(storeParts) == 3 {
				module, storeName, key := storeParts[0], storeParts[1], storeParts[2]
				store := state.GlobalStoreManager.GetStore(module, storeName)
				if store != nil {
					collection = store.Get(key)
					unsubscribe := store.OnChange(key, func(newValue any) {
						dom.UpdateDOM(c.ID, c.Render())
					})
					c.unsubscribes.Add(unsubscribe)
				} else {
					return match
				}
			} else {
				return match
			}
		} else if val, ok := c.Props[expr]; ok {
			collection = val
		} else {
			return match
		}

		switch col := collection.(type) {
		case []Component:
			tmp := make([]any, len(col))
			for i, v := range col {
				tmp[i] = v
			}
			collection = tmp
		case []*HTMLComponent:
			tmp := make([]any, len(col))
			for i, v := range col {
				tmp[i] = v
			}
			collection = tmp
		case map[string]Component:
			tmp := make(map[string]any, len(col))
			for k, v := range col {
				tmp[k] = v
			}
			collection = tmp
		case map[string]*HTMLComponent:
			tmp := make(map[string]any, len(col))
			for k, v := range col {
				tmp[k] = v
			}
			collection = tmp
		}

		switch col := collection.(type) {
		case []any:
			var result strings.Builder
			alias := aliases[0]
			for idx, item := range col {
				iterContent := loopContent
				if comp, ok := item.(Component); ok {
					placeholder := fmt.Sprintf("for-%s-%d", alias, idx)
					c.AddDependency(placeholder, comp)
					iterContent = strings.ReplaceAll(iterContent, fmt.Sprintf("@prop:%s", alias), fmt.Sprintf("@include:%s", placeholder))
				} else if itemMap, ok := item.(map[string]any); ok {
					fieldRegex := regexp.MustCompile(fmt.Sprintf("@prop:%s\\.(\\w+)", alias))
					iterContent = fieldRegex.ReplaceAllStringFunc(iterContent, func(fieldMatch string) string {
						fieldParts := fieldRegex.FindStringSubmatch(fieldMatch)
						if len(fieldParts) == 2 {
							if fieldValue, exists := itemMap[fieldParts[1]]; exists {
								return fmt.Sprintf("%v", fieldValue)
							}
						}
						return fieldMatch
					})
				} else {
					iterContent = strings.ReplaceAll(iterContent, fmt.Sprintf("@prop:%s", alias), fmt.Sprintf("%v", item))
				}
				iterContent = insertDataKey(iterContent, idx)
				result.WriteString(iterContent)
			}
			return result.String()
		case map[string]any:
			keyAlias := aliases[0]
			valAlias := keyAlias
			if len(aliases) > 1 {
				valAlias = aliases[1]
			}
			keys := make([]string, 0, len(col))
			for k := range col {
				keys = append(keys, k)
			}
			sort.Strings(keys)
			var result strings.Builder
			for idx, k := range keys {
				v := col[k]
				iterContent := strings.ReplaceAll(loopContent, fmt.Sprintf("@prop:%s", keyAlias), k)
				if len(aliases) > 1 {
					if vMap, ok := v.(map[string]any); ok {
						fieldRegex := regexp.MustCompile(fmt.Sprintf("@prop:%s\\.(\\w+)", valAlias))
						iterContent = fieldRegex.ReplaceAllStringFunc(iterContent, func(fieldMatch string) string {
							fieldParts := fieldRegex.FindStringSubmatch(fieldMatch)
							if len(fieldParts) == 2 {
								if fieldValue, exists := vMap[fieldParts[1]]; exists {
									return fmt.Sprintf("%v", fieldValue)
								}
							}
							return fieldMatch
						})
					} else if comp, ok := v.(Component); ok {
						placeholder := fmt.Sprintf("for-%s-%d", valAlias, idx)
						c.AddDependency(placeholder, comp)
						iterContent = strings.ReplaceAll(iterContent, fmt.Sprintf("@prop:%s", valAlias), fmt.Sprintf("@include:%s", placeholder))
					} else {
						iterContent = strings.ReplaceAll(iterContent, fmt.Sprintf("@prop:%s", valAlias), fmt.Sprintf("%v", v))
					}
				}
				iterContent = insertDataKey(iterContent, k)
				result.WriteString(iterContent)
			}
			return result.String()
		default:
			return match
		}
	})
}

TestNamedSlotExtraction function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	childTpl := []byte("<root>@slot:avatar<div>default</div>@endslot</root>")
	parentTpl := []byte("<root>@slot:child.avatar<img src=\"pic.png\"/>@endslot@include:child</root>")

	store := state.NewStore("test")
	parent := NewHTMLComponent("Parent", parentTpl, nil)
	parent.Init(store)
	child := NewHTMLComponent("Child", childTpl, nil)
	// child Init will be called via AddDependency
	parent.AddDependency("child", child)

	html := parent.Render()
	if strings.Contains(html, ".avatar") {
		t.Fatalf("slot placeholder not removed: %s", html)
	}
	if !strings.Contains(html, "pic.png") {
		t.Fatalf("slot content not injected: %s", html)
	}
}

TestIncludePlaceholderPrefixCollision function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	childTpl := []byte("<root>@slot:avatar<div>fallback-avatar</div>@endslot<div>@slot<p>fallback-details</p>@endslot</div></root>")
	parentTpl := []byte("<root>@slot:card.avatar<img/>@endslot@slot:card<p>details</p>@endslot@include:card@include:cardFallback</root>")

	store := state.NewStore("test2")
	parent := NewHTMLComponent("Parent2", parentTpl, nil)
	parent.Init(store)
	card := NewHTMLComponent("Child", childTpl, nil)
	fallback := NewHTMLComponent("Child", childTpl, nil)
	parent.AddDependency("card", card)
	parent.AddDependency("cardFallback", fallback)

	html := parent.Render()
	if strings.Count(html, "<img/>") != 1 {
		t.Fatalf("expected one image only: %s", html)
	}
	if !strings.Contains(html, "fallback-avatar") || !strings.Contains(html, "fallback-details") {
		t.Fatalf("fallback content missing: %s", html)
	}
}

Suspense struct

Suspense renders a fallback while the render function returns http.ErrPending.

Fields:

  • render (func() (string, error))
  • fallback (string)

Implements:

  • Component from core

Methods:

Render

Render executes the render function and shows the fallback until it resolves.


Returns:
  • string

Show/Hide Method Body
{
	if s.render == nil {
		return s.fallback
	}
	content, err := s.render()
	if err != nil {
		if errors.Is(err, http.ErrPending) {
			return s.fallback
		}
		return err.Error()
	}
	return content
}

Mount

Mount is a no-op for Suspense.


Show/Hide Method Body
{}

Unmount

Unmount is a no-op for Suspense.


Show/Hide Method Body
{}

OnMount

OnMount is a no-op for Suspense.


Show/Hide Method Body
{}

OnUnmount

OnUnmount is a no-op for Suspense.


Show/Hide Method Body
{}

GetName

GetName returns the component name.


Returns:
  • string

Show/Hide Method Body
{ return "Suspense" }

GetID

GetID returns an empty ID for Suspense.


Returns:
  • string

Show/Hide Method Body
{ return "" }

SetSlots

SetSlots is a no-op since Suspense does not use slots.


Parameters:
  • map[string]any

Show/Hide Method Body
{}

NewSuspense function

NewSuspense creates a Suspense component with the given render function and fallback HTML.

Parameters:

  • render func() (string, error)
  • fallback string

Returns:

  • *Suspense
Show/Hide Function Body
{
	return &Suspense{render: render, fallback: fallback}
}

TestComponentRegistryConcurrentAccess function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	componentRegistryMu.Lock()
	ComponentRegistry = map[string]func() Component{}
	componentRegistryMu.Unlock()

	const n = 100
	var wg sync.WaitGroup
	wg.Add(n)
	for i := 0; i < n; i++ {
		go func(i int) {
			defer wg.Done()
			name := fmt.Sprintf("comp-%d", i)
			if err := RegisterComponent(name, func() Component { return noopComponent{} }); err != nil {
				t.Errorf("register %s: %v", name, err)
			}
			if c := LoadComponent(name); c == nil {
				t.Errorf("load %s: got nil", name)
			}
		}(i)
	}
	wg.Wait()
}

startDevTemplateWatcher function

Show/Hide Function Body
{}

devApplyTemplateUpdate function

Parameters:

  • string
  • string
Show/Hide Function Body
{}

devOverrideTemplate function

Parameters:

  • c *HTMLComponent
  • template string

Returns:

  • string
Show/Hide Function Body
{ return template }

devRegisterComponent function

Parameters:

  • *HTMLComponent
Show/Hide Function Body
{}

devUnregisterComponent function

Parameters:

  • *HTMLComponent
Show/Hide Function Body
{}

panicComponent struct

Implements:

  • Component from core

Methods:

Render


Returns:
  • string

Show/Hide Method Body
{ panic("boom") }

Mount


Show/Hide Method Body
{}

Unmount


Show/Hide Method Body
{}

OnMount


Show/Hide Method Body
{}

OnUnmount


Show/Hide Method Body
{}

GetName


Returns:
  • string

Show/Hide Method Body
{ return "panic" }

GetID


Returns:
  • string

Show/Hide Method Body
{ return "panic" }

SetSlots


Parameters:
  • map[string]any

Show/Hide Method Body
{}

TestErrorBoundaryRender function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	eb := NewErrorBoundary(&panicComponent{}, "<div>fb</div>")
	html := eb.Render()
	expected := "<root data-component-id=\"panic\"><div>fb</div></root>"
	if html != expected {
		t.Fatalf("expected %s, got %s", expected, html)
	}
}

TestForRendersComponentList function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	state.NewStore("default", state.WithModule("app"))

	childTpl1 := []byte("<root><p>first</p></root>")
	childTpl2 := []byte("<root><p>second</p></root>")
	child1 := NewComponent("Child1", childTpl1, nil)
	child2 := NewComponent("Child2", childTpl2, nil)

	parentTpl := []byte("<root>@for:item in items @prop:item @endfor</root>")
	parent := NewComponent("Parent", parentTpl, map[string]any{"items": []Component{child1, child2}})

	html := parent.Render()
	if !strings.Contains(html, "first") || !strings.Contains(html, "second") {
		t.Fatalf("expected child components rendered: %s", html)
	}
}

TestForRendersMapFields function

Parameters:

  • t *testing.T
Show/Hide Function Body
{
	state.NewStore("default", state.WithModule("app"))

	items := []any{
		map[string]any{"name": "Mario", "age": 30},
		map[string]any{"name": "Luigi", "age": 25},
	}

	parentTpl := []byte("<root>@for:item in items <p><b>Name:</b> @prop:item.name <b>Age:</b> @prop:item.age</p> @endfor</root>")
	parent := NewComponent("Parent", parentTpl, map[string]any{"items": items})

	html := parent.Render()
	if !strings.Contains(html, "Mario") || !strings.Contains(html, "Luigi") {
		t.Fatalf("expected names rendered: %s", html)
	}
	if strings.Contains(html, "@prop:item.name") || strings.Contains(html, "@prop:item.age") {
		t.Fatalf("placeholders not replaced: %s", html)
	}
}

unsubscribes struct

Fields:

  • funcs ([]func())

Methods:

Add


Parameters:
  • fn func()

Show/Hide Method Body
{ u.funcs = append(u.funcs, fn) }

Run


Show/Hide Method Body
{
	for _, fn := range u.funcs {
		fn()
	}
	u.funcs = nil
}

HTMLComponent struct

Fields:

  • ID (string)
  • Name (string)
  • Template (string)
  • TemplateFS ([]byte)
  • Dependencies (map[string]Component)
  • unsubscribes (unsubscribes)
  • Store (*state.Store)
  • Props (map[string]any)
  • Slots (map[string]any)
  • HostComponent (string)
  • conditionContents (map[string]ConditionContent)
  • foreachContents (map[string]ForeachConfig)
  • hostVars ([]string)
  • hostCmds ([]string)
  • component (Component)
  • onMount (func(*HTMLComponent))
  • onUnmount (func(*HTMLComponent))
  • parent (*HTMLComponent)
  • provides (map[string]any)
  • cache (map[string]string)
  • lastCacheKey (string)
  • metricsMu (sync.Mutex)
  • renderCount (int)
  • totalRender (time.Duration)
  • lastRender (time.Duration)
  • timeline ([]ComponentTimelineEntry)

Methods:

Stats

Stats returns zeroed metrics on non-wasm builds.


Returns:
  • ComponentStats

References:


Show/Hide Method Body
{ return ComponentStats{} }

Init


Parameters:
  • store *state.Store

Show/Hide Method Body
{
	template, err := LoadComponentTemplate(c.TemplateFS)
	if err != nil {
		panic(fmt.Sprintf("Error loading template for component %s: %v", c.Name, err))
	}
	template = devOverrideTemplate(c, template)
	c.Template = template
	dom.RegisterBindings(c.ID, c.Name, template)
	devRegisterComponent(c)

	if store != nil {
		c.Store = store
	} else {
		c.Store = state.GlobalStoreManager.GetStore("app", "default")
		if c.Store == nil {
			panic(fmt.Sprintf("No store provided and no default store found for component %s", c.Name))
		}
	}
}

Render


Returns:
  • renderedTemplate string

Show/Hide Method Body
{
	start := time.Now()
	defer c.recordRender(time.Since(start))
	key := c.cacheKey()
	if c.cache != nil {
		if val, ok := c.cache[key]; ok {
			renderedTemplate = val
			return
		}
		if c.lastCacheKey != "" && c.lastCacheKey != key {
			delete(c.cache, c.lastCacheKey)
		}
	} else {
		c.cache = make(map[string]string)
	}
	defer func() {
		if r := recover(); r != nil {
			jsStack := js.Error().New().Get("stack").String()
			goStack := string(debug.Stack())
			panic(fmt.Sprintf("%v\nGo stack:\n%s\nJS stack:\n%s", r, goStack, jsStack))
		}
	}()

	c.unsubscribes.Run()

	renderedTemplate = c.Template
	renderedTemplate = strings.Replace(renderedTemplate, "<root", fmt.Sprintf("<root data-component-id=\"%s\"", c.ID), 1)

	// Extract slot contents destined for child components
	renderedTemplate = extractSlotContents(renderedTemplate, c)

	// Replace this component's slot placeholders with provided content or fallbacks
	renderedTemplate = replaceSlotPlaceholders(renderedTemplate, c)

	for key, value := range c.Props {
		placeholder := fmt.Sprintf("{{%s}}", key)
		renderedTemplate = strings.ReplaceAll(renderedTemplate, placeholder, fmt.Sprintf("%v", value))
	}

	// Register @include directives that supply inline props
	renderedTemplate = replaceComponentIncludes(renderedTemplate, c)

	// Handle @include:componentName syntax for dependencies
	renderedTemplate = replaceIncludePlaceholders(c, renderedTemplate)

	// Handle @for loops and legacy @foreach syntax
	renderedTemplate = replaceForPlaceholders(renderedTemplate, c)
	renderedTemplate = replaceForeachPlaceholders(renderedTemplate, c)

	// Handle @store:module.storeName.varName syntax.
	// Append :w for writable inputs; read-only inputs omit the suffix (:r is not supported).
	renderedTemplate = replaceStorePlaceholders(renderedTemplate, c)

	// Handle @signal:name syntax for local signals
	renderedTemplate = replaceSignalPlaceholders(renderedTemplate, c)

	// Handle @prop:propName syntax for props
	renderedTemplate = replacePropPlaceholders(renderedTemplate, c)

	// Handle plugin variable and command placeholders
	renderedTemplate = replacePluginPlaceholders(renderedTemplate)

	// Handle host variable and command placeholders
	if c.HostComponent != "" {
		renderedTemplate = replaceHostPlaceholders(renderedTemplate, c)
	}

	// Handle @if:condition syntax for conditional rendering
	renderedTemplate = replaceConditionals(renderedTemplate, c)

	// Handle @on:event:handler and @event:handler syntax for event binding
	renderedTemplate = replaceEventHandlers(renderedTemplate)

	// Handle rt-is="ComponentName" for dynamic component loading
	renderedTemplate = replaceRtIsAttributes(renderedTemplate, c)

	// Render any components introduced via rt-is placeholders
	renderedTemplate = replaceIncludePlaceholders(c, renderedTemplate)

	// Handle constructor decorators like [ref] and [key expr]
	renderedTemplate = replaceConstructors(renderedTemplate)

	if c.HostComponent != "" {
		hostclient.RegisterComponent(c.ID, c.HostComponent, c.hostVars)
	}

	renderedTemplate = minifyInline(renderedTemplate)

	c.cache[key] = renderedTemplate
	c.lastCacheKey = key
	return renderedTemplate
}

recordRender


Parameters:
  • duration time.Duration

Show/Hide Method Body
{
	if c == nil {
		return
	}
	c.metricsMu.Lock()
	c.renderCount++
	c.totalRender += duration
	c.lastRender = duration
	c.appendTimelineLocked(ComponentTimelineEntry{
		Kind:      "render",
		Timestamp: time.Now(),
		Duration:  duration,
	})
	c.metricsMu.Unlock()
}

appendTimelineLocked


Parameters:
  • entry ComponentTimelineEntry

References:


Show/Hide Method Body
{
	if entry.Kind == "" {
		return
	}
	if c.timeline == nil {
		c.timeline = make([]ComponentTimelineEntry, 0, 8)
	}
	c.timeline = append(c.timeline, entry)
	if len(c.timeline) > componentTimelineLimit {
		c.timeline = append([]ComponentTimelineEntry(nil), c.timeline[len(c.timeline)-componentTimelineLimit:]...)
	}
}

Stats

Stats returns a snapshot of the component's render metrics.


Returns:
  • ComponentStats

References:


Show/Hide Method Body
{
	c.metricsMu.Lock()
	defer c.metricsMu.Unlock()
	stats := ComponentStats{
		RenderCount: c.renderCount,
		TotalRender: c.totalRender,
		LastRender:  c.lastRender,
	}
	if c.renderCount > 0 {
		stats.AverageRender = c.totalRender / time.Duration(c.renderCount)
	}
	if len(c.timeline) > 0 {
		stats.Timeline = append(stats.Timeline, c.timeline...)
	}
	return stats
}

AddDependency


Parameters:
  • placeholderName string
  • dep Component

References:


Show/Hide Method Body
{
	if c.Dependencies == nil {
		c.Dependencies = make(map[string]Component)
	}
	if depComp, ok := dep.(*HTMLComponent); ok {
		depComp.Init(c.Store)
		depComp.parent = c
	}
	c.Dependencies[placeholderName] = dep
}

Unmount


Show/Hide Method Body
{
	devUnregisterComponent(c)
	if c.component != nil {
		c.component.OnUnmount()
	}

	dom.RemoveEventListeners(c.ID)
	dom.RemoveComponentSignals(c.ID)
	log.Printf("Unsubscribing %s from all stores", c.Name)
	c.unsubscribes.Run()

	for _, dep := range c.Dependencies {
		dep.Unmount()
	}
}

Mount


Show/Hide Method Body
{
	for _, dep := range c.Dependencies {
		dep.Mount()
	}
	if c.component != nil {
		c.component.OnMount()
	}
}

GetName


Returns:
  • string

Show/Hide Method Body
{
	return c.Name
}

GetID


Returns:
  • string

Show/Hide Method Body
{
	return c.ID
}

GetRef

GetRef returns the DOM element annotated with a matching constructor

decorator. It searches within this component's root element using the

data-ref attribute injected during template rendering.


Parameters:
  • name string

Returns:
  • dom.Element

Show/Hide Method Body
{
	doc := dom.Doc()
	var root dom.Element
	if c.ID == "" {
		root = doc.ByID("app")
	} else {
		root = doc.Query(fmt.Sprintf("[data-component-id='%s']", c.ID))
	}
	if root.IsNull() || root.IsUndefined() {
		return dom.Element{}
	}
	return root.Query(fmt.Sprintf(`[data-ref="%s"]`, name))
}

OnMount


Show/Hide Method Body
{
	if c.onMount != nil {
		c.onMount(c)
	}
}

OnUnmount


Show/Hide Method Body
{
	if c.onUnmount != nil {
		c.onUnmount(c)
	}
}

SetOnMount


Parameters:
  • fn func(*HTMLComponent)

Show/Hide Method Body
{
	c.onMount = fn
}

SetOnUnmount


Parameters:
  • fn func(*HTMLComponent)

Show/Hide Method Body
{
	c.onUnmount = fn
}

WithLifecycle


Parameters:
  • onMount func(*HTMLComponent)
  • onUnmount func(*HTMLComponent)

Returns:
  • *HTMLComponent

Show/Hide Method Body
{
	c.onMount = onMount
	c.onUnmount = onUnmount
	return c
}

SetComponent


Parameters:
  • component Component

References:


Show/Hide Method Body
{
	c.component = component
}

SetSlots


Parameters:
  • slots map[string]any

Show/Hide Method Body
{
	if c.Slots == nil {
		c.Slots = make(map[string]any)
	}
	for k, v := range slots {
		c.Slots[k] = v
	}
}

Provide

Provide stores a value on this component so that descendants can

retrieve it with Inject. It creates the map on first use.


Parameters:
  • key string
  • val any

Show/Hide Method Body
{
	if c.provides == nil {
		c.provides = make(map[string]any)
	}
	c.provides[key] = val
}

Inject

Inject searches for a provided value starting from this component and

walking up the parent chain. It returns the value as `any` and whether it

was found. Callers can type-assert the result.


Parameters:
  • key string

Returns:
  • any
  • bool

Show/Hide Method Body
{
	if c.provides != nil {
		if v, ok := c.provides[key]; ok {
			return v, true
		}
	}
	if c.parent != nil {
		return c.parent.Inject(key)
	}
	return nil, false
}

SetRouteParams


Parameters:
  • params map[string]string

Show/Hide Method Body
{
	if c.Props == nil {
		c.Props = make(map[string]any)
	}
	for k, v := range params {
		c.Props[k] = v
	}
}

AddHostComponent

AddHostComponent links this HTML component to a server-side HostComponent

by name. When running in SSC mode, messages from the wasm runtime will be

routed to the corresponding host component on the server.


Parameters:
  • name string

Show/Hide Method Body
{
	c.HostComponent = name
}

cacheKey


Returns:
  • string

Show/Hide Method Body
{
	hasher := sha1.New()
	hasher.Write([]byte(serializeProps(c.Props)))

	if len(c.Dependencies) > 0 {
		deps := make([]string, 0, len(c.Dependencies))
		for name, dep := range c.Dependencies {
			deps = append(deps, name+dep.GetID())
		}
		sort.Strings(deps)
		for _, d := range deps {
			hasher.Write([]byte(d))
		}
	}

	return hex.EncodeToString(hasher.Sum(nil))
}

ComponentStats struct

ComponentStats contains aggregated render metrics for an HTML component.

Fields:

  • RenderCount (int)
  • TotalRender (time.Duration)
  • LastRender (time.Duration)
  • AverageRender (time.Duration)
  • Timeline ([]ComponentTimelineEntry)

ComponentTimelineEntry struct

ComponentTimelineEntry represents a point-in-time event collected for diagnostics.

Fields:

  • Kind (string)
  • Timestamp (time.Time)
  • Duration (time.Duration)

NewHTMLComponent function

Parameters:

  • name string
  • templateFs []byte
  • props map[string]any

Returns:

  • *HTMLComponent
Show/Hide Function Body
{
	id := generateComponentID(name, props)
	c := &HTMLComponent{
		ID:                id,
		Name:              name,
		TemplateFS:        templateFs,
		Dependencies:      make(map[string]Component),
		Props:             props,
		Slots:             make(map[string]any),
		conditionContents: make(map[string]ConditionContent),
		foreachContents:   make(map[string]ForeachConfig),
	}
	// Attempt automatic cleanup when component is garbage collected.
	runtime.SetFinalizer(c, func(hc *HTMLComponent) { hc.Unmount() })
	return c
}

minifyInline function

Parameters:

  • src string

Returns:

  • string
Show/Hide Function Body
{
	inlineMinifierOnce.Do(func() {
		inlineMinifier = minify.New()
		inlineMinifier.AddFunc("text/javascript", tdJs.Minify)
		inlineMinifier.AddFunc("text/css", css.Minify)
	})
	return inlineRe.ReplaceAllStringFunc(src, func(match string) string {
		m := inlineRe.FindStringSubmatch(match)
		tag, attrs, code := m[1], m[2], m[3]
		media := "text/javascript"
		if tag == "style" {
			media = "text/css"
		}
		out, err := inlineMinifier.String(media, code)
		if err != nil {
			return match
		}
		return fmt.Sprintf("<%s%s>%s</%s>", tag, attrs, strings.TrimSpace(out), tag)
	})
}

Inject function

InjectTyped is a helper that performs a typed injection using generics.

It calls c.Inject and attempts to cast the value to T.

Parameters:

  • c *HTMLComponent
  • key string

Returns:

  • T
  • bool
Show/Hide Function Body
{
	v, ok := c.Inject(key)
	if !ok {
		var zero T
		return zero, false
	}
	t, ok := v.(T)
	return t, ok
}

generateComponentID function

Parameters:

  • name string
  • props map[string]any

Returns:

  • string
Show/Hide Function Body
{
	hasher := sha1.New()
	hasher.Write([]byte(name))
	propsString := serializeProps(props)
	hasher.Write([]byte(propsString))

	return hex.EncodeToString(hasher.Sum(nil))
}

serializeProps function

Parameters:

  • props map[string]any

Returns:

  • string
Show/Hide Function Body
{
	if props == nil {
		return ""
	}

	var sb strings.Builder
	keys := make([]string, 0, len(props))
	for k := range props {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		v := props[k]
		sb.WriteString(fmt.Sprintf("%s=%v;", k, v))
	}

	return sb.String()
}

LoadComponentTemplate function

Parameters:

  • templateFs []byte

Returns:

  • string
  • error
Show/Hide Function Body
{
	template := string(templateFs)
	if template == "" {
		return "", fmt.Errorf("template is empty")
	}

	return template, nil
}

RegisterComponent function

RegisterComponent registers a component constructor for lookup by name. It

returns an error if a component with the same name has already been

registered and logs a warning.

Parameters:

  • name string
  • constructor func() Component

Returns:

  • error

References:

Show/Hide Function Body
{
	componentRegistryMu.Lock()
	defer componentRegistryMu.Unlock()
	if _, exists := ComponentRegistry[name]; exists {
		Log().Warn("component %s already registered", name)
		return fmt.Errorf("component %s already registered", name)
	}
	ComponentRegistry[name] = constructor
	return nil
}

LoadComponent function

LoadComponent retrieves a component constructor by name. If no component is

registered under that name, nil is returned.

Parameters:

  • name string

Returns:

  • Component

References:

Show/Hide Function Body
{
	componentRegistryMu.RLock()
	ctor, ok := ComponentRegistry[name]
	componentRegistryMu.RUnlock()
	if ok {
		return ctor()
	}
	return nil
}

MustRegisterComponent function

MustRegisterComponent registers a component constructor under the provided name

and panics if the component is already registered.

Parameters:

  • name string
  • ctor func() Component

References:

Show/Hide Function Body
{
	if err := RegisterComponent(name, ctor); err != nil {
		panic(err)
	}
}

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

time import

Import example:

import "time"

encoding/json import

Import example:

import "encoding/json"

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

encoding/json import

Import example:

import "encoding/json"

testing import

Import example:

import "testing"

encoding/json import

Import example:

import "encoding/json"

log import

Import example:

import "log"

sync import

Import example:

import "sync"

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

github.com/rfwlab/rfw/v1/js import

Import example:

import "github.com/rfwlab/rfw/v1/js"

Imported as:

js

encoding/json import

Import example:

import "encoding/json"

testing import

Import example:

import "testing"

strings import

Import example:

import "strings"

testing import

Import example:

import "testing"

fmt import

Import example:

import "fmt"

sync import

Import example:

import "sync"

log import

Import example:

import "log"

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

testing import

Import example:

import "testing"

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

crypto/sha1 import

Import example:

import "crypto/sha1"

fmt import

Import example:

import "fmt"

html import

Import example:

import "html"

reflect import

Import example:

import "reflect"

regexp import

Import example:

import "regexp"

sort import

Import example:

import "sort"

strconv import

Import example:

import "strconv"

strings import

Import example:

import "strings"

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

fmt import

Import example:

import "fmt"

regexp import

Import example:

import "regexp"

sort import

Import example:

import "sort"

strings import

Import example:

import "strings"

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

strings import

Import example:

import "strings"

testing import

Import example:

import "testing"

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

errors import

Import example:

import "errors"

github.com/rfwlab/rfw/v1/http import

Import example:

import "github.com/rfwlab/rfw/v1/http"

Imported as:

http

fmt import

Import example:

import "fmt"

sync import

Import example:

import "sync"

testing import

Import example:

import "testing"

testing import

Import example:

import "testing"

strings import

Import example:

import "strings"

testing import

Import example:

import "testing"

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

crypto/sha1 import

Import example:

import "crypto/sha1"

encoding/hex import

Import example:

import "encoding/hex"

fmt import

Import example:

import "fmt"

log import

Import example:

import "log"

regexp import

Import example:

import "regexp"

runtime import

Import example:

import "runtime"

runtime/debug import

Import example:

import "runtime/debug"

sort import

Import example:

import "sort"

strings import

Import example:

import "strings"

sync import

Import example:

import "sync"

time import

Import example:

import "time"

github.com/rfwlab/rfw/v1/dom import

Import example:

import "github.com/rfwlab/rfw/v1/dom"

github.com/rfwlab/rfw/v1/hostclient import

Import example:

import "github.com/rfwlab/rfw/v1/hostclient"

Imported as:

hostclient

github.com/rfwlab/rfw/v1/js import

Import example:

import "github.com/rfwlab/rfw/v1/js"

Imported as:

js

github.com/rfwlab/rfw/v1/state import

Import example:

import "github.com/rfwlab/rfw/v1/state"

github.com/tdewolff/minify/v2 import

Import example:

import "github.com/tdewolff/minify/v2"

github.com/tdewolff/minify/v2/css import

Import example:

import "github.com/tdewolff/minify/v2/css"

github.com/tdewolff/minify/v2/js import

Import example:

import "github.com/tdewolff/minify/v2/js"

Imported as:

tdJs

fmt import

Import example:

import "fmt"

fmt import

Import example:

import "fmt"

sync import

Import example:

import "sync"