Project Architecture
This guide explains how Angular concepts are applied in the TimeLock project, showing you the overall architecture and design patterns used.
Table of Contents
- Application Overview
- Architecture Patterns
- State Management with Signals
- Data Flow
- Component Hierarchy
- Service Layer
- Routing Strategy
- Data Persistence
Application Overview
TimeLock is a task management application that demonstrates modern Angular patterns:
TimeLock Features:
├── 📋 Project Management
├── ✅ Task Management (with subtasks)
├── 📅 Calendar View
├── ⏰ Time Planning
├── 🔍 Search & Filtering
├── 🏷️ Tags & Categories
├── 🔄 Recurring Tasks
└── 📱 Responsive Design
Technology Stack
- Angular 20 - Latest version with modern features
- TypeScript - Strong typing and better developer experience
- Signals - Reactive state management
- IndexedDB - Client-side data persistence
- CSS3 - Modern styling with CSS Grid and Flexbox
- Playwright - End-to-end testing
Architecture Patterns
1. Standalone Component Architecture
TimeLock uses standalone components throughout, eliminating the need for NgModules:
// Traditional approach (NOT used)
@NgModule({
declarations: [TodoComponent],
imports: [CommonModule]
})
// Modern approach (USED in TimeLock)
@Component({
standalone: true,
imports: [CommonModule, FormsModule]
})
export class TodoComponent { }
2. Signal-Based State Management
Instead of RxJS Observables for local state, TimeLock uses Angular Signals:
// Service layer
@Injectable({ providedIn: 'root' })
export class TodoService {
private todosSignal = signal<Todo[]>([]);
// Read-only access for components
todos = this.todosSignal.asReadonly();
// Computed derived state
activeTodos = computed(() =>
this.todosSignal().filter(todo => !todo.completed)
);
}
3. Dependency Injection Pattern
Services are injected using the modern inject()
function:
export class TodoComponent {
private todoService = inject(TodoService);
private projectService = inject(ProjectService);
private router = inject(Router);
// Clean, functional approach
}
4. Lazy Loading Strategy
Routes are lazy-loaded for better performance:
export const routes: Routes = [
{
path: 'projects',
loadComponent: () => import('./pages/projects/projects.component')
.then(m => m.ProjectsComponent)
}
];
State Management with Signals
TimeLock demonstrates a comprehensive signal-based state management pattern:
Service-Level State
@Injectable({ providedIn: 'root' })
export class TodoService {
// Private writable signals
private todosSignal = signal<Todo[]>([]);
private filterSignal = signal<FilterType>('all');
private searchTermSignal = signal<string>('');
// Public read-only signals
todos = this.todosSignal.asReadonly();
filter = this.filterSignal.asReadonly();
searchTerm = this.searchTermSignal.asReadonly();
// Computed signals for derived state
filteredTodos = computed(() => {
const todos = this.todosSignal();
const filter = this.filterSignal();
const searchTerm = this.searchTermSignal();
return todos
.filter(todo => this.matchesFilter(todo, filter))
.filter(todo => this.matchesSearch(todo, searchTerm));
});
activeTodos = computed(() =>
this.todosSignal().filter(todo => !todo.completed && !todo.archived)
);
completedTodos = computed(() =>
this.todosSignal().filter(todo => todo.completed)
);
}
Component-Level State
export class TodoItemComponent {
@Input() todo!: Todo;
// Local component state
isEditing = signal(false);
isExpanded = signal(false);
// Computed local state
hasSubtasks = computed(() => this.todo.subtasks.length > 0);
completionPercentage = computed(() => {
if (!this.hasSubtasks()) return 0;
const completed = this.todo.subtasks.filter(t => t.completed).length;
return (completed / this.todo.subtasks.length) * 100;
});
}
Data Flow
TimeLock follows a unidirectional data flow pattern:
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Components │───▶│ Services │───▶│ IndexedDB │
│ │ │ │ │ │
│ - Display data │ │ - Business logic │ │ - Data storage │
│ - Handle events │ │ - State management│ │ - Persistence │
│ - User input │ │ - API calls │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
▲ │
│ ▼
└────── Signals ─────────┘
Example Data Flow: Adding a Todo
- User Action: User clicks "Add Todo" button
- Component: TodoFormComponent emits event
- Parent Component: Calls TodoService.addTodo()
- Service: Updates signal and persists to IndexedDB
- Automatic Update: All components using the signal automatically re-render
// 1. User clicks button in template
<button (click)="addTodo()">Add Todo</button>
// 2. Component method
addTodo() {
this.todoService.addTodo(this.newTodo);
}
// 3. Service updates state
async addTodo(todo: Omit<Todo, 'id'>) {
const newTodo = { ...todo, id: this.generateId() };
this.todosSignal.update(todos => [...todos, newTodo]);
await this.indexedDBService.saveTodos(this.todosSignal());
}
// 4. All components automatically update via signals
Component Hierarchy
TimeLock's component structure follows a clear hierarchy:
App (Root Component)
├── Sidebar Navigation
├── Router Outlet
├── ProjectsComponent
│ ├── ProjectCardComponent
│ └── ProjectFormComponent
├── ProjectDetailComponent
│ ├── TodoFiltersComponent
│ ├── TodoFormComponent
│ └── TodoItemComponent
│ ├── TaskEditorComponent
│ └── SubtaskComponent
├── CalendarComponent
├── TimetableComponent
├── UpcomingComponent
└── SearchComponent
Smart vs Dumb Components
Smart Components (Container Components):
- Inject services
- Manage state
- Handle business logic
- Examples:
ProjectsComponent
,ProjectDetailComponent
export class ProjectsComponent {
private projectService = inject(ProjectService);
private todoService = inject(TodoService);
projects = this.projectService.activeProjects;
async createProject(projectData: CreateProjectData) {
await this.projectService.addProject(projectData);
}
}
Dumb Components (Presentation Components):
- Receive data via
@Input()
- Emit events via
@Output()
- No service dependencies
- Examples:
TodoItemComponent
,ProjectCardComponent
export class TodoItemComponent {
@Input() todo!: Todo;
@Output() todoUpdated = new EventEmitter<Todo>();
@Output() todoDeleted = new EventEmitter<string>();
toggleComplete() {
this.todoUpdated.emit({
...this.todo,
completed: !this.todo.completed
});
}
}
Service Layer
TimeLock's service layer is organized by domain:
Core Services
TodoService - Task management
@Injectable({ providedIn: 'root' })
export class TodoService {
// CRUD operations
async addTodo(todo: Omit<Todo, 'id'>): Promise<string>
async updateTodo(id: string, updates: Partial<Todo>): Promise<void>
async deleteTodo(id: string): Promise<void>
// Business logic
async toggleComplete(id: string): Promise<void>
async addSubtask(parentId: string, subtask: Omit<Todo, 'id'>): Promise<void>
async autoPlanTasks(tasks: Todo[], timeSlots: TimeSlot[]): Promise<void>
}
ProjectService - Project management
@Injectable({ providedIn: 'root' })
export class ProjectService {
async addProject(project: Omit<Project, 'id'>): Promise<string>
async updateProject(id: string, updates: Partial<Project>): Promise<void>
async deleteProject(id: string): Promise<void>
setCurrentProject(projectId: string | null): void
}
IndexedDBService - Data persistence
@Injectable({ providedIn: 'root' })
export class IndexedDBService {
async saveTodos(todos: Todo[]): Promise<void>
async loadTodos(): Promise<Todo[]>
async saveProjects(projects: Project[]): Promise<void>
async loadProjects(): Promise<Project[]>
}
Utility Services
TagsService - Tag management RecurringTasksService - Recurring task logic ConfirmationService - Dialog management
Routing Strategy
TimeLock uses a feature-based routing structure:
export const routes: Routes = [
{ path: '', redirectTo: '/projects', pathMatch: 'full' },
// Main pages
{ path: 'projects', loadComponent: () => import('./pages/projects/projects.component') },
{ path: 'project/:id', loadComponent: () => import('./pages/project-detail/project-detail.component') },
// Views
{ path: 'calendar', loadComponent: () => import('./pages/calendar/calendar.component') },
{ path: 'timetable', loadComponent: () => import('./pages/timetable/timetable.component') },
{ path: 'upcoming', loadComponent: () => import('./pages/upcoming/upcoming.component') },
{ path: 'search', loadComponent: () => import('./pages/search/search.component') },
// Fallback
{ path: '**', redirectTo: '/projects' }
];
Route Guards and Resolvers
While not currently implemented, TimeLock's architecture supports:
// Example guard
export const projectExistsGuard: CanActivateFn = (route) => {
const projectService = inject(ProjectService);
const projectId = route.paramMap.get('id');
return projectService.getProjectById(projectId!) !== undefined;
};
// Example resolver
export const projectResolver: ResolveFn<Project> = (route) => {
const projectService = inject(ProjectService);
const projectId = route.paramMap.get('id')!;
return projectService.getProjectById(projectId)!;
};
Data Persistence
TimeLock uses IndexedDB for client-side data persistence:
Why IndexedDB?
- Offline capability - Works without internet
- Large storage - Much more than localStorage
- Structured data - Supports complex objects
- Asynchronous - Non-blocking operations
Data Schema
interface DBSchema {
todos: {
key: string;
value: Todo;
indexes: { 'by-project': string; 'by-due-date': Date; };
};
projects: {
key: string;
value: Project;
};
tags: {
key: string;
value: Tag;
};
}
Service Integration
@Injectable({ providedIn: 'root' })
export class IndexedDBService {
private dbName = 'TimeLockDB';
private version = 1;
async initDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
this.createObjectStores(db);
};
});
}
}
Performance Considerations
1. Lazy Loading
- Routes are loaded on-demand
- Reduces initial bundle size
- Faster application startup
2. OnPush Change Detection
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})
export class OptimizedComponent { }
3. Signal-Based Updates
- Automatic dependency tracking
- Minimal re-renders
- Better performance than traditional observables
4. Virtual Scrolling (Future Enhancement)
// For large lists
<cdk-virtual-scroll-viewport itemSize="50">
<div *cdkVirtualFor="let todo of todos">{{todo.title}}</div>
</cdk-virtual-scroll-viewport>
Next Steps
Continue learning about TimeLock:
- Getting Started - Set up your development environment
- Code Structure - Detailed code organization
- Development Guide - How to extend the application