OnChange fails to update event handler when function is a closure
I have code like the following, and it causes go-app to behave in a way I did not expect:
app.Range(c.expenses).Slice(func(i int) app.UI {
return app.Tr().
Body(
app.Td().
Class("text-end").
Body(
app.Input().
ID(fmt.Sprintf("expense-%d-selected", i)).
Type("checkbox").
OnChange(func(ctx app.Context, e app.Event) {
c.expenses[i].selected = ctx.JSSrc().Get("checked").Bool()
}),
...
}),
app.Range(c.jobs).Slice(func(i int) app.UI {
return app.Tr().
Body(
app.Td().
Class("text-end").
Body(
app.Input().
ID(fmt.Sprintf("job-%d-selected", i)).
Type("checkbox").
OnChange(func(ctx app.Context, e app.Event) {
c.jobs[i].selected = ctx.JSSrc().Get("checked").Bool()
}),
...
}),It appears that this code does not update the HTMLTd's "change" handler under certain circumstances under which an update is necessary. For example, imagine two passes through this code: (1) c.jobs has one item and c.expenses has two items and (2) c.jobs has two items and c.expenses has one. In both scenarios, this would generate three tr elements, with the final one differing only in its "change" handler across the two passes. (Some other UI element changes to cause the two passes to occur with these values.) In this scenario, the second pass will leave the third "change" handler unmodified and thus i = 1 in the context of the handler. But, this will cause an out-of-bounds access on c.expenses, which has one item on the pass even though index i = 1.
I studied go-app's pkg/app/node.go, and I found this in func (m nodeManager) updateHTMLEventHandlers(ctx Context, v HTML, newEvents eventHandlers):
if handler.Equal(newHandler) {
continue
}The continue prevents the new handler from being installed, as the Equal function does not seem to detect that the handler I have above is different (due to the i closure value) across the two passes. Here is the definition of the Equal function:
func (h eventHandler) Equal(v eventHandler) bool {
return h.event == v.event &&
h.scope == v.scope &&
h.passive == v.passive &&
reflect.ValueOf(h.goHandler).Pointer() == reflect.ValueOf(v.goHandler).Pointer()
}This does not appear to detect the difference between the two closures installed across the two passes described above. While the function logic is the same, the closure value for i is not.
Source: maxence-charriere/go-app