Skip to main content
cngx-src documentation

CngxTimeline

ComponentPrimaryOnPushNo encapsulationv0.1.0WCAG AA

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>

https://cngxjs.github.io/cngx/examples/#/ui/timeline/basics/flat-array https://cngxjs.github.io/cngx/examples/#/ui/timeline/basics/group-by-week https://cngxjs.github.io/cngx/examples/#/ui/timeline/basics/direction https://cngxjs.github.io/cngx/examples/#/ui/timeline/basics/ungrouped https://cngxjs.github.io/cngx/examples/#/ui/timeline/layout/horizontal https://cngxjs.github.io/cngx/examples/#/ui/timeline/layout/alternating-infographic https://cngxjs.github.io/cngx/examples/#/ui/timeline/layout/opposite-time https://cngxjs.github.io/cngx/examples/#/ui/timeline/integration/data-source https://cngxjs.github.io/cngx/examples/#/ui/timeline/integration/scroll-spy-nav https://cngxjs.github.io/cngx/examples/#/ui/timeline/async/error-retry https://cngxjs.github.io/cngx/examples/#/ui/timeline/async/refreshing-tail https://cngxjs.github.io/cngx/examples/#/ui/timeline/skins/activity-vs-narrative

Metadata#

Content Slots#

Host#

Providers#

CNGX_TIMELINE_MARKER_HOST
useExisting CngxTimeline

Relationships

Index#

Inputs#

ariaLabel#string | undefined
input()

Accessible name for the list. Falls back to the config's timelineRegion.

default undefined, { alias: 'aria-label' }
ariaLabelledBy#string | undefined
input()

Names the list from existing markup instead. Wins over aria-label.

default undefined, { alias: 'aria-labelledby' }
Required

Pulls the timestamp out of an event. Anything the Date constructor accepts, so ISO strings off an API response need no pre-mapping.

input()

'desc' (default) is newest-first; 'asc' reads oldest-first.

default 'desc'
emptyReason#EmptyReason
input()

Why 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.

default 'first-use'

Bucketing. 'day' (default), 'week', 'month', 'none' for a flat ungrouped list, or a custom function for anything else.

default 'day'
items#readonly T[]
input()

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.

default []
input()

Row raster. See CngxTimelineMode.

default 'narrative'
input()

Which axis the run reads along. See CngxTimelineOrientation.

default 'vertical'
input()

Which side of the rail rows sit on. See CngxTimelinePlacement.

default 'start'
input()

Whether the rail breaks between rows. See CngxTimelineRail.

default 'segmented'
skeletonRowCount#number
input()

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.

default 3
input()

Visual skin. See CngxTimelineSkin.

default 'line'

The 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.

default
undefined, { transform: (value) => (typeof value === 'string' ? undefined : value) }

Outputs#

retry#void
output()

Fires when the consumer asks to retry a failed load.

Instance Properties#

activeView#unknown
ProtectedReadonly
this.view.activeView
announcement#unknown
ProtectedReadonly
this.view.announcement
ariaBusy#unknown
ProtectedReadonly
this.view.ariaBusy
asyncState#CngxAsyncState
Readonly

Forwarding 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)
dateHeaderTpl#unknown
ProtectedReadonly
this.slots.dateHeader
emptyTpl#unknown
ProtectedReadonly
this.slots.empty
errorTpl#unknown
ProtectedReadonly
this.slots.error
Readonly

The derived bands, in sort order.

this.grouping.groups
idAccessor#unknown
Readonly

Stable 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)
itemTpl#unknown
ProtectedReadonly
this.slots.item
loadingTailTpl#unknown
ProtectedReadonly
this.slots.loadingTail
markerTpl#unknown
Readonly

Public: this is the CNGX_TIMELINE_MARKER_HOST contract the rows read.

this.slots.marker
refreshing#unknown
ProtectedReadonly
this.view.refreshing
retryButtonTpl#unknown
ProtectedReadonly
this.slots.retryButton
showsContent#unknown
ProtectedReadonly
this.view.showsContent
skeletonTpl#unknown
ProtectedReadonly
this.slots.skeleton

HostBindings#

BindingExpression
[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 surface
  • card - each row body lifted onto its own surface
  • bands - 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

--cngx-timeline-item-gap#<length>
Default value 16px

Vertical gap between consecutive rows inside one group. SET from --cngx-space-md.

--cngx-timeline-group-gap#<length>
Default value 24px

Vertical gap between two groups. SET from --cngx-space-lg.

--cngx-timeline-surface-padding#<length>
Default value 4px

Padding 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.

--cngx-timeline-skeleton-line-size#<length>
Default value 12px

Height 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].