Server-side paginated table with sortable headers, optional virtual scrolling, and single / multiple row selection. The component is fetch-driven — provide a BsDatatableFetch callback that resolves a PaginationResponse, and the table handles paging UI, sort state, and (optionally) the virtual viewport.
It has no shadow root. Anything you render into it — row templates, node templates, cell renderers — stays in the document, so your app's stylesheet, your own component styles and Bootstrap's utility classes reach it normally.
Its own CSS is scoped at build time onto a data-mps attribute, the same device Angular uses for _ngcontent, so it cannot leak out onto your markup.
The trade-off runs the other way: page CSS now reaches this component's internals, exactly as emulated encapsulation behaves everywhere else. ::part() and ::slotted() no longer address it — use ordinary CSS selectors instead.
With [virtualScroll] + [fetch] the table fetches only the pages whose rows are in (or near) the viewport — against the same real, server-paged artist endpoint as the basic section. Scroll and watch the fetch log below: it never drains the whole result set up front; placeholder rows hold the scroll position until each window arrives. Sorting or changing settings drops the cached windows and refetches the visible range.
Pages fetched: — (0 so far)
Click a chevron to expand a row. Children load lazily on first expand and stay cached for subsequent collapse/expand cycles. Placeholder rows reserve viewport space while the fetch is in flight so the scrollbar stays accurate. Cascading selection: checking a parent selects all currently-loaded descendants; partially-selected parents render an indeterminate state.
Mark a column filterable and the table grows a second header row with a dropdown trigger in that column. Columns without it get an empty, correctly-sized cell, so the row stays aligned. No filterable column, no second row at all — toggle the checkbox below to watch the row appear and disappear.
A filterable column gets a built-in panel for free: a search box, an include/exclude toggle, a checkbox list of the column's distinct values, and a clear button. It lives in the web component, so React and Vue get the same panel with no wrapper code. The Artist column below uses it as-is.
A checkbox list is the wrong question for a quantity, so a column can ask a different one: filterMode: 'comparison' renders an operator (=, ≠, <, ≤, >, ≥) and a single input, with filterInputType choosing number (the default) or date — the Year started column below. Still the built-in panel: no template and no styling of your own. Narrow the operator list with filterOperators when only some make sense.
Quantities only — there is no text input type and no contains operator. Comparing strings is either exact match, which the value list already does better, or a lexicographic >, which is almost never what anyone means. A free-text filter is a nest-your-own case, below.
The consumer picks the mode per column and the component never infers it: a numeric column is often an enum and a string column is often ordinal, so any guess would be wrong about half the time.
When neither built-in question fits, nest a *bsDatatableFilterPanel inside the column and render whatever you like — the Year quit column does. The template receives the loaded values as a signal and a context object for driving the search and the selection. That markup is yours, so you style it yourself: Bootstrap's .form-control is only styled inside bs-* components in this workspace, and the panel renders in the document-root overlay, outside the table's DOM.
The panel opens in an overlay at the document root, so it is not clipped by the table's scroll container and is not hidden behind the sticky header — this demo runs in virtual-scroll mode, where both would otherwise happen.
The component decides nothing about what a filter means. It collects a selection and emits it; the page below holds an unfiltered master copy, applies the selection itself, and sets filterActive and filterSummary back on the column. That is what lets the same row drive a client-side match here and a server query elsewhere.
Keyboard:Tab reaches each filter trigger; Enter or Space opens the panel and moves focus to its search box; Tab cycles within the open panel; Esc closes it and returns focus to the trigger. Clearing a filter moves focus back to the search box, because the clear button disables itself.