Przejdź do treści
Frontend

Angular - The Complete Frontend Framework 2025

Published on:
·
Updated on:
·4 min read·Author: MDS Software Solutions Group

Angular Complete Frontend

frontend

Angular - The Complete Frontend Framework for Building Modern Applications

Angular is one of the most popular frontend frameworks in the world, developed and maintained by Google. Since the release of Angular 2 in 2016, the framework has undergone a massive evolution - from a monolithic module-based architecture to a modern approach featuring standalone components, signals, and defer blocks in versions 17 and 18. In this article, we will explore all the key aspects of Angular, from the fundamentals to advanced performance optimization techniques.

Why Angular in 2025?#

Angular stands out from the competition with several key features:

  • Completeness - Angular provides everything out-of-the-box: routing, forms, HTTP client, dependency injection, testing
  • TypeScript first - Angular was built from the ground up with TypeScript in mind, ensuring strong typing and better developer tooling
  • Scalability - works equally well for small projects and large enterprise applications
  • Ecosystem - Angular CLI, Angular Material, Angular Universal, Angular CDK
  • Stability - predictable release cycle and long-term support (LTS)
  • New APIs - Signals, standalone components, and defer blocks revolutionize the developer experience

What's New in Angular 17/18#

Standalone Components#

Starting with Angular 17, standalone components have become the default way of creating components. They eliminate the need for declaring NgModule for simple use cases:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-hero',
  standalone: true,
  imports: [CommonModule, RouterLink],
  template: `
    <section class="hero">
      <h1>{{ title }}</h1>
      <p>{{ description }}</p>
      <a routerLink="/contact" class="cta-button">Get in Touch</a>
    </section>
  `,
  styles: [`
    .hero {
      padding: 4rem 2rem;
      text-align: center;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
    }
    .cta-button {
      display: inline-block;
      padding: 12px 32px;
      background: white;
      color: #667eea;
      border-radius: 8px;
      text-decoration: none;
      font-weight: 600;
    }
  `]
})
export class HeroComponent {
  title = 'Welcome to Our Application';
  description = 'Built with Angular 18 and standalone components';
}

Signals - The New Reactivity System#

Signals are a new reactivity primitive in Angular that enables more granular change tracking and better performance compared to the traditional Zone.js-based approach:

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

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <div class="counter">
      <h2>Counter: {{ count() }}</h2>
      <p>Doubled value: {{ doubleCount() }}</p>
      <button (click)="increment()">+1</button>
      <button (click)="decrement()">-1</button>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class CounterComponent {
  // Signal - reactive value
  count = signal(0);

  // Computed signal - automatically recalculated
  doubleCount = computed(() => this.count() * 2);

  constructor() {
    // Effect - reacts to signal changes
    effect(() => {
      console.log(`Current counter value: ${this.count()}`);
    });
  }

  increment() {
    this.count.update(value => value + 1);
  }

  decrement() {
    this.count.update(value => value - 1);
  }

  reset() {
    this.count.set(0);
  }
}

Signals also support integration with RxJS through the toSignal() and toObservable() functions:

import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-clock',
  standalone: true,
  template: `<p>Time: {{ time() }}</p>`
})
export class ClockComponent {
  // Convert Observable to Signal
  time = toSignal(
    interval(1000).pipe(
      map(() => new Date().toLocaleTimeString())
    ),
    { initialValue: new Date().toLocaleTimeString() }
  );
}

Defer Blocks - Lazy Loading Components Declaratively#

Defer blocks provide a declarative way to lazy-load parts of a template, significantly improving the initial page load time:

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [HeavyChartComponent, DataTableComponent],
  template: `
    <h1>Dashboard</h1>

    <!-- Load chart after 2 seconds -->
    @defer (on timer(2s)) {
      <app-heavy-chart [data]="chartData" />
    } @placeholder {
      <div class="skeleton">Loading chart...</div>
    } @loading (minimum 500ms) {
      <div class="spinner">Loading component...</div>
    } @error {
      <p>Failed to load chart</p>
    }

    <!-- Load table when visible in viewport -->
    @defer (on viewport) {
      <app-data-table [data]="tableData" />
    } @placeholder {
      <div class="skeleton-table">Scroll to see the table</div>
    }
  `
})
export class DashboardComponent {
  chartData = signal<ChartData[]>([]);
  tableData = signal<TableRow[]>([]);
}

