Create Fresh App In Angular

1) Create a fresh app

# Angular 17+ (works on 18/20 too)
npm create @angular@latest task-tracker -- --standalone
cd task-tracker
npm i

2) Wire up main.ts

// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent).catch(err => console.error(err));

3) Task model

// src/app/task.model.ts
export interface Task {
  id: string;
  title: string;
  done: boolean;
  createdAt: number;
}

4) Signal store (core of the app)

  • Holds state in signals
  • Derives computed views (filtered list, counts)
  • Uses an effect to persist to localStorage
// src/app/task.store.ts
import { Injectable, effect, signal, computed } from '@angular/core';
import { Task } from './task.model';

type Filter = 'all' | 'open' | 'done';

@Injectable({ providedIn: 'root' })
export class TaskStore {
  // raw state
  private _tasks = signal<Task[]>([]);
  readonly tasks = this._tasks.asReadonly();

  filter = signal<Filter>('all');

  // derived state
  readonly openCount = computed(
    () => this._tasks().filter(t => !t.done).length
  );

  readonly filtered = computed(() => {
    const f = this.filter();
    const list = this._tasks();
    if (f === 'open') return list.filter(t => !t.done);
    if (f === 'done') return list.filter(t => t.done);
    return list;
  });

  constructor() {
    // hydrate from localStorage
    const raw = localStorage.getItem('tasks');
    if (raw) this._tasks.set(JSON.parse(raw));

    // persist whenever tasks change
    effect(() => {
      localStorage.setItem('tasks', JSON.stringify(this._tasks()));
    });
  }

  add(title: string) {
    const trimmed = title.trim();
    if (!trimmed) return;
    this._tasks.update(list => [
      ...list,
      { id: crypto.randomUUID(), title: trimmed, done: false, createdAt: Date.now() }
    ]);
  }

  toggle(id: string) {
    this._tasks.update(list =>
      list.map(t => (t.id === id ? { ...t, done: !t.done } : t))
    );
  }

  remove(id: string) {
    this._tasks.update(list => list.filter(t => t.id !== id));
  }

  clearDone() {
    this._tasks.update(list => list.filter(t => !t.done));
  }

  setFilter(f: Filter) {
    this.filter.set(f);
  }
}

5) Root component (standalone + signals + control flow)

  • Uses @for (Angular’s modern template loop)
  • No Zone.js tricks needed; signals keep updates precise
  • OnPush is natural with signals
// src/app/app.component.ts
import { ChangeDetectionStrategy, Component, ViewChild, ElementRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, NgForm } from '@angular/forms';
import { TaskStore } from './task.store';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, FormsModule],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styles: [`
    :host { display:block; max-width:720px; margin:32px auto; font: 14px/1.4 system-ui, Arial; }
    h1 { margin: 0 0 16px; }
    form { display:flex; gap:8px; margin: 16px 0; }
    input[type=text]{ flex:1; padding:10px; border:1px solid #ddd; border-radius:8px; }
    button { padding:10px 14px; border:0; border-radius:8px; cursor:pointer; }
    .list { margin-top:12px; border:1px solid #eee; border-radius:12px; overflow:hidden; }
    .row { display:flex; align-items:center; gap:12px; padding:10px 12px; border-top:1px solid #f3f3f3; }
    .row:first-child { border-top:0; }
    .title { flex:1; }
    .done .title { text-decoration: line-through; color:#888; }
    nav { display:flex; gap:8px; margin: 8px 0 16px; }
    nav button { background:#f4f4f4; }
    nav button.active { background:#222; color:#fff; }
    .meta { color:#666; font-size:12px; margin-top:8px; }
  `],
  template: `
    <h1>Task Tracker (Signals)</h1>

    <form #f="ngForm" (ngSubmit)="create()">
      <input #titleInput="ngModel" name="title" [(ngModel)]="title" type="text" placeholder="Add a task…" required minlength="2" />
      <button type="submit">Add</button>
    </form>

    <nav>
      <button (click)="store.setFilter('all')"  [class.active]="store.filter() === 'all'">All</button>
      <button (click)="store.setFilter('open')" [class.active]="store.filter() === 'open'">Open</button>
      <button (click)="store.setFilter('done')" [class.active]="store.filter() === 'done'">Done</button>
    </nav>

    <div class="list" *ngIf="store.filtered().length; else empty">
      @for (task of store.filtered(); track task.id) {
        <div class="row" [class.done]="task.done">
          <input type="checkbox" [checked]="task.done" (change)="store.toggle(task.id)" />
          <div class="title">{{ task.title }}</div>
          <button (click)="store.remove(task.id)" title="Remove">✕</button>
        </div>
      }
    </div>

    <ng-template #empty>
      <div class="meta">No tasks yet — add one above.</div>
    </ng-template>

    <div class="meta">
      {{ store.openCount() }} open /
      {{ store.tasks().length }} total
      <button *ngIf="store.tasks().some(t => t.done)" (click)="store.clearDone()">Clear done</button>
    </div>
  `
})
export class AppComponent {
  title = '';
  constructor(public store: TaskStore) {}

  create() {
    this.store.add(this.title);
    this.title = '';
  }
}

6) Run it

npm start
# or
ng serve -o

How this demonstrates the Signals concept

  • signal() holds the source of truth (_tasks, filter).
  • computed() creates derived views (openCount, filtered) that update only when their dependencies change.
  • effect() performs side effects (persistence to localStorage) whenever the underlying signal changes—no manual subscriptions necessary.
  • The UI stays fast and predictable without extra change detection work.

Optional: add a tiny HTTP service (mock) with Signals interop

If you later want to sync with an API, add a service and call it from the store. Use toSignal/toObservable interop for streaming scenarios. For most CRUD, keep the store as the single entry point so your components stay dumb.


What you can show in interviews

“I built a small Angular app where Signals are the single source of truth. I keep state in signal(), derive computed() lists for filters and counts, and use an effect() for persistence. Components are standalone and purely declarative: the UI re-renders precisely when the relevant signals change—no manual subscriptions or boilerplate.”

Author: Syed Abdi