MemberJunction: Responsive timeline component for Angular. Works with MemberJunction entities or plain JavaScript objects. No external dependencies.
npm install @memberjunction/ng-timelineA powerful, flexible, and fully responsive Angular timeline component. Works with both MemberJunction entities and plain JavaScript objects. No external dependencies - pure HTML/CSS implementation.
- Universal Compatibility: Works with MemberJunction BaseEntity objects OR plain JavaScript objects
- Responsive Design: Mobile-first approach with automatic layout adaptation
- Virtual Scrolling: Built-in support for large datasets with dynamic loading
- Time Segment Collapsing: Group events by day/week/month/quarter/year with collapsible sections
- Rich Event System: BeforeX/AfterX event pattern for full control from container components
- Customizable Cards: Configurable fields, images, actions, and custom templates
- Keyboard Navigation: Full accessibility with ARIA support
- Theming: CSS variables for easy customization including dark mode
- Zero Dependencies: No Kendo UI or other external libraries required
``bash`
npm install @memberjunction/ng-timeline
| Requirement | Version |
|------------|---------|
| Angular | 18+ |
| TypeScript | 5.0+ |
| @memberjunction/core | Optional (only for entity data sources) |
`mermaid
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
`
`typescript
import { TimelineModule } from '@memberjunction/ng-timeline';
@NgModule({
imports: [TimelineModule]
})
export class YourModule { }
`
`typescript
import { TimelineGroup } from '@memberjunction/ng-timeline';
interface MyEvent {
id: string;
title: string;
eventDate: Date;
description: string;
}
@Component({
template:
})
export class MyComponent {
groups: TimelineGroup
ngOnInit() {
const group = TimelineGroup.FromArray(this.myEvents, {
titleField: 'title',
dateField: 'eventDate',
descriptionField: 'description'
});
this.groups = [group];
}
}
`
`typescript
import { TimelineGroup } from '@memberjunction/ng-timeline';
import { TaskEntity } from '@memberjunction/core-entities';
@Component({
template:
})
export class MyComponent {
groups: TimelineGroup
ngOnInit() {
const group = new TimelineGroup
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 | [] | 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 | Before card click |afterEventClick
| | AfterEventClickArgs | After card click |beforeEventExpand
| | BeforeEventExpandArgs | Before card expand |afterEventExpand
| | AfterEventExpandArgs | After card expand |beforeEventCollapse
| | BeforeEventCollapseArgs | Before card collapse |afterEventCollapse
| | AfterEventCollapseArgs | After card collapse |beforeEventHover
| | BeforeEventHoverArgs | Before hover state change |afterEventHover
| | AfterEventHoverArgs | After hover state change |beforeActionClick
| | BeforeActionClickArgs | Before action button click |afterActionClick
| | AfterActionClickArgs | 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 |
`typescript
// 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();
`
`typescript
const group = new TimelineGroup
// 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
});
`typescript
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'
}
`
`typescript
@Component({
template:
(beforeEventClick)="onBeforeClick($event)"
(afterActionClick)="onAction($event)">
})
export class MyComponent {
onBeforeClick(args: BeforeEventClickArgs
// Prevent click on archived items
if (args.event.entity.status === 'archived') {
args.cancel = true;
this.showToast('Archived items cannot be opened');
}
}
onAction(args: AfterActionClickArgs
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:
`html
{{ event.description }}
{{ event.title }}
Nothing here yet!
`
Available templates: cardTemplate, headerTemplate, bodyTemplate, actionsTemplate, segmentHeaderTemplate, emptyTemplate, loadingTemplate
Customize via CSS variables:
`scss
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:
`typescript`
[virtualScroll]="{
enabled: true,
batchSize: 25,
loadThreshold: 200,
showLoadingIndicator: true,
loadingMessage: 'Loading more...'
}">
- Chrome (last 2 versions)
- Firefox (last 2 versions)
- Safari (last 2 versions)
- Edge (last 2 versions)
- iOS Safari
- Android Chrome
If migrating from the Kendo-based version:
1. No Kendo dependencies - Remove all @progress/kendo-* packagesGroups
2. Property names changed - → groups, DisplayOrientation → orientation
3. New event system - Replace Kendo events with BeforeX/AfterX pattern
4. Segments are new - Time-based grouping is now built-in
`bashFrom package directory
npm run build
ISC