Components and Templates#

Angular uses TypeScript decorators to define components. Each component consists of a TypeScript class, an HTML template, and optional styles:

import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';

interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  inStock: boolean;
}

@Component({
  selector: 'app-product-card',
  standalone: true,
  imports: [CommonModule],
  template: `
    <div class="product-card" [class.out-of-stock]="!product.inStock">
      <h3>{{ product.name }}</h3>
      <p class="price">{{ product.price | currency:'USD':'symbol':'1.2-2' }}</p>
      <p>{{ product.description }}</p>

      @if (product.inStock) {
        <button (click)="onAddToCart()" class="btn-primary">
          Add to Cart
        </button>
      } @else {
        <span class="badge-unavailable">Out of Stock</span>
      }
    </div>
  `
})
export class ProductCardComponent {
  @Input({ required: true }) product!: Product;
  @Output() addToCart = new EventEmitter<Product>();

  onAddToCart() {
    this.addToCart.emit(this.product);
  }
}

New Control Flow Syntax#

Angular 17 introduced a new built-in control flow syntax that replaces structural directives:

<!-- New @if syntax -->
@if (users().length > 0) {
  <ul>
    @for (user of users(); track user.id) {
      <li>{{ user.name }} - {{ user.email }}</li>
    } @empty {
      <li>No users to display</li>
    }
  </ul>
} @else {
  <p>Loading data...</p>
}

<!-- New @switch syntax -->
@switch (status()) {
  @case ('active') {
    <span class="badge-success">Active</span>
  }
  @case ('inactive') {
    <span class="badge-warning">Inactive</span>
  }
  @default {
    <span class="badge-info">Unknown</span>
  }
}

Dependency Injection#

The dependency injection (DI) system in Angular is one of its most important pillars. It enables loosely coupled components and easier testing:

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

interface User {
  id: number;
  name: string;
  email: string;
  role: string;
}

