Web Development

Modern Angular from Scratch: Standalone Components, Signals, and Control Flow

DD
Ankur Ishwar
8 min read Updated Sep 7, 2026
Getting started with modern Angular development

The Outdated Syllabus Problem

Most Indian engineering colleges and Ameerpet training institutes teach an obsolete version of Angular from 2017: bloated NgModule declarations, painful Zone.js change detection bugs, and structural directives like *ngIf and *ngFor that required importing entire library modules just to render a bulleted list.

Modern Angular has completely evolved. The framework is now lean, fast, and enjoyable to write. Standalone components are the default, reactive Signals handle state without dirty-checking overhead, and built-in control flow blocks (@if, @for) live directly inside your templates without external imports.

If you want to land a modern frontend engineering role, here is how you build Angular applications using current production standards.

Step 1: Scaffolding a Clean Standalone Project

Make sure you are running Node.js v20+ LTS. You do not need to install the Angular CLI globally; run it directly with npx:

npx @angular/cli@latest new modern-angular-app --style=scss --ssr=false --routing=true
cd modern-angular-app
npm start

Open src/app/app.component.ts in VS Code. Notice what is missing: there is no app.module.ts. The application bootstraps directly from src/main.ts using bootstrapApplication(AppComponent).

Step 2: Anatomy of a Standalone Component

Every component in modern Angular explicitly imports the dependencies it needs. There is zero hidden magic. Here is a standalone root component:

import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <header class="navbar">
      <h1>Developer Task Hub</h1>
      <nav>
        <a routerLink="/">Dashboard</a>
        <a routerLink="/tasks">Tasks</a>
      </nav>
    </header>
    <main>
      <router-outlet />
    </main>
  `,
  styles: [`
    .navbar {
      display: flex;
      justify-content: space-between;
      padding: 1rem 2rem;
      background: #0f172a;
      color: #fff;
    }
    nav a {
      color: #94a3b8;
      margin-left: 1rem;
      text-decoration: none;
    }
  `]
})
export class AppComponent {}

Step 3: Mastering Reactive State with Signals

Historically, Angular patched browser APIs with Zone.js to check every component tree whenever an event occurred. Signals introduced fine-grained, dependency-tracking reactivity. When a Signal value updates, only the specific template node bound to that signal re-renders.

Let us create a practical task tracker component using signal() and computed():

import { Component, signal, computed } from '@angular/core';

export interface DevSprintTask {
  id: number;
  title: string;
  isDone: boolean;
}

@Component({
  selector: 'app-sprint-tracker',
  standalone: true,
  templateUrl: './sprint-tracker.component.html'
})
export class SprintTrackerComponent {
  // Core reactive signal
  tasks = signal<DevSprintTask[]>([
    { id: 1, title: 'Configure TypeScript strict mode', isDone: true },
    { id: 2, title: 'Implement Signal state store', isDone: true },
    { id: 3, title: 'Write unit tests with Vitest', isDone: false }
  ]);

  // Computed derived signals automatically update when tasks() updates
  completedCount = computed(() => 
    this.tasks().filter(task => task.isDone).length
  );

  totalCount = computed(() => this.tasks().length);

  allCompleted = computed(() => 
    this.totalCount() > 0 && this.completedCount() === this.totalCount()
  );

  toggleTask(id: number): void {
    this.tasks.update(currentTasks =>
      currentTasks.map(task =>
        task.id === id ? { ...task, isDone: !task.isDone } : task
      )
    );
  }
}

Step 4: Clean Template Control Flow: @if and @for

Old Angular required structural directives (*ngIf, *ngFor) that made templates noisy and required importing CommonModule everywhere. Modern Angular provides native control flow blocks directly built into the compiler:

<div class="sprint-card">
  <header>
    <h2>Sprint Progress</h2>
    <p>Completed {{ completedCount() }} out of {{ totalCount() }} tasks</p>
  </header>

  <ul class="task-list">
    @for (task of tasks(); track task.id) {
      <li class="task-row">
        <label>
          <input
            type="checkbox"
            [checked]="task.isDone"
            (change)="toggleTask(task.id)"
          />
          <span [class.strike]="task.isDone">{{ task.title }}</span>
        </label>
      </li>
    } @empty {
      <p class="empty-state">No sprint tasks assigned yet.</p>
    }
  </ul>

  @if (allCompleted()) {
    <div class="banner-success">
      <strong>Sprint Complete:</strong> Ready for production release!
    </div>
  } @else {
    <div class="banner-pending">
      Work in progress. Keep pushing code.
    </div>
  }
</div>

Notice the track task.id expression. It is mandatory in @for blocks, preventing accidental DOM thrashing and rendering bugs.

Step 5: Modern Dependency Injection with inject()

Constructor-based dependency injection is optional in modern Angular. You can use the clean inject() function right where you define your class properties:

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface ServerMetric {
  cpuPercent: number;
  memoryPercent: number;
  uptimeSeconds: number;
}

@Injectable({
  providedIn: 'root'
})
export class MetricsService {
  private readonly http = inject(HttpClient);

  fetchLiveMetrics(): Observable<ServerMetric> {
    return this.http.get<ServerMetric>('/api/v1/metrics/live');
  }
}

In your component, you inject the service with a single line:

export class DashboardComponent {
  private readonly metricsService = inject(MetricsService);
  // Use directly in your signals or component methods
}

Step 6: Lazy Loading Components with @defer

One of the most powerful modern Angular features is the @defer template block. It allows you to lazy-load heavy components automatically when they scroll into the viewport or meet specific trigger conditions:

<!-- Only download the heavy chart chunk when the user scrolls near it -->
@defer (on viewport) {
  <app-heavy-analytics-chart />
} @placeholder {
  <div class="chart-skeleton">Loading chart visualizer...</div>
} @error {
  <div class="chart-error">Failed to load analytics bundle.</div>
}

The compiler automatically splits app-heavy-analytics-chart into a separate JavaScript chunk, keeping your initial landing page load under 50KB.

Conclusion

Forget the outdated tutorials teaching NgModules from five years ago. Modern Angular is fast, typesafe, and built for high-performance engineering teams. Start with standalone components, manage your reactive state with Signals, use native template control flow, and use @defer for instant bundle optimization.

Found this useful?
View all articles

Free Utilities

Recommended Developer Tools for this Topic

Explore all 25+ tools

Keep Reading

Related Articles

Learn with Dropout Developer

Build real software with AI

Step-by-step learning paths, vibe coding tutorials, and certified developer programs designed for the modern engineer.