Event Participant List — Reference Implementation

1. Purpose

This page is a reading guide, not a design spec. The Event Participant list at tanstack-event-participant-list.component.ts is the first canonical bake of the project’s list-screen patterns. When building a new list screen — T02 Number Stock, M02 Membership Members, E04 Event Results, future Audit Log, future Affiliations — read the design docs first, then read the EP component side-by-side as the live reference.

The component is ~1,600 lines because it carries every pattern. A purely substrate-driven list screen with no stats strip, no side panel, no chip flags, and no row-summoned modal would be ~400 lines.

2. Reading order

  1. Filterable List Pattern — substrate (ColumnConfig, query adapter, widgets) and composition (out-of-band filters, side panel, bulk lifecycle, effects discipline, single navigate). Read fully.

  2. List Screen Affordances — visual + interaction spec for the chrome (page header, stats strip, filter chips, utilities row, hover panel, side panel). Read fully.

  3. C07 Data Table — the table itself (rows, headers, bulk shell). Skim — relevant when bulk-mode chrome surfaces.

  4. E02 Event Participants — the screen spec being implemented. Read fully.

  5. The component file below — read in the order suggested in Pattern map below; jump straight to the file:line citations for each pattern you need.

3. Pattern map

The component is structured as one config + one model + one service + one component + three presentational sub-components. The component itself follows a fixed section order via comment banners (/* ----- <name> ----- */). Read these sections:

Pattern File Approximate location Why read it

Column config — single source of truth

event-participant.config.ts

Whole file

One ColumnConfig per column — including the eventId path-pinned column with visible: 'hide-when-pinned'. Use this as the template for a new screen’s config.

Out-of-band filter signals (chip flags + showInactive)

event-participant.model.ts

IEventParticipantChipFlags, CHIP_FLAG_KEYS, EMPTY_CHIP_FLAGS

Co-located with the substrate types so all filter dimensions are visible together.

Path-param remap idiom

tanstack-event-participant-list.component.ts

Bottom of file — routeParamReader() helper + pathReader computed signal

The seam between Angular’s :id route convention and the column config’s apiField: 'eventId'. Adapt for any pinned dimension on a new screen.

Value-equal derived state signals

Same file

state and chipFlags computed(…​, { equal: stateEqual }); stateEqual / sortingEqual / filtersEqual / chipFlagsEqual helpers at the bottom

Without value-equality, every router emit re-fires the fetch effect in a loop. Copy the equality helpers verbatim — they’re not entity-specific.

Single navigate() entry-point + foreign-param preservation

Same file

Private navigate() near the bottom of the class (last method before the helper functions)

The discipline that keeps ?view=<id> alive across substrate-driven navigations. Every list screen with a side panel needs an equivalent.

Row-fetch effect with untracked + onCleanup

Same file

constructor() — first effect block; merges chip flags + scope toggle into the API params, strips leaked active.*, cancels in-flight requests on cleanup

The canonical effect-discipline bake. Note the untracked() wrapping for synchronous signal writes (NG0600) and the onCleanup hook for HttpClient cancellation.

Stats fetch effect (event-scoped, refetch only on scope change)

Same file

Constructor — second effect block

Stats counts are scope-driven, not filter-driven. Refetch only when the parent id changes.

Snapshot-fallback signals (side panel + cluster modal)

Same file

Constructor — third + fourth effect blocks; viewedSnapshot and clusterModalSnapshot field declarations

The pattern that keeps a side panel or row-summoned modal rendered when the row falls off the visible page mid-fetch.

Side panel via ?view=<id>

Same file

viewedEpId, viewedParticipant, openView(), closeView() near the middle of the class; <ap-event-participant-detail-panel> in the template at the very bottom

Browser-back closes the panel naturally. The panel component is purely presentational — input: row, output: close.

Bulk-selection lifecycle

Same file

selectedIds, bulkActive, isSelected/toggleSelected/allVisibleSelected/someVisibleSelected/toggleAllVisible/clearSelection group; the Page actions vs Bulk toolbar slot in the template

Selection cleared on every scope change by the row-fetch effect (single source of truth). Bulk toolbar replaces the action toolbar at ≥2 selected; row-level View action disables while bulk is active.

Sort header click behaviour

Same file

onSortClick() — cycles asc → desc → none; shift-click for additive multi-sort

~25-line state machine. Copy verbatim.

Stats strip + chip-flag toggling

Same file

Stats-strip <div> in the template; toggleChipFlag() method

[class.btn-primary]="chipFlags().firstTimeOnly" is the pressed-state idiom. aria-pressed for AT.

Hover panel anchor pattern

Same file

Order-cluster <td> in the template — the cluster-chip-anchor wrapper enclosing chip + hover-panel-anchor

The mouseenter/mouseleave region must span both the chip AND the panel; a chip-only listener unmounts the panel mid-read when the cursor crosses into the panel.

Row-summoned modal (cluster overview)

Same file

clusterModalEpId, clusterModalSnapshot, clusterReturnTarget, onClusterChipClick(), closeClusterModal(); <ap-event-cluster-overview-modal> at the bottom of the template

Captures event.currentTarget for return-focus; queueMicrotask on close so focus restoration runs after the DOM update.

Disabled-with-tooltip stub affordances

Same file

Action toolbar in the page header — every <span title="Coming soon"> wrapper

Disabled buttons don’t dispatch hover events on most browsers; the wrapper span is the tooltip surface. Project-wide convention.

4. Adapting for a new screen

The minimum surface a new list screen needs from this reference:

  1. Mirror the column-config / model split. The model module is the home for out-of-band signal types (chip flags, scope toggle keys) and view-model intersections that augment the typed-client DTO.

  2. Copy the equality helpers (stateEqual, sortingEqual, filtersEqual, chipFlagsEqual) and the routeParamReader adapter verbatim — they’re not entity-specific.

  3. Adapt the navigate() method’s foreign-param preservation list to the screen’s URL params (drop view if no side panel; add the screen’s specific ambient params).

  4. Decide which patterns the new screen needs:

    1. Stats strip → wire chip flags + a stats endpoint (or skip).

    2. Side panel → wire ?view= + snapshot signal pair (or skip; defer to a full-page detail screen).

    3. Row-summoned modal → wire the clusterModalEpId triple (state signal + snapshot + return-target) (or skip).

    4. Bulk actions → wire selectedIds + bulk toolbar (or skip — read-only screens don’t need it).

    5. Hover panel → wire the anchor wrapper pattern (or skip — most cells don’t need a rich hover).

  5. Compose the affordances per List Screen Affordances in the template — same vertical band order, same Bootstrap classes.

The presentational sub-components (side panel, hover panel, row-summoned modal) are screen-specific — they consume an entity’s data shape. The list-component patterns above are not.

5. Status

Reference implementation as of US #610 / Feature #608. Future refinements surface upstream in Filterable List Pattern and List Screen Affordances; this page tracks the EP component as the canonical bake until a second list-screen consumer surfaces refinements that justify cross-screen consolidation (then this page either gets companion entries or retires in favour of multi-screen pattern docs).

6. References