#angular
#capstone
#ngrx
#cdk
#drag-drop
Day 29 β Capstone Project Part 1: Architecture, Store & Drag-and-Drop
π Day 29 β Capstone Project Part 1: Architecture, Store & Drag-and-Drop
Zero to Hero β Hands-on Angular Tutorial
Today is Part 1 of our Final Project: A Trello-style Kanban Board.
Today you will:
- βοΈ Scaffold the application (Angular Material, SignalStore).
- βοΈ Architect the Data Model (Board, Columns, Tasks).
- βοΈ Implement the State Management (Signals).
- βοΈ Build the Drag-and-Drop Interface (Angular CDK).
This connects everything: Components, Services, Signals, Material, and Directives. ποΈ
π¦ 1. Setup & Dependencies
Create a fresh app (or use your existing one).
ng add @angular/material
npm install @angular/cdk @ngrx/signals
app.config.ts:
Enable standard providers.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideAnimations()
]
};
π© 2. The Data Model
Define what we are building.
src/app/models/board.model.ts
export type Task = {
id: string;
title: string;
description?: string;
tags?: string[];
};
export type Column = {
id: string;
title: string;
tasks: Task[];
};
export type Board = {
id: string;
title: string;
columns: Column[];
};
π§ 3. The SignalStore (State Management)
We need a central place to hold the Board state.
src/app/store/board.store.ts
import { patchState, signalStore, withComputed, withMethods, withState } from '@ngrx/signals';
import { Board, Column, Task } from '../models/board.model';
import { computed, inject } from '@angular/core';
type BoardState = {
board: Board;
};
const initialState: BoardState = {
board: {
id: '1',
title: 'My Project',
columns: [
{ id: 'todo', title: 'To Do', tasks: [] },
{ id: 'doing', title: 'In Progress', tasks: [] },
{ id: 'done', title: 'Done', tasks: [] }
]
}
};
export const BoardStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed(({ board }) => ({
// Derived: Total tasks across all columns
totalTasks: computed(() => board().columns.reduce((acc, col) => acc + col.tasks.length, 0))
})),
withMethods((store) => ({
addTask(columnId: string, title: string) {
const newTask: Task = { id: Date.now().toString(), title };
patchState(store, (state) => ({
board: {
...state.board,
columns: state.board.columns.map(col =>
col.id === columnId ? { ...col, tasks: [...col.tasks, newTask] } : col
)
}
}));
},
moveTask(prevColId: string, currColId: string, prevIndex: number, currIndex: number) {
// β οΈ Logic for Drag & Drop movement (removing from old, adding to new) goes here.
// We will hook this up to the CDK event later.
}
}))
);
π₯ 4. The Board Component (Drag-and-Drop UI)
We use Angular CDK (@angular/cdk/drag-drop) for the magic.
board.component.ts
import { Component, inject } from '@angular/core';
import { CdkDragDrop, DragDropModule, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { BoardStore } from '../../store/board.store';
import { NgFor } from '@angular/common';
@Component({
selector: 'app-board',
standalone: true,
imports: [DragDropModule, NgFor],
templateUrl: './board.component.html',
styleUrl: './board.component.scss'
})
export class BoardComponent {
readonly store = inject(BoardStore);
drop(event: CdkDragDrop<any[]>) {
// CDK Helper: if moving inside same list
if (event.previousContainer === event.container) {
moveItemInArray(event.container.data, event.previousIndex, event.currentIndex);
} else {
// CDK Helper: if moving to another list
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex,
);
}
// TO DO: Sync this change back to the Store!
}
}
board.component.html
<div class="board">
<div class="column" *ngFor="let col of store.board().columns">
<h2>{{ col.title }}</h2>
<!-- CDK Drop List -->
<div
cdkDropList
[cdkDropListData]="col.tasks"
class="task-list"
(cdkDropListDropped)="drop($event)">
<!-- Draggable Item -->
<div
class="task-card"
*ngFor="let task of col.tasks"
cdkDrag>
{{ task.title }}
</div>
</div>
</div>
</div>
π« 5. Styling the Board
Drag and drop needs specific CSS to look good.
board.component.scss
.board {
display: flex;
gap: 20px;
padding: 20px;
overflow-x: auto;
height: 100vh;
}
.column {
min-width: 300px;
background: #f4f5f7;
border-radius: 8px;
padding: 10px;
}
.task-list {
min-height: 100px; /* Important for empty lists */
background: white;
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 10px;
}
.task-card {
padding: 15px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
cursor: move;
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
}
/* CDK Specific Classes */
.cdk-drag-preview {
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2);
}
.cdk-drag-placeholder {
opacity: 0;
}
π End of Day 29 β What You Built
Today you built the Skeleton of a Kanban App:
- βοΈ Store: Normalized State for Columns & Tasks.
- βοΈ UI: A horizontal scrolling board with columns.
- βοΈ Interactivity: Dragging items between columns using Angular CDK.
π§ͺ Day 29 Challenge
βPersistenceβ
- Add an
effect()to theBoardStore. - Whenever the
boardstate changes, save it tolocalStorage. - On initialization (
withHooks->onInit), checklocalStorage. - If data exists, load it into the store (
patchState). - Now refresh the page. Your tasks should remain exactly where you dropped them.