{ u.funcs = append(u.funcs, fn) }
{
for _, fn := range u.funcs {
fn()
}
u.funcs = nil
}
{
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))
}
}
}
{
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
}
{
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()
}
{
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 returns a snapshot of the component's render metrics.
{
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
}
{
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
}
{
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()
}
}
{
for _, dep := range c.Dependencies {
dep.Mount()
}
if c.component != nil {
c.component.OnMount()
}
}
{
return c.Name
}
{
return c.ID
}
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.
{
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))
}
{
if c.onMount != nil {
c.onMount(c)
}
}
{
if c.onUnmount != nil {
c.onUnmount(c)
}
}
{
c.onMount = fn
}
{
c.onUnmount = fn
}
{
c.onMount = onMount
c.onUnmount = onUnmount
return c
}
{
c.component = component
}
{
if c.Slots == nil {
c.Slots = make(map[string]any)
}
for k, v := range slots {
c.Slots[k] = v
}
}
Provide stores a value on this component so that descendants can
retrieve it with Inject. It creates the map on first use.
{
if c.provides == nil {
c.provides = make(map[string]any)
}
c.provides[key] = val
}
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.
{
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
}
{
if c.Props == nil {
c.Props = make(map[string]any)
}
for k, v := range params {
c.Props[k] = v
}
}
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.
{
c.HostComponent = name
}
{
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))
}
Stats returns zeroed metrics on non-wasm builds.
{ return ComponentStats{} }
ComponentStats contains aggregated render metrics for an HTML component.
ComponentTimelineEntry represents a point-in-time event collected for diagnostics.
{
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
}
{
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)
})
}
InjectTyped is a helper that performs a typed injection using generics.
It calls c.Inject and attempts to cast the value to T.
{
v, ok := c.Inject(key)
if !ok {
var zero T
return zero, false
}
t, ok := v.(T)
return t, ok
}
{
hasher := sha1.New()
hasher.Write([]byte(name))
propsString := serializeProps(props)
hasher.Write([]byte(propsString))
return hex.EncodeToString(hasher.Sum(nil))
}
{
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()
}
Logger defines logging interface used by the framework.
{ state.SetLogger(logger) }
SetLogger allows applications to replace the default logger.
{
if l != nil {
logger = l
state.SetLogger(l)
}
}
Log returns the active logger implementation.
{ return logger }
defaultLogger is the fallback logger using the standard log package.
{ log.Printf("DEBUG: "+format, v...) }
{ log.Printf("INFO: "+format, v...) }
{ log.Printf("WARN: "+format, v...) }
{ log.Printf("ERROR: "+format, v...) }
{
template := string(templateFs)
if template == "" {
return "", fmt.Errorf("template is empty")
}
return template, nil
}
{ panic("boom") }
{}
{}
{}
{}
{ return "panic" }
{ return "panic" }
{}
{
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)
}
}
{
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)
}
}
{
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)
}
}
AST structures for template parsing
{ return t.Text }
Render evaluates the conditional branches and renders the appropriate content.
{
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)
}
ConditionContent stores rendered content for each branch of a conditional block
{
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 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.
{
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
}
{
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
})
}
{
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
})
}
{
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
})
}
{
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
})
}
{
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
})
}
{
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
}
{
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
}
{
// 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 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.
{
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 parses the template string into an AST of nodes.
{
lines := strings.Split(template, "\n")
idx := 0
return parseBlock(lines, &idx)
}
{
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
}
{
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 parses conditionals using the AST and renders them.
{
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()
}
{
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
}
{
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)
}
{
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))
}
}
}
{
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 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.
{
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)
})
}
{
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")
}
{
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
}
})
}
{
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)
})
}
{
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 ""
}
}
{
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)
}
{
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)
}
{
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
}
}
}
}
}
{
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
}
Suspense renders a fallback while the render function returns http.ErrPending.
Render executes the render function and shows the fallback until it resolves.
{
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 is a no-op for Suspense.
{}
Unmount is a no-op for Suspense.
{}
OnMount is a no-op for Suspense.
{}
OnUnmount is a no-op for Suspense.
{}
GetName returns the component name.
{ return "Suspense" }
GetID returns an empty ID for Suspense.
{ return "" }
SetSlots is a no-op since Suspense does not use slots.
{}
NewSuspense creates a Suspense component with the given render function and fallback HTML.
{
return &Suspense{render: render, fallback: fallback}
}
SetDevMode toggles development mode features.
{
DevMode = enabled
if enabled {
startDevTemplateWatcher()
}
}
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.
{
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 retrieves a component by name using the registry. If no
component is registered under that name, nil is returned.
{
componentRegistryMu.RLock()
ctor, ok := ComponentRegistry[name]
componentRegistryMu.RUnlock()
if ok {
return ctor()
}
return nil
}
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.
{
c := NewHTMLComponent(name, templateFS, props)
c.SetComponent(c)
c.Init(nil)
return c
}
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.
{
c := NewHTMLComponent(name, templateFS, props)
if any(self) != nil {
c.SetComponent(self)
} else {
c.SetComponent(c)
}
c.Init(nil)
return c
}
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.
{
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 retrieves a component constructor by name. If no component is
registered under that name, nil is returned.
{
componentRegistryMu.RLock()
ctor, ok := ComponentRegistry[name]
componentRegistryMu.RUnlock()
if ok {
return ctor()
}
return nil
}
MustRegisterComponent registers a component constructor under the provided name
and panics if the component is already registered.
{
if err := RegisterComponent(name, ctor); err != nil {
panic(err)
}
}
{ return "" }
{}
{}
{}
{}
{ return "noop" }
{ return "noop" }
{}
{
// 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")
}
}
{
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{} })
}
Plugin is a no-op stub for non-WASM builds.
App is a stub holder for callbacks.
{}
{}
{}
{}
{}
{ return false }
RegisterRouter adds a router navigation hook.
{
a.routerHooks = append(a.routerHooks, fn)
}
RegisterStore adds a store mutation hook.
{
a.storeHooks = append(a.storeHooks, fn)
}
RegisterTemplate adds a template render hook.
{
a.templateHooks = append(a.templateHooks, fn)
}
RegisterLifecycle adds hooks for component mount and unmount.
{
if mount != nil {
a.mountHooks = append(a.mountHooks, mount)
}
if unmount != nil {
a.unmountHooks = append(a.unmountHooks, unmount)
}
}
RegisterRTMLVar registers a value that can be referenced from RTML as
{plugin:NAME.VAR}.
{
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 reports whether a plugin with the given name is installed.
{
if a.plugins == nil {
return false
}
_, ok := a.plugins[name]
return ok
}
{}
{}
{}
{}
{}
{}
{}
{
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)
}
})
}
{
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)
}
}
{
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)
}
}
{
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
}
{
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
}
{
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)
}
}
}
{}
{}
{ return template }
{}
{}
ComponentStats is a stub for non-wasm builds.
ComponentTimelineEntry is a stub for non-wasm builds.
App maintains registered hooks and exposes helper methods for plugins
to attach to framework events.
{}
{}
{}
{}
{}
{ return false }
RegisterRouter adds a router navigation hook.
{
a.routerHooks = append(a.routerHooks, fn)
}
RegisterStore adds a store mutation hook.
{
a.storeHooks = append(a.storeHooks, fn)
}
RegisterTemplate adds a template render hook.
{
a.templateHooks = append(a.templateHooks, fn)
}
RegisterLifecycle adds hooks for component mount and unmount.
{
if mount != nil {
a.mountHooks = append(a.mountHooks, mount)
}
if unmount != nil {
a.unmountHooks = append(a.unmountHooks, unmount)
}
}
RegisterRTMLVar registers a value that can be referenced from RTML as
{plugin:NAME.VAR}.
{
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 reports whether a plugin with the given name is installed.
{
if a.plugins == nil {
return false
}
_, ok := a.plugins[name]
return ok
}
newApp creates an App with initialized hook storage.
{
return &App{hooks: &hooks{}, pluginVars: make(map[string]map[string]any), plugins: make(map[string]Plugin)}
}
getRTMLVar retrieves a registered plugin variable.
{
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 is a convenience wrapper for plugins to expose variables.
{
app.RegisterRTMLVar(plugin, name, val)
}
RegisterPlugin registers a plugin and allows it to add hooks. If the plugin
implements Named and has already been installed, it is skipped.
{
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 invokes router hooks with the given path.
{
for _, h := range app.routerHooks {
h(path)
}
}
TriggerStore invokes store hooks for a mutation.
{
for _, h := range app.storeHooks {
h(module, store, key, value)
}
}
TriggerTemplate invokes template hooks with rendered HTML for a component.
{
for _, h := range app.templateHooks {
h(componentID, html)
}
}
TriggerMount invokes mount lifecycle hooks.
{
for _, h := range app.mountHooks {
h(c)
}
}
TriggerUnmount invokes unmount lifecycle hooks.
{
for _, h := range app.unmountHooks {
h(c)
}
}
{
state.StoreHook = TriggerStore
dom.TemplateHook = TriggerTemplate
}
{
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)
}
}
{
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
}
})
}
Tests complex conditional scenarios including @else-if and nested blocks.
{
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)
}
}
{
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)
}
}
Tests constructor decorators for refs and keyed lists.
{
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)
}
}
Tests plugin placeholders for variables, commands and constructors.
{
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)
}
}
{
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)
}
}
{
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)
}
}
{
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()
}
{
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))
}
}
}
{
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
}
{
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()
}
{
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 returns a snapshot of the component's render metrics.
{
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
}
{
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
}
{
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()
}
}
{
for _, dep := range c.Dependencies {
dep.Mount()
}
if c.component != nil {
c.component.OnMount()
}
}
{
return c.Name
}
{
return c.ID
}
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.
{
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))
}
{
if c.onMount != nil {
c.onMount(c)
}
}
{
if c.onUnmount != nil {
c.onUnmount(c)
}
}
{
c.onMount = fn
}
{
c.onUnmount = fn
}
{
c.onMount = onMount
c.onUnmount = onUnmount
return c
}
{
c.component = component
}
{
if c.Slots == nil {
c.Slots = make(map[string]any)
}
for k, v := range slots {
c.Slots[k] = v
}
}
Provide stores a value on this component so that descendants can
retrieve it with Inject. It creates the map on first use.
{
if c.provides == nil {
c.provides = make(map[string]any)
}
c.provides[key] = val
}
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.
{
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
}
{
if c.Props == nil {
c.Props = make(map[string]any)
}
for k, v := range params {
c.Props[k] = v
}
}
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.
{
c.HostComponent = name
}
{
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))
}
Stats returns zeroed metrics on non-wasm builds.
{ return ComponentStats{} }
{}
{}
{ return template }
{}
{}
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.
{
return "<root data-component-id=\"" + e.Child.GetID() + "\">" + e.Fallback + "</root>"
}
Render renders the child component, returning the fallback HTML if the child
panics or if a previous panic was recorded.
{
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 mounts the child component, updating the DOM with the fallback HTML if
the child panics during mounting.
{
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 delegates to the child component's Unmount method.
{ e.Child.Unmount() }
OnMount is a no-op for ErrorBoundary.
{}
OnUnmount is a no-op for ErrorBoundary.
{}
GetName returns the name of the component.
{ return "ErrorBoundary" }
GetID returns the wrapped child's ID.
{ return e.Child.GetID() }
SetSlots delegates slot assignment to the child component.
{
if e.Child != nil {
e.Child.SetSlots(slots)
}
}
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.
{
return &ErrorBoundary{Child: child, Fallback: fallback}
}
{ return nil }
{ p.installed++ }
{ return "named-test" }
{
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")
}
}
{ return nil }
{ p.installed++ }
{ return "dep" }
{ return nil }
{}
{ return "requires" }
{ return []Plugin{p.dep} }
{
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")
}
}
{ return nil }
{}
{ return "optional" }
{
if !p.enable {
return nil
}
return []Plugin{p.dep}
}
{
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")
}
}
import "crypto/sha1"
import "encoding/hex"
import "fmt"
import "log"
import "regexp"
import "runtime"
import "runtime/debug"
import "sort"
import "strings"
import "sync"
import "time"
import "github.com/rfwlab/rfw/v1/dom"
import "github.com/rfwlab/rfw/v1/hostclient"
hostclient
import "github.com/rfwlab/rfw/v1/js"
js
import "github.com/rfwlab/rfw/v1/state"
import "github.com/tdewolff/minify/v2"
import "github.com/tdewolff/minify/v2/css"
import "github.com/tdewolff/minify/v2/js"
tdJs
import "log"
import "github.com/rfwlab/rfw/v1/state"
import "fmt"
import "testing"
import "strings"
import "testing"
import "github.com/rfwlab/rfw/v1/state"
import "crypto/sha1"
import "fmt"
import "html"
import "reflect"
import "regexp"
import "sort"
import "strconv"
import "strings"
import "github.com/rfwlab/rfw/v1/dom"
import "github.com/rfwlab/rfw/v1/state"
import "errors"
import "github.com/rfwlab/rfw/v1/http"
http
import "fmt"
import "sync"
import "fmt"
import "sync"
import "testing"
import "encoding/json"
import "encoding/json"
import "log"
import "sync"
import "github.com/rfwlab/rfw/v1/dom"
import "github.com/rfwlab/rfw/v1/js"
js
import "time"
import "encoding/json"
import "github.com/rfwlab/rfw/v1/dom"
import "github.com/rfwlab/rfw/v1/state"
import "testing"
import "github.com/rfwlab/rfw/v1/state"
import "fmt"
import "regexp"
import "sort"
import "strings"
import "github.com/rfwlab/rfw/v1/dom"
import "github.com/rfwlab/rfw/v1/state"
import "strings"
import "testing"
import "strings"
import "testing"
import "github.com/rfwlab/rfw/v1/state"
import "fmt"
import "sync"
import "testing"
import "github.com/rfwlab/rfw/v1/dom"
import "encoding/json"
import "testing"