@Injectable({
  providedIn: 'root'  // Application-wide singleton
})
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = '/api/users';

  private usersSubject = new BehaviorSubject<User[]>([]);
  users$ = this.usersSubject.asObservable();

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl).pipe(
      tap(users => this.usersSubject.next(users))
    );
  }

  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }

  createUser(user: Omit<User, 'id'>): Observable<User> {
    return this.http.post<User>(this.apiUrl, user).pipe(
      tap(newUser => {
        const current = this.usersSubject.getValue();
        this.usersSubject.next([...current, newUser]);
      })
    );
  }

  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`).pipe(
      tap(() => {
        const current = this.usersSubject.getValue();
        this.usersSubject.next(current.filter(u => u.id !== id));
      })
    );
  }
}

The inject() function is the modern alternative to constructor injection:

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [CommonModule],
  template: `
    @for (user of users(); track user.id) {
      <div class="user-card">
        <h3>{{ user.name }}</h3>
        <p>{{ user.email }}</p>
      </div>
    }
  `
})
export class UserListComponent {
  private userService = inject(UserService);
  users = toSignal(this.userService.users$, { initialValue: [] });
}

RxJS and Observables#

RxJS is an integral part of Angular. Observables are used in HTTP requests, reactive forms, the router, and many other places:

import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import {
  Subject, debounceTime, distinctUntilChanged,
  switchMap, takeUntil, catchError, of
} from 'rxjs';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `
    <div class="search-container">
      <input
        [formControl]="searchControl"
        placeholder="Search products..."
        class="search-input"
      />

      @if (isLoading()) {
        <div class="loading-spinner"></div>
      }

      <div class="results">
        @for (result of results(); track result.id) {
          <div class="result-item">
            <h4>{{ result.name }}</h4>
            <p>{{ result.description }}</p>
          </div>
        }
      </div>
    </div>
  `
})
export class SearchComponent implements OnInit, OnDestroy {
  private searchService = inject(SearchService);
  private destroy$ = new Subject<void>();

  searchControl = new FormControl('');
  results = signal<SearchResult[]>([]);
  isLoading = signal(false);

  ngOnInit() {
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      tap(() => this.isLoading.set(true)),
      switchMap(query =>
        this.searchService.search(query ?? '').pipe(
          catchError(() => of([]))
        )
      ),
      takeUntil(this.destroy$)
    ).subscribe(results => {
      this.results.set(results);
      this.isLoading.set(false);
    });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Angular Router#

Angular Router is a powerful navigation system with support for lazy loading, guards, resolvers, and nested routes:

import { Routes } from '@angular/router';
import { inject } from '@angular/core';
import { AuthService } from './services/auth.service';

export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./pages/home/home.component')
        .then(m => m.HomeComponent),
    title: 'Home'
  },
  {
    path: 'products',
    loadChildren: () =>
      import('./pages/products/product.routes')
        .then(m => m.PRODUCT_ROUTES),
    title: 'Products'
  },
  {
    path: 'admin',
    loadChildren: () =>
      import('./pages/admin/admin.routes')
        .then(m => m.ADMIN_ROUTES),
    canActivate: [() => inject(AuthService).isAuthenticated()],
    title: 'Admin Panel'
  },
  {
    path: '**',
    loadComponent: () =>
      import('./pages/not-found/not-found.component')
        .then(m => m.NotFoundComponent),
    title: 'Page Not Found'
  }
];

Example of nested routes with resolvers:

// product.routes.ts
export const PRODUCT_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./product-list.component')
        .then(m => m.ProductListComponent)
  },
  {
    path: ':id',
    loadComponent: () =>
      import('./product-detail.component')
        .then(m => m.ProductDetailComponent),
    resolve: {
      product: (route: ActivatedRouteSnapshot) => {
        const productService = inject(ProductService);
        return productService.getById(Number(route.paramMap.get('id')));
      }
    }
  }
];

Reactive Forms#

Reactive forms in Angular offer full control over validation and form state:

import { Component, inject } from '@angular/core';
import {
  FormBuilder, FormGroup, Validators,
  ReactiveFormsModule, AbstractControl
} from '@angular/forms';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-registration',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <div class="form-group">
        <label for="name">Full Name</label>
        <input id="name" formControlName="name" />
        @if (form.get('name')?.hasError('required') &&
             form.get('name')?.touched) {
          <span class="error">Name is required</span>
        }
      </div>

      <div class="form-group">
        <label for="email">Email</label>
        <input id="email" type="email" formControlName="email" />
        @if (form.get('email')?.hasError('email') &&
             form.get('email')?.touched) {
          <span class="error">Please enter a valid email address</span>
        }
      </div>

      <div class="form-group">
        <label for="password">Password</label>
        <input id="password" type="password" formControlName="password" />
        @if (form.get('password')?.hasError('minlength') &&
             form.get('password')?.touched) {
          <span class="error">Password must be at least 8 characters</span>
        }
      </div>

      <div class="form-group">
        <label for="confirmPassword">Confirm Password</label>
        <input id="confirmPassword" type="password"
               formControlName="confirmPassword" />
        @if (form.hasError('passwordsMismatch') &&
             form.get('confirmPassword')?.touched) {
          <span class="error">Passwords must match</span>
        }
      </div>

      <button type="submit" [disabled]="form.invalid">
        Register
      </button>
    </form>
  `
})
export class RegistrationComponent {
  private fb = inject(FormBuilder);

