Centralizes date calculation logic into DateColumnDataSource Improves separation of concerns across rendering components Key changes: - Introduces IColumnInfo interface for flexible column data - Moves date calculation from multiple managers to dedicated datasource - Removes duplicate date rendering logic - Prepares architecture for future resource-based views
24 KiB
DataSource Architecture Implementation
Date: November 13, 2025 Type: Architecture refactoring, Data flow optimization Status: ✅ Complete (with course corrections) Main Goal: Implement DataSource pattern to centralize date calculation and prepare for dual-mode calendar (date-based vs resource-based)
Executive Summary
Successfully implemented DataSource architecture to centralize date calculations and clean up rendering responsibilities. Initial implementation had architectural violations that were corrected through user guidance.
Key Outcomes:
- ✅ DateColumnDataSource created as single source of truth for dates
- ✅ GridRenderer responsibility corrected (structure only, not event rendering)
- ✅ ColumnRenderer receives dates instead of calculating them
- ✅ Navigation flow fixed (no duplicate rendering)
- ✅ Clean separation of concerns restored
- ⚠️ Initial conversation compaction caused architectural mistakes
Code Volume:
- Created: 2 new files (DateColumnDataSource.ts, ColumnDataSource.ts interface)
- Modified: 5 files (GridManager.ts, NavigationManager.ts, GridRenderer.ts, ColumnRenderer.ts, EventTypes.ts)
User Corrections & Course Changes
Correction #1: GridRenderer Event Rendering Violation
What Happened: After conversation compaction, I mistakenly added event rendering logic directly into GridRenderer.
Code I Added (WRONG):
// GridRenderer.ts - renderColumnContainer()
private renderColumnContainer(dates: Date[], events: ICalendarEvent[]): void {
dates.forEach(date => {
const eventsForDate = events.filter(event => {...});
// Created columns...
// ❌ WRONG: Rendering events directly in GridRenderer
this.renderColumnEvents(eventsForDate, eventsLayer);
});
}
// Added entire event layout logic to GridRenderer
private renderColumnEvents() { ... }
private renderGridGroup() { ... }
private renderEvent() { ... }
User Intervention:
"hvorfor har du flyttet eventrendering iind i griidrendere?
det er jo columnsrenderes opgave"
followed by:
"ja naturligivs da"
Problem:
- GridRenderer should ONLY create grid structure (skeleton)
- Event rendering is EventRenderingService's responsibility
- ColumnRenderer creates columns, not GridRenderer's loop
- Violated separation of concerns
- Duplicated logic that already existed in DateEventRenderer
Correct Solution:
// GridRenderer.ts - renderColumnContainer()
private renderColumnContainer(dates: Date[], events: ICalendarEvent[]): void {
// Delegate to ColumnRenderer
this.columnRenderer.render(columnContainer, {
dates: dates,
config: this.config
});
// Events rendered by EventRenderingService via GRID_RENDERED event
}
Lesson: After conversation compaction, I lost context about architectural boundaries. GridRenderer = structure, EventRenderingService = events, ColumnRenderer = columns. Never mix responsibilities.
Correction #2: NavigationManager Configuration Injection
What Happened: I attempted to inject Configuration into NavigationManager so it could calculate dates.
User Intervention:
"hvad er det her du har lavet... NavigationManager skal bruge Configuration
for at få adgang til workweek settings, så den kan beregne dates.
Lad mig opdatere det:
ja det vi nok. Men hva dbruger GridManager det event tiil?"
Problem:
- NavigationManager doesn't need to know about workweek settings
- Date calculation should be DataSource's responsibility
- NavigationManager should focus on animation, not data
Initial Wrong Approach:
// NavigationManager.ts
constructor(..., config: Configuration) {
this.config = config;
}
private animateTransition() {
const workWeekSettings = this.config.getWorkWeekSettings();
const dates = this.dateService.getWorkWeekDates(targetWeek, workWeekSettings.workDays);
// ❌ NavigationManager calculating dates manually
}
Correct Solution:
// NavigationManager.ts
constructor(..., config: Configuration) {
this.config = config;
this.dataSource = new DateColumnDataSource(dateService, config, this.currentWeek, 'week');
}
private animateTransition() {
this.dataSource.setCurrentDate(targetWeek);
const dates = this.dataSource.getColumns();
// ✅ DataSource calculates dates
}
Lesson: NavigationManager needed Configuration, but not to calculate dates itself. It needed it to create a DataSource instance, which handles date calculation.
Correction #3: GridManager Navigation Rendering Conflict
What Happened:
GridManager was calling render() when receiving NAVIGATION_COMPLETED event, creating conflict with NavigationManager's animation.
User Analysis: I explained the problem flow:
1. NavigationManager creates NEW grid with animation
2. After animation: emits NAVIGATION_COMPLETED
3. GridManager listens and calls render() → updates OLD grid
4. Result: Two grids, broken state
User response:
"ja" (approved the fix)
Problem:
// GridManager.ts - BEFORE (WRONG)
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => {
this.currentDate = detail.newDate;
this.dataSource.setCurrentDate(this.currentDate);
this.render(); // ❌ Re-renders, conflicts with NavigationManager's new grid
});
Correct Solution:
// GridManager.ts - AFTER (CORRECT)
// Listen for navigation events from NavigationManager
// NavigationManager has already created new grid with animation
// GridManager only needs to update state, NOT re-render
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => {
this.currentDate = detail.newDate;
this.dataSource.setCurrentDate(this.currentDate);
// Do NOT call render() - NavigationManager already created new grid
});
Lesson: Two managers can't both render during navigation. NavigationManager owns animation rendering, GridManager only syncs state.
Implementation Details
Phase 1: DateColumnDataSource Creation
File Created: src/datasources/DateColumnDataSource.ts
Purpose: Single source of truth for date calculation based on workweek settings.
export class DateColumnDataSource implements IColumnDataSource {
private dateService: DateService;
private config: Configuration;
private currentDate: Date;
private currentView: CalendarView;
public getColumns(): Date[] {
switch (this.currentView) {
case 'week':
return this.getWeekDates();
case 'month':
return this.getMonthDates();
case 'day':
return [this.currentDate];
}
}
private getWeekDates(): Date[] {
const weekStart = this.getISOWeekStart(this.currentDate);
const workWeekSettings = this.config.getWorkWeekSettings();
return this.dateService.getWorkWeekDates(weekStart, workWeekSettings.workDays);
}
}
Key Methods:
getColumns()- Returns Date[] based on current viewsetCurrentDate(date)- Updates current datesetCurrentView(view)- Switches between day/week/monthgetType()- Returns 'date' (vs 'resource' for future mode)
Phase 2: GridManager Integration
File Modified: src/managers/GridManager.ts
Before:
// GridManager calculated dates itself
const weekStart = this.getISOWeekStart(this.currentDate);
const workWeekSettings = this.config.getWorkWeekSettings();
const dates = this.dateService.getWorkWeekDates(weekStart, workWeekSettings.workDays);
After:
// GridManager uses DataSource
constructor(...) {
this.dataSource = new DateColumnDataSource(dateService, config, this.currentDate, this.currentView);
}
public async render(): Promise<void> {
const dates = this.dataSource.getColumns(); // Single source of truth
const events = await this.eventManager.getEventsForPeriod(dates[0], dates[dates.length - 1]);
this.gridRenderer.renderGrid(this.container, this.currentDate, this.currentView, dates, events);
eventBus.emit(CoreEvents.GRID_RENDERED, {
container: this.container,
currentDate: this.currentDate,
dates: dates
});
}
Navigation Handling Fixed:
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => {
this.currentDate = detail.newDate;
this.dataSource.setCurrentDate(this.currentDate);
// Removed render() call - NavigationManager already created new grid
});
Phase 3: NavigationManager DataSource Integration
File Modified: src/managers/NavigationManager.ts
Added:
import { DateColumnDataSource } from '../datasources/DateColumnDataSource';
import { Configuration } from '../configurations/CalendarConfig';
constructor(..., config: Configuration) {
this.config = config;
this.dataSource = new DateColumnDataSource(dateService, config, this.currentWeek, 'week');
}
Animation Flow Updated:
private animateTransition(direction: 'prev' | 'next', targetWeek: Date): void {
// Update DataSource with target week and get dates
this.dataSource.setCurrentDate(targetWeek);
const dates = this.dataSource.getColumns();
// Create new grid with correct dates
newGrid = this.gridRenderer.createNavigationGrid(container, dates);
// Animate transition...
// After animation: emit NAVIGATION_COMPLETED
this.eventBus.emit(CoreEvents.NAVIGATION_COMPLETED, {
direction,
newDate: this.currentWeek
});
}
Phase 4: GridRenderer Responsibility Cleanup
File Modified: src/renderers/GridRenderer.ts
Removed:
- WorkHoursManager injection (ColumnRenderer handles it)
- Event rendering logic (EventRenderingService handles it)
- Column creation loop (ColumnRenderer handles it)
Kept:
- Grid skeleton creation (swp-grid-container, swp-time-grid)
- Time axis rendering
- Delegation to ColumnRenderer
Before (WRONG - after conversation compaction):
private renderColumnContainer(dates: Date[], events: ICalendarEvent[]): void {
dates.forEach(date => {
const column = document.createElement('swp-day-column');
// Apply work hours styling
this.applyWorkHoursStyling(column, date);
// Render events
this.renderColumnEvents(eventsForDate, eventsLayer);
});
}
After (CORRECT):
private renderColumnContainer(dates: Date[], events: ICalendarEvent[]): void {
// Delegate to ColumnRenderer
this.columnRenderer.render(columnContainer, {
dates: dates,
config: this.config
});
// Events rendered by EventRenderingService via GRID_RENDERED event
}
Phase 5: ColumnRenderer Context Update
File Modified: src/renderers/ColumnRenderer.ts
Context Interface Changed:
// Before
export interface IColumnRenderContext {
currentWeek: Date;
config: Configuration;
}
// After
export interface IColumnRenderContext {
dates: Date[]; // ← Receives dates instead of calculating them
config: Configuration;
}
Render Method Updated:
// Before
render(columnContainer: HTMLElement, context: IColumnRenderContext): void {
const { currentWeek, config } = context;
const workWeekSettings = config.getWorkWeekSettings();
const dates = this.dateService.getWorkWeekDates(currentWeek, workWeekSettings.workDays);
// ❌ ColumnRenderer calculating dates
}
// After
render(columnContainer: HTMLElement, context: IColumnRenderContext): void {
const { dates } = context;
// ✅ ColumnRenderer receives dates from DataSource
dates.forEach((date) => {
const column = document.createElement('swp-day-column');
this.applyWorkHoursToColumn(column, date);
// ...
});
}
Architecture Flow
Before DataSource:
┌──────────────────┐
│ GridManager │ ← Calculates dates manually
└────────┬─────────┘
│
▼
┌──────────────────┐
│ GridRenderer │ ← No dates parameter
└────────┬─────────┘
│
▼
┌──────────────────┐
│ ColumnRenderer │ ← Calculates dates again (duplicate logic)
└──────────────────┘
NavigationManager: Also calculates dates separately
After DataSource:
┌─────────────────────┐
│ DateColumnDataSource│ ← Single source of truth for dates
└──────────┬──────────┘
│
┌────┴────────────────────┐
│ │
┌─────▼────────┐ ┌────────▼──────────┐
│ GridManager │ │ NavigationManager │
└─────┬────────┘ └────────┬──────────┘
│ │
│ dates[] │ dates[]
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ GridRenderer │ │ GridRenderer │
└────────┬─────────┘ │ (createNavGrid) │
│ └──────────────────┘
▼
┌──────────────────┐
│ ColumnRenderer │ ← Receives dates, creates columns
└────────┬─────────┘
│
▼
┌──────────────────┐
│ swp-day-column │ ← Column structure with work hours
└──────────────────┘
EventRenderingService ← Listens to GRID_RENDERED, renders events
Responsibilities After Refactoring
DateColumnDataSource
- ✅ Calculate dates based on view (day/week/month)
- ✅ Apply workweek settings
- ✅ Single source of truth for dates
GridManager
- ✅ Coordinate rendering flow
- ✅ Get dates from DataSource
- ✅ Fetch events from EventManager
- ✅ Call GridRenderer with dates + events
- ✅ Emit GRID_RENDERED event
- ❌ NOT calculate dates itself
- ❌ NOT render during NAVIGATION_COMPLETED
NavigationManager
- ✅ Handle animation
- ✅ Use DataSource to get dates for new grid
- ✅ Create new grid via GridRenderer.createNavigationGrid()
- ✅ Emit NAVIGATION_COMPLETED after animation
- ❌ NOT calculate dates manually
GridRenderer
- ✅ Create grid skeleton structure
- ✅ Render time axis
- ✅ Delegate column creation to ColumnRenderer
- ❌ NOT create columns directly
- ❌ NOT render events
- ❌ NOT handle work hours styling
ColumnRenderer
- ✅ Receive dates from context
- ✅ Create swp-day-column elements
- ✅ Apply work hours styling
- ✅ Create eventsLayer structure
- ❌ NOT calculate dates itself
EventRenderingService
- ✅ Listen to GRID_RENDERED event
- ✅ Render events into columns
- ✅ Handle event layout (stacked vs grid)
- ❌ NOT be called directly by GridRenderer
Challenges & Solutions
Challenge 1: Conversation Compaction Memory Loss
Issue: After conversation compaction, I lost context about architectural boundaries and started violating separation of concerns.
Impact:
- Added event rendering to GridRenderer
- Added column creation loop to GridRenderer
- Duplicated logic that already existed
Solution: User corrections guided me back to correct architecture:
- GridRenderer delegates to ColumnRenderer
- EventRenderingService handles events via GRID_RENDERED
- DataSource calculates dates, not managers
Lesson: After conversation compaction, explicitly review architectural boundaries before implementing. Ask about responsibilities if unsure.
Challenge 2: Navigation Rendering Conflict
Issue: Both NavigationManager and GridManager tried to render during navigation, creating duplicate grids.
Root Cause:
NavigationManager: Creates NEW grid with animation
GridManager: Receives NAVIGATION_COMPLETED → renders AGAIN
Result: Two grids, broken state
Solution: GridManager only updates state on NAVIGATION_COMPLETED, doesn't re-render:
eventBus.on(CoreEvents.NAVIGATION_COMPLETED, (e: Event) => {
this.currentDate = detail.newDate;
this.dataSource.setCurrentDate(this.currentDate);
// Removed render() call
});
Challenge 3: DataSource Scope Confusion
Issue: Initially unsure whether NavigationManager should calculate dates or use DataSource.
User Guidance: NavigationManager should use DateColumnDataSource just like GridManager, ensuring consistent date calculation.
Solution: Both managers create their own DataSource instances:
// GridManager
this.dataSource = new DateColumnDataSource(dateService, config, this.currentDate, this.currentView);
// NavigationManager
this.dataSource = new DateColumnDataSource(dateService, config, this.currentWeek, 'week');
Navigation Flow (Final)
User Clicks "Next Week":
1. NavigationButtons
├─ Calculates newDate (next week)
└─ emit NAV_BUTTON_CLICKED { direction: 'next', newDate }
2. NavigationManager (listens to NAV_BUTTON_CLICKED)
├─ Updates DataSource with newDate
├─ Gets dates from DataSource.getColumns()
├─ Creates NEW grid: createNavigationGrid(dates)
├─ Animates slide transition (old grid out, new grid in)
└─ After animation: emit NAVIGATION_COMPLETED { direction: 'next', newDate }
3. GridManager (listens to NAVIGATION_COMPLETED)
├─ Updates currentDate
├─ Updates dataSource.setCurrentDate()
└─ Does NOT call render() ← Key fix!
4. EventRenderingService (listens to GRID_RENDERED - emitted during step 2)
├─ Reads GRID_RENDERED payload { container, dates }
├─ Fetches events for date range
└─ Renders events into new grid's columns
Result: Single grid created by NavigationManager with animation, events rendered by EventRenderingService, GridManager stays in sync.
Files Modified
Created:
src/types/ColumnDataSource.ts- IColumnDataSource interfacesrc/datasources/DateColumnDataSource.ts- Date-based DataSource implementation
Modified:
-
src/managers/GridManager.ts- Added DateColumnDataSource
- Removed date calculation logic
- Fixed NAVIGATION_COMPLETED handler (removed render() call)
-
src/managers/NavigationManager.ts- Added DateColumnDataSource
- Uses DataSource.getColumns() instead of manual calculation
- Added Configuration injection
-
src/renderers/GridRenderer.ts- Removed WorkHoursManager dependency
- Removed event rendering logic (corrected after mistake)
- Removed column creation loop (delegates to ColumnRenderer)
- Updated createNavigationGrid() to accept dates array
-
src/renderers/ColumnRenderer.ts- Changed IColumnRenderContext to receive dates array
- Removed date calculation logic
- Simplified render() method
-
src/types/EventTypes.ts- No changes needed (considered adding skipRender flag but not required)
Test Results
Build Status:
npm run build
# ✅ Success
# Total transform time: 1105.12ms
# No TypeScript errors
Manual Testing Required:
- ❓ Navigation animation works correctly
- ❓ Events render after navigation
- ❓ Work hours styling applied
- ❓ Workweek preset changes reflected
Note: No automated tests exist for navigation flow or DataSource.
Lessons Learned
1. Conversation Compaction Awareness
- Problem: Lost architectural context after compaction
- Impact: Violated separation of concerns, added code to wrong places
- Solution: After compaction, explicitly review architectural boundaries before coding
- Prevention: Ask user to confirm responsibilities if unsure
2. Separation of Concerns Is Critical
- GridRenderer: Structure only
- ColumnRenderer: Columns only
- EventRenderingService: Events only
- DataSource: Dates only
- Mixing these leads to maintenance hell
3. Single Source of Truth Pattern
- Before: 3 places calculated dates (GridManager, ColumnRenderer, NavigationManager)
- After: 1 place calculates dates (DateColumnDataSource)
- Benefit: Change workweek logic once, applies everywhere
4. Event-Driven Rendering Conflicts
- Two managers can't both render on same event
- NavigationManager owns animation rendering
- GridManager only syncs state during navigation
- Clear ownership prevents bugs
5. Dependency Injection Must Match Responsibility
- GridRenderer doesn't need WorkHoursManager (ColumnRenderer does)
- NavigationManager needs Configuration (to create DataSource)
- Only inject what's actually used
Future Considerations
Potential Improvements
-
Resource-Based View Support
- Create
ResourceColumnDataSourceimplementingIColumnDataSource - Returns resource columns instead of date columns
- Switch between DateColumnDataSource and ResourceColumnDataSource based on view mode
- Create
-
DataSource Caching
- Cache calculated dates to avoid recalculation
- Invalidate cache on workweek settings change
- Performance optimization for frequent navigation
-
Navigation Testing
- Add integration tests for navigation flow
- Test animation + event rendering coordination
- Verify no duplicate grids created
-
DataSource Configuration
- Allow DataSource to be configured via DI container
- Support custom DataSource implementations
- Enable A/B testing of different date calculation strategies
Code Statistics
Lines Changed:
- Created: ~150 lines (DateColumnDataSource + interface)
- Modified: ~80 lines (GridManager, NavigationManager, GridRenderer, ColumnRenderer)
- Removed: ~60 lines (duplicate date calculations, event rendering in GridRenderer)
- Net: +170 lines
Complexity:
- Cyclomatic complexity reduced (centralized date logic)
- Coupling reduced (fewer dependencies in GridRenderer)
- Cohesion improved (each class has single responsibility)
Production Readiness
✅ Ready for Testing
Confidence Level: Medium-High
Reasons:
- Build succeeds without errors
- Architecture follows separation of concerns
- All architectural violations corrected
- User-guided course corrections applied
Concerns:
- No automated tests for navigation flow
- Manual testing required
- Event rendering after navigation not verified
- DataSource not tested with different workweek configs
Testing Checklist:
- Navigate next/previous week
- Verify events render after animation
- Check work hours styling applied
- Change workweek preset and navigate
- Verify no duplicate grids created
- Test navigation while events are being dragged (edge case)
Rollback Plan: If navigation breaks:
- Git revert to before DataSource implementation
- Restore direct date calculations in managers
- Restore GridManager render() call on NAVIGATION_COMPLETED
Conclusion
Successfully implemented DataSource architecture despite initial architectural violations caused by conversation compaction. User corrections guided the implementation back to correct separation of concerns.
The migration process revealed:
- Importance of maintaining architectural awareness after conversation compaction
- Value of centralized data calculation (DataSource pattern)
- Critical need for clear responsibility boundaries
- How event-driven architecture requires careful coordination
Key Success: Single source of truth for dates, clean separation of structure/columns/events rendering, navigation rendering conflict resolved.
Key Learning: After conversation compaction, always verify architectural boundaries before implementing.
Status: ✅ Implementation complete, ready for manual testing.