π οΈ Weekend Project: Vue & Angular
Reading about frameworks only takes you so far β this weekend you build. You'll ship a real task-manager app in either Vue 3 or Angular, working through it as a series of milestones with a checklist and clear quality bars, so you always know what "done" looks like.
π― Learning Objectives
By the end of this project, you will be able to:
- Turn a fuzzy feature request into concrete requirements and a component plan
- Scaffold and build a small app with modern Vue 3
<script setup>or Angular standalone components with signals - Manage local reactive state and persist it to
localStorage - Work in milestones and self-assess against a "what good looks like" bar
Estimated Time: 6β10 hours (a weekend) β’ Difficulty: Intermediate
Hands-on: This whole lesson is the exercise β build the app, tick the checklist, then run the quiz to confirm the concepts stuck.
In This Lesson
The Build & How to Approach It
You've compared Vue and Angular, weighed their ecosystems, and seen how each handles components, reactivity, and state. Now you put one of them to work. Pick one framework β the one you're most curious about β and build the same app either way. Doing it once in one framework teaches you far more than reading about both.
To keep a weekend build from sprawling, we structure it with a time-tested problem-solving loop from mathematician George PΓ³lya (How to Solve It, 1945): understand the problem, plan a solution, carry out the plan, then look back to verify and extend. Those four steps map cleanly onto our four milestones.
π Why a "todo app" β again?
A task manager is the frontend equivalent of scales for a musician. It is small enough to finish in a weekend but touches every muscle a real app uses: forms and validation, list rendering, reactive derived data, component communication, and persistence. Nail it here and larger apps feel familiar.
What You're Building
A personal task manager that runs entirely in the browser. Keep the core tight; reach for extras only once the core works end to end.
Core requirements (all of these)
- Create, edit, and delete tasks
- Toggle a task complete / incomplete
- Assign a priority (low / medium / high) and a category tag
- Filter the list by status, priority, or category
- Persist tasks to
localStorageand reload them on startup - Responsive layout that works on phone and desktop
Pick at least two extensions
| Extension | Why it's a good stretch |
|---|---|
| Due date + overdue highlighting | Practice date handling and derived/computed styling |
| Drag-and-drop reordering | Learn a third-party integration in your framework's idiom |
| Completion statistics | Reinforces derived state (computed values / signals) |
| Dark-mode toggle | Small, satisfying, and touches persistence again |
| Subtasks / checklist items | Nested component + nested state, a real step up |
β οΈ Scope guardrail
Resist gold-plating. The most common weekend-project failure is a half-built app with five features started and none finished. Ship the core first, commit it, and only then add extensions one at a time.
The Milestones
Four checkpoints. After each one you should have something that runs. Commit at every milestone so you can always roll back to a working state.
requirements, user stories,
component tree"] --> M2["M2 Β· Scaffold & State
project setup, data model,
localStorage"] M2 --> M3["M3 Β· Build Components
form, list, item, filters"] M3 --> M4["M4 Β· Polish & Extend
responsive, a11y,
2+ extensions"]
π‘ A weekend cadence that works
Saturday morning: M1 + M2 (plan + scaffold). Saturday afternoon: M3 core components. Sunday: M4 polish, extensions, and a short write-up. Timebox each block β a plan you can't finish in an hour is too big.
Milestone 1 β Understand & Plan
Don't open your editor yet. Spend 30β45 minutes turning the spec into concrete artifacts you can build against.
Write user stories
Capture 5β7 needs in the form "As a [user], I want to [action] so that [benefit]." They keep you honest about why a feature exists:
- "As a busy student, I want to tag tasks by course so that I can focus on one subject at a time."
- "As a forgetful person, I want overdue tasks highlighted so that nothing slips past me."
Sketch the component tree
Both Vue and Angular build UIs from a tree of components. Deciding the boundaries now saves rework later:
Design the data model
One well-shaped Task type carries the whole app. Using TypeScript here pays off in both frameworks (Angular is TS-first; Vue's <script setup lang="ts"> gives you the same safety):
export type Priority = 'low' | 'medium' | 'high';
export interface Task {
id: string; // crypto.randomUUID()
title: string;
completed: boolean;
priority: Priority;
category: string; // e.g. 'work', 'home'
dueDate?: string; // ISO date string, optional
createdAt: string; // ISO timestamp
}
export interface Filters {
status: 'all' | 'active' | 'completed';
priority: 'all' | Priority;
category: 'all' | string;
}
π Milestone 1 is "done" whenβ¦
You have a written list of user stories, a component-tree sketch, and a data model. No code yet β but you now know exactly what to build.
Milestone 2 β Scaffold & State
Now create the project and get reactive state persisting. By the end of this milestone you can add a task in the console or a stub form, refresh the page, and see it survive.
Scaffold the project
Use the official tooling for whichever framework you chose:
Vue 3 (Vite)
npm create vue@latest task-manager
# choose: TypeScript = Yes, everything else No for now
cd task-manager
npm install
npm run dev
Angular (standalone)
npm install -g @angular/cli
ng new task-manager --standalone --style=css --routing=false
cd task-manager
ng serve
A framework-agnostic storage helper
Persistence is identical in both frameworks β plain functions over localStorage, wrapped in try/catch so a quota error or corrupt value can't crash the app:
// storage.ts
import type { Task } from './task';
const KEY = 'tasks:v1'; // version the key so future shape changes are safe
export function loadTasks(): Task[] {
try {
const raw = localStorage.getItem(KEY);
return raw ? (JSON.parse(raw) as Task[]) : [];
} catch {
console.warn('Could not read tasks; starting empty.');
return [];
}
}
export function saveTasks(tasks: Task[]): void {
try {
localStorage.setItem(KEY, JSON.stringify(tasks));
} catch {
console.warn('Could not save tasks (storage full?).');
}
}
Reactive state, Vue vs Angular
Same idea, two idioms. In Vue you hold state in a ref and re-save with a watch; in Angular you hold it in a signal inside a service and re-save with an effect.
Vue β a composable
// useTasks.ts
import { ref, watch } from 'vue';
import type { Task } from './task';
import { loadTasks, saveTasks } from './storage';
export function useTasks() {
const tasks = ref<Task[]>(loadTasks());
// persist automatically whenever the list changes
watch(tasks, (value) => saveTasks(value), { deep: true });
function addTask(task: Task) {
tasks.value.push(task);
}
function updateTask(id: string, patch: Partial<Task>) {
const t = tasks.value.find((x) => x.id === id);
if (t) Object.assign(t, patch);
}
function deleteTask(id: string) {
tasks.value = tasks.value.filter((x) => x.id !== id);
}
return { tasks, addTask, updateTask, deleteTask };
}
Angular β a signal-based service
// task.service.ts
import { Injectable, signal, effect } from '@angular/core';
import type { Task } from './task';
import { loadTasks, saveTasks } from './storage';
@Injectable({ providedIn: 'root' })
export class TaskService {
readonly tasks = signal<Task[]>(loadTasks());
constructor() {
// persist automatically whenever the signal changes
effect(() => saveTasks(this.tasks()));
}
addTask(task: Task) {
this.tasks.update((list) => [...list, task]);
}
updateTask(id: string, patch: Partial<Task>) {
this.tasks.update((list) =>
list.map((t) => (t.id === id ? { ...t, ...patch } : t))
);
}
deleteTask(id: string) {
this.tasks.update((list) => list.filter((t) => t.id !== id));
}
}
β Milestone 2 is "done" whenβ¦
The dev server runs, you can add a task programmatically, and it persists across a page refresh. The plumbing works β now build the UI on top of it.
Milestone 3 β Build the Components
Wire up the real UI: a form to add tasks, a list that renders them, an item with edit/delete/toggle, and derived filtering. Below is the heart of each framework β the add form and a task item β in modern style.
The add-task form
Vue β TaskForm.vue with <script setup>
<script setup lang="ts">
import { ref } from 'vue';
import type { Task, Priority } from './task';
const emit = defineEmits<{ add: [task: Task] }>();
const title = ref('');
const priority = ref<Priority>('medium');
const category = ref('general');
function submit() {
const text = title.value.trim();
if (!text) return; // simple validation: no empty tasks
emit('add', {
id: crypto.randomUUID(),
title: text,
completed: false,
priority: priority.value,
category: category.value.trim() || 'general',
createdAt: new Date().toISOString(),
});
title.value = '';
}
</script>
<template>
<form class="task-form" @submit.prevent="submit">
<input v-model="title" placeholder="What needs doing?" aria-label="Task title" />
<select v-model="priority" aria-label="Priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<input v-model="category" placeholder="Category" aria-label="Category" />
<button type="submit">Add</button>
</form>
</template>
Angular β TaskFormComponent (standalone, signals)
// task-form.component.ts
import { Component, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import type { Task, Priority } from './task';
@Component({
selector: 'app-task-form',
standalone: true,
imports: [FormsModule],
template: `
<form class="task-form" (ngSubmit)="submit()">
<input [(ngModel)]="title" name="title"
placeholder="What needs doing?" aria-label="Task title" />
<select [(ngModel)]="priority" name="priority" aria-label="Priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<input [(ngModel)]="category" name="category"
placeholder="Category" aria-label="Category" />
<button type="submit">Add</button>
</form>
`,
})
export class TaskFormComponent {
add = output<Task>();
title = signal('');
priority = signal<Priority>('medium');
category = signal('general');
submit() {
const text = this.title().trim();
if (!text) return;
this.add.emit({
id: crypto.randomUUID(),
title: text,
completed: false,
priority: this.priority(),
category: this.category().trim() || 'general',
createdAt: new Date().toISOString(),
});
this.title.set('');
}
}
Derived data: filtering & stats
Never store filtered results β derive them. Vue uses computed; Angular uses computed signals. Both recalculate only when their inputs change, so lists stay in sync automatically.
// Vue
import { computed } from 'vue';
const visibleTasks = computed(() =>
tasks.value.filter((t) =>
(filters.value.status === 'all' ||
(filters.value.status === 'completed') === t.completed) &&
(filters.value.priority === 'all' || filters.value.priority === t.priority) &&
(filters.value.category === 'all' || filters.value.category === t.category)
)
);
// Angular (inside a component)
import { computed } from '@angular/core';
readonly visibleTasks = computed(() =>
this.taskService.tasks().filter((t) => /* same predicate */ true)
);
Rendering the list
Loop over the derived list. Give each item a stable key so the framework can update efficiently:
<!-- Vue -->
<TaskItem v-for="task in visibleTasks" :key="task.id" :task="task"
@toggle="updateTask(task.id, { completed: !task.completed })"
@delete="deleteTask(task.id)" />
<!-- Angular (control flow) -->
@for (task of visibleTasks(); track task.id) {
<app-task-item [task]="task"
(toggle)="onToggle(task)"
(delete)="taskService.deleteTask(task.id)" />
}
β Milestone 3 is "done" whenβ¦
You can add, edit, complete, delete, and filter tasks entirely through the UI, and everything still survives a refresh. That's a genuinely useful app β congratulations, the core is shipped.
Milestone 4 β Polish & Extend
This is PΓ³lya's "look back" step. First verify the core against your requirements, then add your two extensions, then make it feel finished.
Verify before you extend
- Walk each user story from Milestone 1 β does the app satisfy it?
- Add ~30 tasks and confirm the UI stays responsive
- Resize to a phone width; check nothing overflows or overlaps
- Try edge cases: empty title, very long title, deleting the last task
Accessibility passes you can do in minutes
- Every input has a
<label>oraria-label(the code above already does) - The form submits on Enter, and buttons are real
<button>elements - Completion state isn't signalled by colour alone β add a strikethrough or icon
- Check colour contrast for priority badges in both light and dark themes
π‘ Extension idea worth doing first: completion stats
It's the cheapest high-value extension because it's pure derived state β no new UI plumbing. In Vue: const done = computed(() => tasks.value.filter(t => t.completed).length). In Angular: readonly done = computed(() => this.tasks().filter(t => t.completed).length). Render "3 of 8 done" and you've reinforced the single most important reactive-programming habit: derive, don't duplicate.
Write a short reflection
Spend 15 minutes on a README that answers: what did the framework make easy? What fought you? What would you structure differently next time? This is where a weekend project becomes a portfolio piece.
β Milestone 4 is "done" whenβ¦
Core verified, two extensions working, responsive and accessible, and a README committed. Deploy it (Netlify, Vercel, or GitHub Pages) and you have a shareable link.
Definition-of-Done Checklist
Print this or paste it into your README. The project is complete when every box is honestly ticked.
π Core (required)
- β Create, edit, delete, and toggle tasks from the UI
- β Priority and category on every task
- β Filter by status, priority, and category
- β Tasks persist to
localStorageand reload on start - β Layout works on a 375px-wide phone and on desktop
- β At least 5 distinct components
- β Derived data (filtering / stats) uses
computed/ signals, not stored copies
π Quality (required)
- β Empty / invalid input is handled (no blank tasks)
- β Every interactive control is keyboard-reachable and labelled
- β Committed to Git with at least one commit per milestone
- β README with description, run instructions, and a reflection
π Stretch (pick 2+)
- β Due dates with overdue highlighting
- β Drag-and-drop reordering
- β Completion statistics
- β Dark-mode toggle (persisted)
- β Subtasks / nested checklist
What Good Looks Like
Two submissions can both "work" and still be worlds apart. Here's how a strong build differs from a shaky one β use it to self-grade.
| Dimension | Shaky | Strong (aim here) |
|---|---|---|
| State | Filtered list stored in a variable and manually re-synced | Single source of truth; filtered/stat views are derived |
| Components | One giant component doing everything | Small, single-purpose components with clear inputs/outputs |
| Persistence | Direct localStorage calls scattered through the UI |
Isolated in one helper with error handling and a versioned key |
| Validation | Blank or duplicate tasks can be created | Input trimmed and validated; clear feedback on error |
| Accessibility | Clickable <div>s, colour-only status |
Real buttons/labels, keyboard-friendly, non-colour cues |
| Framework idiom | jQuery-style manual DOM poking | Vue <script setup> / Angular standalone + signals used naturally |
The tell of a strong build: when you add a feature, you change one place. That only happens when state has a single source of truth and components have clean boundaries β exactly the habits this project drills.
Grading weights (if you want a number)
- Functionality β 40%: core + two extensions actually work
- Code quality β 25%: component structure, derived state, clean persistence
- Framework usage β 20%: idiomatic Vue or Angular, not fighting the framework
- UX & accessibility β 10%: responsive, keyboard-friendly, clear feedback
- Docs β 5%: README and reflection
Summary & Quiz
π Key Takeaways
- Structure a build as milestones that each leave you with something runnable, and commit at every one.
- Plan before you code: user stories, a component tree, and a data model remove most mid-build confusion.
- Derive, don't duplicate: filtered lists and stats are
computedvalues / signals, never stored copies. - Modern idioms β Vue 3
<script setup>and Angular standalone components with signals β express the same reactive ideas with different syntax. - A "what good looks like" bar and a checklist turn "it works" into "it's done well."
π― Quick Quiz
Question 1: In both Vue and Angular, how should the filtered task list be produced?
Question 2: What's the point of finishing the core features and committing before adding extensions?
Question 3: Which pairing correctly matches the framework to its modern reactive primitive used in this project?
π Further Reading
- Vue 3 Guide β Composition API &
<script setup> - Angular β Signals
- Angular β Standalone components
- PΓ³lya, How to Solve It β the four-step method
π What's Next?
You've now built a complete frontend and felt where its limits are: data only lives in one browser, and there's no shared, secure source of truth. That's exactly what the backend solves. Next module we cross to the server side and start building the other half of the stack.
π You shipped an app!
Building beats reading every time. Push it to a repo, deploy it, and keep it in your portfolio.