  form: FormGroup = this.fb.group({
    name: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
    confirmPassword: ['', Validators.required]
  }, {
    validators: this.passwordMatchValidator
  });

  passwordMatchValidator(control: AbstractControl) {
    const password = control.get('password')?.value;
    const confirmPassword = control.get('confirmPassword')?.value;
    return password === confirmPassword ? null : { passwordsMismatch: true };
  }

  onSubmit() {
    if (this.form.valid) {
      console.log('Form submitted:', this.form.value);
    }
  }
}

HttpClient and API Communication#

Angular HttpClient is a powerful tool for API communication that returns Observables and supports interceptors:

// auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from './auth.service';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();

  if (token) {
    const cloned = req.clone({
      headers: req.headers.set('Authorization', `Bearer ${token}`)
    });
    return next(cloned);
  }

  return next(req);
};

// error.interceptor.ts
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        inject(AuthService).logout();
        inject(Router).navigate(['/login']);
      }
      return throwError(() => error);
    })
  );
};

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(
      withInterceptors([authInterceptor, errorInterceptor])
    )
  ]
};

Angular CLI#

Angular CLI is an indispensable tool for working with Angular. It accelerates project creation, component generation, service scaffolding, and much more:

# Create a new project
ng new my-app --style=scss --routing --strict

# Generate components and services
ng generate component features/dashboard --standalone
ng generate service services/api
ng generate pipe pipes/currency-format
ng generate guard guards/auth --functional
ng generate interceptor interceptors/auth --functional

# Build and run
ng serve --open
ng build --configuration=production
ng test
ng e2e

# Update Angular
ng update @angular/core @angular/cli

Testing with Jasmine/Karma and Jest#

Angular has built-in testing support. Traditionally it uses Jasmine and Karma, but an increasing number of teams are switching to Jest:

// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import {
  HttpClientTestingModule,
  HttpTestingController
} from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });

    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should fetch the user list', () => {
    const mockUsers = [
      { id: 1, name: 'John Smith', email: 'john@example.com', role: 'admin' },
      { id: 2, name: 'Jane Doe', email: 'jane@example.com', role: 'user' }
    ];

    service.getUsers().subscribe(users => {
      expect(users.length).toBe(2);
      expect(users[0].name).toBe('John Smith');
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });

  it('should create a new user', () => {
    const newUser = { name: 'Peter Wilson', email: 'peter@example.com', role: 'user' };
    const createdUser = { id: 3, ...newUser };

    service.createUser(newUser).subscribe(user => {
      expect(user.id).toBe(3);
      expect(user.name).toBe('Peter Wilson');
    });

    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('POST');
    req.flush(createdUser);
  });
});

Component testing:

// counter.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CounterComponent } from './counter.component';

describe('CounterComponent', () => {
  let component: CounterComponent;
  let fixture: ComponentFixture<CounterComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [CounterComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(CounterComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should start with a value of 0', () => {
    expect(component.count()).toBe(0);
  });

  it('should increment the value when +1 is clicked', () => {
    component.increment();
    expect(component.count()).toBe(1);
    expect(component.doubleCount()).toBe(2);
  });

  it('should reset the value', () => {
    component.increment();
    component.increment();
    component.reset();
    expect(component.count()).toBe(0);
  });
});

Angular Material#

Angular Material is the official UI component library built according to Material Design guidelines:

import { Component, inject } from '@angular/core';
import { MatTableModule, MatTableDataSource } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';

