Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion crudview/crud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions crudview/crudview.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
84 changes: 84 additions & 0 deletions crudview/crudview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package crudview
import (
"testing"

"webtyp.com/dom"
. "webtyp.com/fmt"
"webtyp.com/fmt/lang"
. "webtyp.com/html"
Expand Down Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions docs/PLAN.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
---
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: running
STATUS: review
SESSION: 7064163444445900914
PR: https://github.com/webtyp/layout/pull/34
---

# PLAN — `crudview.Config.OnAfterReload` (Etapa G del `DEMO_AGENDA_MASTER_PLAN`)
Expand Down
2 changes: 1 addition & 1 deletion docs/img/badges.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.