#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”

  1. Add an effect() to the BoardStore.
  2. Whenever the board state changes, save it to localStorage.
  3. On initialization (withHooks -> onInit), check localStorage.
  4. If data exists, load it into the store (patchState).
  5. Now refresh the page. Your tasks should remain exactly where you dropped them.