From 20412fe6770f6b3be9e7e64b914b9fd1a5fa8b47 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Tue, 8 Sep 2026 00:38:46 +0000
Subject: [PATCH 1/3] feat(crudview): add OnAfterReload hook to Config and
CrudView
Co-authored-by: cdvelop <44058491+cdvelop@users.noreply.github.com>
---
crudview/crud.go | 14 ++++++-
crudview/crudview.go | 14 +++++++
crudview/crudview_test.go | 84 +++++++++++++++++++++++++++++++++++++++
docs/ARCHITECTURE.md | 1 +
4 files changed, 112 insertions(+), 1 deletion(-)
diff --git a/crudview/crud.go b/crudview/crud.go
index 81418dd..d1ddef4 100644
--- a/crudview/crud.go
+++ b/crudview/crud.go
@@ -45,6 +45,17 @@ type Config struct {
// ListView) factory instead when the data wants a leading date/time
// badge (view.Item's LeadTop/Main/Bottom) rather than a plain label.
List func(selected *dom.SignalString, onSelect func(view.Item)) ListView
+
+ // OnAfterReload se invoca al final de Reload(), justo después de que la lista
+ // se re-llena con los items del presenter (list.SetItems). Recibe el list
+ // concreto ya pintado, para que el consumidor acomode detalles que el widget no
+ // puede derivar — p. ej. type-assert a *targetdate.TargetDate / *targethour.
+ // TargetHour y setear sus campos (FreeSlots). nil = hook ausente.
+ //
+ // Un solo argumento a propósito: items[] no viajan (list.Items() /
+ // Presenter.Items() ya los dan); lo único que NO se puede conseguir de otro
+ // lado es el list concrete que Config.List construyó.
+ OnAfterReload func(list ListView)
}
// New builds the renderer around an already-constructed Presenter. It generates the form from
@@ -82,7 +93,8 @@ func New(cfg Config) (*CrudView, error) {
// List is passed through as-is, nil included: Init resolves the
// same targetlist.TargetList default that a nil List would get
// here, so there is exactly one place that decision lives.
- List: cfg.List,
+ List: cfg.List,
+ OnAfterReload: cfg.OnAfterReload,
}
// Auto-save: every field commit (blur/change) persists immediately — see
diff --git a/crudview/crudview.go b/crudview/crudview.go
index 5982c5f..2cb83fa 100644
--- a/crudview/crudview.go
+++ b/crudview/crudview.go
@@ -135,6 +135,17 @@ type CrudView struct {
OnUpdated func(ids []string, err error)
OnCancel func()
+ // OnAfterReload se invoca al final de Reload(), justo después de que la lista
+ // se re-llena con los items del presenter (list.SetItems). Recibe el list
+ // concreto ya pintado, para que el consumidor acomode detalles que el widget no
+ // puede derivar — p. ej. type-assert a *targetdate.TargetDate / *targethour.
+ // TargetHour y setear sus campos (FreeSlots). nil = hook ausente.
+ //
+ // Un solo argumento a propósito: items[] no viajan (list.Items() /
+ // Presenter.Items() ya los dan); lo único que NO se puede conseguir de otro
+ // lado es el list concrete que Config.List construyó.
+ OnAfterReload func(list ListView)
+
// internal
form *form.Form // typed handle set by New; nil when standalone
list ListView // owns the row rendering + ⋮ menu
@@ -329,6 +340,9 @@ func (v *CrudView) Reload() error {
return err
}
v.filter()
+ if v.OnAfterReload != nil && v.list != nil {
+ v.OnAfterReload(v.list)
+ }
return nil
}
diff --git a/crudview/crudview_test.go b/crudview/crudview_test.go
index dfd1bdf..20d9042 100644
--- a/crudview/crudview_test.go
+++ b/crudview/crudview_test.go
@@ -5,6 +5,7 @@ package crudview
import (
"testing"
+ "webtyp.com/dom"
. "webtyp.com/fmt"
"webtyp.com/fmt/lang"
. "webtyp.com/html"
@@ -136,3 +137,86 @@ func TestCrudView_DeleteConfirm_Language(t *testing.T) {
}
}
}
+
+type stubList struct {
+ items []view.Item
+ selected *dom.SignalString
+}
+
+func (s *stubList) GetID() string { return "" }
+func (s *stubList) SetID(id string) {}
+func (s *stubList) String() string { return "" }
+func (s *stubList) Render() *dom.Element { return nil }
+func (s *stubList) Children() []dom.Component { return nil }
+func (s *stubList) SetItems(items []view.Item) { s.items = items }
+func (s *stubList) Items() []view.Item { return s.items }
+func (s *stubList) Count() int { return len(s.items) }
+func (s *stubList) SetSelectMode(on bool) {}
+func (s *stubList) SetDanger(on bool) {}
+func (s *stubList) CheckedIDs() []string { return nil }
+func (s *stubList) OnCheckedChange(fn func(n int)) {}
+
+func TestOnAfterReload_FiresWithList(t *testing.T) {
+ fb := fakeListBackend()
+ p := view.New(fb, &Device{})
+
+ var capturedList ListView
+ sList := &stubList{}
+
+ v := &CrudView{
+ Title: "OnAfterReload Test",
+ Presenter: p,
+ List: func(selected *dom.SignalString, onSelect func(view.Item)) ListView {
+ sList.selected = selected
+ return sList
+ },
+ OnAfterReload: func(list ListView) {
+ capturedList = list
+ },
+ }
+ v.Init(&mockCtx{})
+
+ if capturedList != ListView(sList) {
+ t.Errorf("expected capturedList to be sList, got %v", capturedList)
+ }
+}
+
+func TestOnAfterReload_AfterSetItems(t *testing.T) {
+ fb := fakeListBackend()
+ p := view.New(fb, &Device{})
+
+ var countInHook int
+ sList := &stubList{}
+
+ v := &CrudView{
+ Title: "OnAfterReload Test",
+ Presenter: p,
+ List: func(selected *dom.SignalString, onSelect func(view.Item)) ListView {
+ sList.selected = selected
+ return sList
+ },
+ OnAfterReload: func(list ListView) {
+ countInHook = len(list.Items())
+ },
+ }
+ v.Init(&mockCtx{})
+
+ if countInHook != len(fb.Rows) {
+ t.Errorf("expected countInHook == %d, got %d", len(fb.Rows), countInHook)
+ }
+}
+
+func TestOnAfterReload_NilNoop(t *testing.T) {
+ fb := &conformance.FakeLister{}
+ p := view.New(fb, &Device{})
+
+ v := &CrudView{
+ Title: "OnAfterReload Nil Test",
+ Presenter: p,
+ }
+ v.Init(&mockCtx{})
+
+ if err := v.Reload(); err != nil {
+ t.Fatalf("unexpected error on Reload: %v", err)
+ }
+}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 3e42154..f17c7bf 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -270,6 +270,7 @@ The high-level pattern for constructing a CRUD view is `crudview.New(Config)`. T
- Saves are validated and synced via `form.SyncValues` before shipping to `Presenter.Save`.
- `OnSave`/`OnDelete` are only wired when `Presenter.CanSave()`/`CanDelete()` are true.
- Empty search string placeholders default to `"Search…"`, but can be customized via `Presenter.SearchPlaceholder()`.
+- The `OnAfterReload func(list ListView)` hook runs at the end of `Reload()`, right after the list widget has been repopulated via `filter()`. It passes only the concrete `ListView` instance constructed by `Config.List`, adhering to the minimal API surface principle (items do not need to be passed separately as they are accessible directly via `list.Items()`).
#### Principle: Standard-shaped tests
From c2a13b00fd6ac7c6ee0af6957687e8f89a0358f2 Mon Sep 17 00:00:00 2001
From: Cesar Solis <44058491+cdvelop@users.noreply.github.com>
Date: Mon, 7 Sep 2026 21:54:07 -0300
Subject: [PATCH 2/3] chore: status transition to review [pr
https://github.com/webtyp/layout/pull/34]
---
docs/PLAN.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/PLAN.md b/docs/PLAN.md
index 58c3c0e..f5f369c 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -3,8 +3,9 @@ PLAN: "feat(crudview): OnAfterReload hook — consumer personalizes the list wid
TAG: v0.2.21
EXECUTOR: jules
REVIEWER: none
-STATUS: running
+STATUS: review
SESSION: 7064163444445900914
+PR: https://github.com/webtyp/layout/pull/34
---
# PLAN — `crudview.Config.OnAfterReload` (Etapa G del `DEMO_AGENDA_MASTER_PLAN`)
From 5db69ddd3e65308f1f2f4351bcfecfe0c126fa05 Mon Sep 17 00:00:00 2001
From: Cesar Solis <44058491+cdvelop@users.noreply.github.com>
Date: Mon, 7 Sep 2026 22:03:11 -0300
Subject: [PATCH 3/3] review: corrections before merge
---
docs/PLAN.md | 2 +-
docs/img/badges.svg | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/PLAN.md b/docs/PLAN.md
index f5f369c..c4c8b76 100644
--- a/docs/PLAN.md
+++ b/docs/PLAN.md
@@ -1,6 +1,6 @@
---
PLAN: "feat(crudview): OnAfterReload hook — consumer personalizes the list widget after every load"
-TAG: v0.2.21
+TAG: v0.2.23
EXECUTOR: jules
REVIEWER: none
STATUS: review
diff --git a/docs/img/badges.svg b/docs/img/badges.svg
index c023da6..4176845 100644
--- a/docs/img/badges.svg
+++ b/docs/img/badges.svg
@@ -51,7 +51,7 @@
text-anchor="middle" font-family="sans-serif" font-size="11" fill="white">Coverage
85.3%
+ text-anchor="middle" font-family="sans-serif" font-size="11" fill="white">85.4%