CngxTimeline
projects/ui/timeline/timeline.component.ts
Import#
import { CngxTimeline } from '@cngx/ui/timeline'
Description#
Grouped, themed, RTL-safe timeline over a flat list of events.
Data in, rows out: bind [items] and a [dateAccessor], project one
*cngxTimelineItem template, and the organism buckets, sorts and renders
the rest. Bucketing is not its own code - it resolves
CNGX_TIMELINE_GROUPING_FACTORY and derives everything from one
computed(), so an app can swap in fiscal quarters or server-supplied
grouping without forking the component.
ARIA. One chain in two configurations, both derived rather than set:
grouped is group -> list -> listitem, one list per band named by its
date header, so a screen reader counts items per band rather than across
the whole history. The header sits beside that list, never inside it - a
list may own nothing but listitem, and the header is a consumer slot.
groupBy="none" moves list up to the container and collapses the chain
to list -> listitem.
Not keyboard-navigable, deliberately. v1 items are content, not widgets - links and buttons inside a row are natively tabbable in DOM order, which is the right behaviour for a read-only history. Roving tabindex becomes mandatory the moment a row is selectable, and that is a v2 concern.
<cngx-timeline [items]="events()" [dateAccessor]="at" groupBy="day">
<ng-template [cngxTimelineItem]="events()" let-event let-last="last">
<cngx-timeline-item [position]="last ? 'last' : 'middle'">
<cngx-time cngxTimelineTime [date]="event.at" />
<p>{{ event.summary }}</p>
</cngx-timeline-item>
</ng-template>
</cngx-timeline>Metadata#
Content Slots#
Host#
Providers#
CNGX_TIMELINE_MARKER_HOST- useExisting
CngxTimeline CNGX_STATEFUL- useFactory
(): { readonly state: CngxAsyncState} => ({ state: inject(CngxTimeline).asyncState, })
Relationships
Index#
Properties
Inputs
Outputs
Inputs#
string | undefinedAccessible name for the list. Falls back to the config's timelineRegion.
undefined, { alias: 'aria-label' }string | undefinedNames the list from existing markup instead. Wins over aria-label.
undefined, { alias: 'aria-labelledby' }TimelineDateAccessorPulls the timestamp out of an event. Anything the Date constructor
accepts, so ISO strings off an API response need no pre-mapping.
TimelineDirection'desc' (default) is newest-first; 'asc' reads oldest-first.
'desc'EmptyReasonWhy the timeline is empty, forwarded to *cngxTimelineEmpty. The
organism cannot infer this - only the consumer knows whether a filter
cleared the list or nothing has happened yet.
'first-use'TimelineGroupByBucketing. 'day' (default), 'week', 'month', 'none' for a flat
ungrouped list, or a custom function for anything else.
'day'readonly T[]The flat event list. Order is irrelevant - the presenter sorts.
A bound [state] supplies the rows instead as soon as it has data;
until then these are still used, so binding both is a legitimate way to
render a seed list while the first load runs. Note that the body still
follows the state, so an idle state renders nothing whatever [items]
holds.
[]CngxTimelineOrientationWhich axis the run reads along. See CngxTimelineOrientation.
'vertical'CngxTimelinePlacementWhich side of the rail rows sit on. See CngxTimelinePlacement.
'start'How many placeholder rows the loading body draws. Match it to the usual length so the skeleton reserves roughly the space the content will take.
3The list's async state. Wins over [items], and drives the whole body:
skeleton on first load, error surface with a retry, empty surface, and a
refreshing tail over content that stays on screen.
Also republished through CNGX_STATEFUL, so a transition bridge inside
the timeline needs no binding of its own.
undefined, { transform: (value) => (typeof value === 'string' ? undefined : value) }Outputs#
Instance Properties#
CngxAsyncStateForwarding façade over the bound [state], published through
CNGX_STATEFUL. Every member delegates, so swapping the bound state (or
binding none at all) never leaves a bridge holding a stale object.
createForwardedAsyncState(this.state)unknownStable event identity, used as the @for track expression so a row
keeps its DOM across a refetch instead of being torn down and rebuilt.
It deliberately does not drive band reuse: a refetch returns new objects at the same ids, and reusing a band on an id match would pin it to the old payload.
input<((item: T) => unknown) | undefined>(undefined)unknownPublic: this is the CNGX_TIMELINE_MARKER_HOST contract the rows read.
this.slots.markerHostBindings#
| Binding | Expression |
|---|---|
[attr.data-mode] | mode() |
[attr.data-skin] | skin() |
[attr.data-placement] | placement() |
[attr.data-rail] | rail() |
[attr.data-orientation] | orientation() |
[attr.data-ungrouped] | ungrouped() ? "" : null |
[attr.aria-label] | null |
[attr.aria-labelledby] | null |
Default visuals for <cngx-timeline> - the group and item stacking the
organism owns, on top of the per-row raster CngxTimelineItem ships.
The organism deliberately owns very little: the rail, the dot and the row grid all live with the atoms, so an item still renders complete without this file. What is here is the vertical rhythm between rows and between bands, plus the three skins.
Density
Every spacing token the organism owns is registered and SET from
--cngx-space-* in this one file, so a registration cannot outlive its
derivation and a [data-density] swap on any ancestor re-scales them.
--cngx-timeline-surface-padding is the organism's own padding; the row's
--cngx-timeline-content-padding is registered and SET in
timeline-item.component.css, and is out of scope for anything outside a
.cngx-timeline-item.
--cngx-timeline-gap and --cngx-timeline-row-gap belong to the row and
are registered there, but are SET a second time on this host: the skeleton
restates the row raster without borrowing .cngx-timeline-item, so it
never enters the scope that derives them and would otherwise render at the
registered literals while real rows track density.
Skins
[data-skin] is thematic only - no skin changes structure, ARIA or slot
behaviour:
line(default) - bare rail, no surfacecard- each row body lifted onto its own surfacebands- alternating group tint for long timelines
Modes and placement
[data-mode] and [data-placement] are read by
timeline-item.component.css, not here - the raster belongs to the row
that draws it. The organism only sets the attributes and, per row wrapper,
[data-row-side] derived from the loop index.
The cngx-timeline container name is a cross-library contract
:scope below declares container-type: inline-size and
container-name: cngx-timeline. @cngx/common's
timeline-item.component.css queries that exact name to collapse
placement="alternate" back to a single side below 30rem - the sm
rung of the responsive tier table. CSS offers
nothing tighter: container-name cannot be a custom property, and an
unnamed @container would match the nearest container of any name, which
is worse. So the name is API, not an implementation detail - renaming it
in an ejected skin silently disables the collapse. e2e/timeline-layout.spec.ts
asserts the degrade fires, so a rename breaks a test rather than a layout.
Index#
Layout
<length>16pxVertical gap between consecutive rows inside one group.
SET from --cngx-space-md.
<length>24pxVertical gap between two groups.
SET from --cngx-space-lg.
<length>4pxPadding around the organism's own surfaces - empty, error, refreshing
tail. Distinct from --cngx-timeline-content-padding, which belongs to
the row and is SET at the row host where the row can see it.
SET from --cngx-space-xs.
<length>12pxHeight of one placeholder bar in the loading body. Deliberately outside
the density scale and not SET from it: the bar is a fixed affordance,
and the surrounding raster already compacts with [data-density].