A powerful, flexible, and fully responsive Angular timeline component. Works with both MemberJunction entities and plain JavaScript objects. No external dependencies - pure HTML/CSS implementation.
npm install @memberjunction/ng-timeline
| Requirement | Version |
|---|---|
| Angular | 18+ |
| TypeScript | 5.0+ |
| @memberjunction/core | Optional (only for entity data sources) |
graph TB
subgraph "Data Layer"
A[TimelineGroup<T>] --> B[Plain Objects]
A --> C[BaseEntity Objects]
end
subgraph "Component Layer"
D[TimelineComponent] --> E[Segments]
E --> F[Event Cards]
D --> G[Virtual Scroll]
end
subgraph "Event System"
H[BeforeX Events] --> I{Cancel?}
I -->|No| J[Default Behavior]
I -->|Yes| K[Blocked]
J --> L[AfterX Events]
end
A --> D
D --> H
import { TimelineModule } from '@memberjunction/ng-timeline';
@NgModule({
imports: [TimelineModule]
})
export class YourModule { }
import { TimelineGroup } from '@memberjunction/ng-timeline';
interface MyEvent {
id: string;
title: string;
eventDate: Date;
description: string;
}
@Component({
template: `<mj-timeline [groups]="groups"></mj-timeline>`
})
export class MyComponent {
groups: TimelineGroup<MyEvent>[] = [];
ngOnInit() {
const group = TimelineGroup.FromArray(this.myEvents, {
titleField: 'title',
dateField: 'eventDate',
descriptionField: 'description'
});
this.groups = [group];
}
}
import { TimelineGroup } from '@memberjunction/ng-timeline';
import { TaskEntity } from '@memberjunction/core-entities';
@Component({
template: `<mj-timeline [groups]="groups"></mj-timeline>`
})
export class MyComponent {
groups: TimelineGroup<TaskEntity>[] = [];
ngOnInit() {
const group = new TimelineGroup<TaskEntity>();
group.EntityName = 'Tasks';
group.DataSourceType = 'entity';
group.Filter = "Status = 'Open'";
group.TitleFieldName = 'Name';
group.DateFieldName = 'DueDate';
this.groups = [group];
}
}
| Input | Type | Default | Description |
|---|---|---|---|
groups |
TimelineGroup<T>[] |
[] |
Data groups to display |
allowLoad |
boolean |
true |
Control deferred loading |
orientation |
'vertical' | 'horizontal' |
'vertical' |
Timeline orientation |
layout |
'single' | 'alternating' |
'single' |
Card layout (vertical only) |
sortOrder |
'asc' | 'desc' |
'desc' |
Event sort order |
segmentGrouping |
'none' | 'day' | 'week' | 'month' | 'quarter' | 'year' |
'month' |
Time segment grouping |
segmentsCollapsible |
boolean |
true |
Allow collapsing segments |
segmentsDefaultExpanded |
boolean |
true |
Segments start expanded |
defaultCardConfig |
TimelineCardConfig |
(see below) | Default card configuration |
virtualScroll |
VirtualScrollConfig |
(see below) | Virtual scroll settings |
emptyMessage |
string |
'No events to display' |
Empty state message |
emptyIcon |
string |
'fa-regular fa-calendar-xmark' |
Empty state icon |
enableKeyboardNavigation |
boolean |
true |
Enable keyboard navigation |
The component uses a BeforeX/AfterX event pattern. BeforeX events include a cancel property - set it to true to prevent the default behavior.
| Event | Args Type | Description |
|---|---|---|
beforeEventClick |
BeforeEventClickArgs<T> |
Before card click |
afterEventClick |
AfterEventClickArgs<T> |
After card click |
beforeEventExpand |
BeforeEventExpandArgs<T> |
Before card expand |
afterEventExpand |
AfterEventExpandArgs<T> |
After card expand |
beforeEventCollapse |
BeforeEventCollapseArgs<T> |
Before card collapse |
afterEventCollapse |
AfterEventCollapseArgs<T> |
After card collapse |
beforeEventHover |
BeforeEventHoverArgs<T> |
Before hover state change |
afterEventHover |
AfterEventHoverArgs<T> |
After hover state change |
beforeActionClick |
BeforeActionClickArgs<T> |
Before action button click |
afterActionClick |
AfterActionClickArgs<T> |
After action button click |
beforeSegmentExpand |
BeforeSegmentExpandArgs |
Before segment expand |
afterSegmentExpand |
AfterSegmentExpandArgs |
After segment expand |
beforeSegmentCollapse |
BeforeSegmentCollapseArgs |
Before segment collapse |
afterSegmentCollapse |
AfterSegmentCollapseArgs |
After segment collapse |
beforeLoad |
BeforeLoadArgs |
Before data load |
afterLoad |
AfterLoadArgs |
After data load |
// Refresh all data
await timeline.refresh();
// Load more (virtual scroll)
await timeline.loadMore();
// Expand/collapse all events
timeline.expandAllEvents();
timeline.collapseAllEvents();
// Expand/collapse all segments
timeline.expandAllSegments();
timeline.collapseAllSegments();
// Target specific events
timeline.expandEvent(eventId);
timeline.collapseEvent(eventId);
// Navigation
timeline.scrollToEvent(eventId, 'smooth');
timeline.scrollToDate(new Date(), 'smooth');
// Data access
const event = timeline.getEvent(eventId);
const allEvents = timeline.getAllEvents();
const group = new TimelineGroup<MyType>();
// Data Source
group.DataSourceType = 'array'; // or 'entity' for MJ
group.EntityObjects = myData; // For array mode
group.EntityName = 'Tasks'; // For entity mode
group.Filter = "Status='Open'"; // SQL filter (entity mode)
group.OrderBy = 'DueDate DESC'; // SQL order (entity mode)
// Field Mappings
group.TitleFieldName = 'title';
group.DateFieldName = 'eventDate';
group.SubtitleFieldName = 'category';
group.DescriptionFieldName = 'details';
group.ImageFieldName = 'thumbnailUrl';
group.IdFieldName = 'id'; // Defaults to 'ID' or 'id'
// Group Display
group.DisplayIconMode = 'custom';
group.DisplayIcon = 'fa-solid fa-check-circle';
group.DisplayColorMode = 'manual';
group.DisplayColor = '#4caf50';
group.GroupLabel = 'Completed Tasks';
// Card Configuration
group.CardConfig = {
collapsible: true,
defaultExpanded: false,
summaryFields: [
{ fieldName: 'Status', icon: 'fa-solid fa-circle' }
],
actions: [
{ id: 'view', label: 'View', variant: 'primary' }
]
};
// Custom Functions
group.SummaryFunction = (record) => `Priority: ${record.priority}`;
group.EventConfigFunction = (record) => ({
color: record.isUrgent ? '#f44336' : undefined
});
interface TimelineCardConfig {
// Header
showIcon?: boolean; // Show icon in header
showDate?: boolean; // Show date in header
showSubtitle?: boolean; // Show subtitle
dateFormat?: string; // Angular date format
// Image
imageField?: string; // Field containing image URL
imagePosition?: 'left' | 'top' | 'none';
imageSize?: 'small' | 'medium' | 'large';
// Body
descriptionField?: string; // Description field name
descriptionMaxLines?: number; // Max lines before truncate
allowHtmlDescription?: boolean;
// Expansion
collapsible?: boolean; // Allow expand/collapse
defaultExpanded?: boolean; // Start expanded
expandedFields?: TimelineDisplayField[]; // Fields in expanded view
summaryFields?: TimelineDisplayField[]; // Always visible fields
// Actions
actions?: TimelineAction[]; // Action buttons
actionsOnHover?: boolean; // Show actions only on hover
// Styling
cssClass?: string;
minWidth?: string;
maxWidth?: string; // Default: '400px'
}
@Component({
template: `
<mj-timeline
[groups]="groups"
(beforeEventClick)="onBeforeClick($event)"
(afterActionClick)="onAction($event)">
</mj-timeline>
`
})
export class MyComponent {
onBeforeClick(args: BeforeEventClickArgs<MyType>) {
// Prevent click on archived items
if (args.event.entity.status === 'archived') {
args.cancel = true;
this.showToast('Archived items cannot be opened');
}
}
onAction(args: AfterActionClickArgs<MyType>) {
switch (args.action.id) {
case 'view':
this.router.navigate(['/items', args.event.entity.id]);
break;
case 'edit':
this.openEditDialog(args.event.entity);
break;
}
}
}
Override any part of the card rendering:
<mj-timeline [groups]="groups">
<!-- Full card override -->
<ng-template #cardTemplate let-event="event" let-group="group">
<div class="my-card">
<h3>{{ event.title }}</h3>
<p>{{ event.description }}</p>
</div>
</ng-template>
<!-- Just override actions -->
<ng-template #actionsTemplate let-event="event" let-actions="actions">
<my-action-bar [event]="event"></my-action-bar>
</ng-template>
<!-- Custom empty state -->
<ng-template #emptyTemplate>
<div class="no-data">
<img src="empty.svg" />
<p>Nothing here yet!</p>
</div>
</ng-template>
</mj-timeline>
Available templates: cardTemplate, headerTemplate, bodyTemplate, actionsTemplate, segmentHeaderTemplate, emptyTemplate, loadingTemplate
Customize via CSS variables:
mj-timeline {
// Colors
--mj-timeline-line-color: #e0e0e0;
--mj-timeline-marker-bg: #1976d2;
--mj-timeline-card-bg: #ffffff;
--mj-timeline-card-border: #e0e0e0;
--mj-timeline-card-shadow: 0 2px 8px rgba(0,0,0,0.1);
--mj-timeline-text-primary: #212121;
--mj-timeline-text-secondary: #757575;
--mj-timeline-accent: #1976d2;
// Sizing
--mj-timeline-card-max-width: 400px;
--mj-timeline-card-padding: 16px;
--mj-timeline-marker-size: 14px;
// Animation
--mj-timeline-transition: 0.25s ease;
}
// Dark mode
.dark-theme mj-timeline {
--mj-timeline-card-bg: #1e1e1e;
--mj-timeline-card-border: #424242;
--mj-timeline-text-primary: #ffffff;
}
●─── Dec 2, 2025
│
│ ┌──────────────────────────┐
│ │ Task Completed │
│ │ Review timeline design │
│ │ [View] [Edit] │
│ └──────────────────────────┘
│
●─── Nov 28, 2025
│
│ ┌──────────────────────────┐
│ │ Feature Released │
│ │ New dashboard v2.5 │
│ └──────────────────────────┘
┌────────────────┐
│ Task Done │────●─── Dec 2
└────────────────┘ │
│
Nov 28 ───●────┤
│ │
└────┼────┌────────────────┐
│ │ Released │
│ └────────────────┘
▼ December 2025 (3 events) ────────────────────
│
│ [Events in December...]
│
► November 2025 (12 events) ─────────────────── ← Collapsed
| Key | Action |
|---|---|
↓ / → |
Move to next event |
↑ / ← |
Move to previous event |
Enter / Space |
Toggle expand/collapse |
Escape |
Collapse focused event |
Home |
Jump to first event |
End |
Jump to last event |
Configure for large datasets:
<mj-timeline
[groups]="groups"
[virtualScroll]="{
enabled: true,
batchSize: 25,
loadThreshold: 200,
showLoadingIndicator: true,
loadingMessage: 'Loading more...'
}">
</mj-timeline>
If migrating from the Kendo-based version:
@progress/kendo-* packagesGroups → groups, DisplayOrientation → orientation# From package directory
npm run build
# From monorepo root
turbo build --filter="@memberjunction/ng-timeline"
ISC
Fileoverview
Public API surface for @memberjunction/ng-timeline
This module exports all public types, classes, and components for the MJ Timeline component. The component works with both MemberJunction BaseEntity objects and plain JavaScript objects.