Skip to content

Commit 5e60532

Browse files
committed
Fix event handler state capture issue
Event handlers were capturing local variables by value instead of reading current state. This caused sequential operations to fail as each handler used stale state from initial render. Changes: - Track variables assigned from channel receives (e.g., currentState := <-stateChannel) - Skip generating local variable declarations for these tracked variables - Replace all references to tracked variables with c.current<ChannelName> field access - Fix applies to both simple identifiers and selector expressions (e.g., currentState.Display) This ensures event handlers always read the latest state value when clicked, enabling correct sequential calculator operations like "3 + 2 - 1 = 4"
1 parent f223bc0 commit 5e60532

2 files changed

Lines changed: 59 additions & 24 deletions

File tree

examples/calculator/calculator_gen.go

Lines changed: 16 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/codegen/codegen.go

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Generator struct {
2929
currentCompBody *guixast.Body // Current component body being generated
3030
currentComp *guixast.Component // Current component being generated
3131
componentParams map[string]bool // Track current component's parameter names
32+
channelReceiveVars map[string]string // Map local var names to channel names (e.g., "currentState" -> "StateChannel")
3233
verbose bool // Generate verbose logging statements
3334
}
3435

@@ -989,14 +990,20 @@ func (g *Generator) generateConstructor(comp *guixast.Component) *ast.FuncDecl {
989990
func (g *Generator) generateRenderMethod(comp *guixast.Component) *ast.FuncDecl {
990991
// Set up context for hoisted variable tracking
991992
g.hoistedVars = make(map[string]bool)
993+
g.channelReceiveVars = make(map[string]string)
992994
g.currentCompBody = comp.Body
993995

994-
// Collect hoisted variable names
996+
// Collect hoisted variable names and channel receive variables
995997
if comp.Body != nil {
996998
for _, varDecl := range comp.Body.VarDecls {
997-
if len(varDecl.Names) == 1 && len(varDecl.Values) == 1 &&
998-
g.inferTypeFromExpr(varDecl.Values[0]) != nil {
999-
g.hoistedVars[varDecl.Names[0]] = true
999+
if len(varDecl.Names) == 1 && len(varDecl.Values) == 1 {
1000+
if g.inferTypeFromExpr(varDecl.Values[0]) != nil {
1001+
g.hoistedVars[varDecl.Names[0]] = true
1002+
} else if varDecl.Values[0].Left != nil && varDecl.Values[0].Left.ChannelOp != nil {
1003+
// Track channel receive: currentState := <-stateChannel
1004+
channelName := varDecl.Values[0].Left.ChannelOp.Channel
1005+
g.channelReceiveVars[varDecl.Names[0]] = capitalize(channelName)
1006+
}
10001007
}
10011008
}
10021009
}
@@ -1087,8 +1094,13 @@ func (g *Generator) generateBody(body *guixast.Body) ast.Expr {
10871094
canHoist := len(varDecl.Names) == 1 && len(varDecl.Values) == 1 &&
10881095
g.inferTypeFromExpr(varDecl.Values[0]) != nil
10891096

1090-
if canHoist {
1091-
// Hoisted field - skip initialization in Render (already done in constructor)
1097+
// Check if this is a channel receive variable (skip - will be accessed via c.current<ChannelName>)
1098+
isChannelReceive := len(varDecl.Names) == 1 &&
1099+
g.channelReceiveVars != nil &&
1100+
g.channelReceiveVars[varDecl.Names[0]] != ""
1101+
1102+
if canHoist || isChannelReceive {
1103+
// Hoisted field or channel receive - skip initialization in Render
10921104
continue
10931105
} else {
10941106
// Local variable(s) - generate: name := value or n, err := value
@@ -1546,6 +1558,15 @@ func (g *Generator) generatePrimary(primary *guixast.Primary) ast.Expr {
15461558
}
15471559

15481560
if primary.Ident != "" {
1561+
// Check if it's a channel receive variable (e.g., currentState -> c.currentStateChannel)
1562+
if g.channelReceiveVars != nil {
1563+
if channelName, ok := g.channelReceiveVars[primary.Ident]; ok {
1564+
return &ast.SelectorExpr{
1565+
X: ast.NewIdent("c"),
1566+
Sel: ast.NewIdent("current" + channelName),
1567+
}
1568+
}
1569+
}
15491570
// Check if it's a hoisted variable (stored as-is in component struct)
15501571
if g.hoistedVars != nil && g.hoistedVars[primary.Ident] {
15511572
return &ast.SelectorExpr{
@@ -1710,7 +1731,22 @@ func (g *Generator) generateCallOrSelect(cos *guixast.CallOrSelect) ast.Expr {
17101731
}
17111732

17121733
// Build the base selector expression from base and fields
1713-
var expr ast.Expr = ast.NewIdent(cos.Base)
1734+
// Check if base is a channel receive variable that needs to be replaced
1735+
var expr ast.Expr
1736+
if g.channelReceiveVars != nil {
1737+
if channelName, ok := g.channelReceiveVars[cos.Base]; ok {
1738+
// Replace base with c.current<ChannelName>
1739+
expr = &ast.SelectorExpr{
1740+
X: ast.NewIdent("c"),
1741+
Sel: ast.NewIdent("current" + channelName),
1742+
}
1743+
} else {
1744+
expr = ast.NewIdent(cos.Base)
1745+
}
1746+
} else {
1747+
expr = ast.NewIdent(cos.Base)
1748+
}
1749+
17141750
for _, field := range cos.Fields {
17151751
expr = &ast.SelectorExpr{
17161752
X: expr,

0 commit comments

Comments
 (0)