@Component({
  selector: 'app-user-table',
  standalone: true,
  imports: [
    MatTableModule, MatPaginatorModule, MatSortModule,
    MatInputModule, MatFormFieldModule, MatButtonModule,
    MatIconModule, MatDialogModule
  ],
  template: `
    <mat-form-field appearance="outline" class="filter-field">
      <mat-label>Filter</mat-label>
      <input matInput (keyup)="applyFilter($event)" placeholder="Search..." />
      <mat-icon matSuffix>search</mat-icon>
    </mat-form-field>

    <table mat-table [dataSource]="dataSource" matSort>
      <ng-container matColumnDef="name">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
        <td mat-cell *matCellDef="let user">{{ user.name }}</td>
      </ng-container>

      <ng-container matColumnDef="email">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th>
        <td mat-cell *matCellDef="let user">{{ user.email }}</td>
      </ng-container>

      <ng-container matColumnDef="actions">
        <th mat-header-cell *matHeaderCellDef>Actions</th>
        <td mat-cell *matCellDef="let user">
          <button mat-icon-button color="primary" (click)="editUser(user)">
            <mat-icon>edit</mat-icon>
          </button>
          <button mat-icon-button color="warn" (click)="deleteUser(user)">
            <mat-icon>delete</mat-icon>
          </button>
        </td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
    </table>

    <mat-paginator [pageSizeOptions]="[5, 10, 25]" showFirstLastButtons />
  `
})
export class UserTableComponent {
  private dialog = inject(MatDialog);
  displayedColumns = ['name', 'email', 'actions'];
  dataSource = new MatTableDataSource<User>();

  applyFilter(event: Event) {
    const filterValue = (event.target as HTMLInputElement).value;
    this.dataSource.filter = filterValue.trim().toLowerCase();
  }

  editUser(user: User) {
    // Edit user logic
  }

  deleteUser(user: User) {
    // Delete user logic
  }
}

SSR with Angular Universal#

Server-Side Rendering (SSR) in Angular improves SEO and first-load performance. In Angular 17+, SSR configuration is significantly simpler:

# Add SSR to an existing project
ng add @angular/ssr
// app.config.server.ts
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';

const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering()
  ]
};

export const config = mergeApplicationConfig(appConfig, serverConfig);

Angular 17+ also supports client-side hydration, which eliminates page flickering:

// app.config.ts
import { provideClientHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(withFetch()),
    provideClientHydration()
  ]
};

Performance Optimization#

1. Change Detection Strategy#

Use OnPush change detection in combination with Signals:

@Component({
  selector: 'app-product-list',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (product of products(); track product.id) {
      <app-product-card [product]="product" />
    }
  `
})
export class ProductListComponent {
  products = input.required<Product[]>();
}

2. Lazy Loading and Preloading#

// Preloading configuration
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules),
      withComponentInputBinding(),
      withViewTransitions()
    )
  ]
};

3. TrackBy in Loops#

The new @for syntax requires the use of track, ensuring optimal list rendering performance:

@for (item of items(); track item.id) {
  <app-item [data]="item" />
}

4. Image Optimization#

Angular provides the NgOptimizedImage directive for image optimization:

import { NgOptimizedImage } from '@angular/common';

@Component({
  standalone: true,
  imports: [NgOptimizedImage],
  template: `
    <img ngSrc="/images/hero.webp"
         width="800"
         height="400"
         priority
         placeholder />
  `
})
export class HeroImageComponent {}

Summary#

Angular in versions 17 and 18 is a modern, mature framework that combines the best design patterns with cutting-edge APIs. Standalone components, signals, and defer blocks significantly simplify application development, while the rich ecosystem of tools - from Angular CLI to Angular Material - accelerates team productivity at every stage of a project.

The key advantages of Angular are the completeness of the solution, strong typing through TypeScript, an efficient dependency injection system, and built-in support for testing, SSR, and performance optimization. The framework excels in large enterprise applications, SaaS platforms, and anywhere that scalability and maintainability are required.

Need Help with Your Angular Project?#

At MDS Software Solutions Group, we specialize in building modern web applications with Angular. We offer:

  • Building Angular applications from scratch
  • Migrations from older Angular versions and AngularJS
  • Performance optimization for existing applications
  • SSR and prerendering implementation
  • Integration with .NET, Node.js, and other backends

Contact us to discuss your project!

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

Angular - The Complete Frontend Framework 2025 | MDS Software Solutions Group | MDS Software Solutions Group