Skip to main content

Overview

Components are the building blocks of Angular applications. This guide shows you how to create components using Angular 21 patterns, including signals, the inject() function, and standalone components.

Component architecture

The Angular PWA Demo uses two types of components:

Standalone components

Self-contained components that declare their own dependencies

Module-based components

Traditional components registered in NgModule

Creating a standalone component

Standalone components are the modern approach in Angular 21. They don’t require NgModule registration.
1

Generate the component

Use the Angular CLI to create a new component:
ng generate component _components/my-component --standalone
2

Configure the component decorator

Set standalone: true and declare imports:
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';

@Component({
  selector: 'app-my-component',
  templateUrl: './my-component.component.html',
  styleUrl: './my-component.component.css',
  standalone: true,
  imports: [RouterLink]
})
export class MyComponent {
  // Component logic
}
3

Use the component

Import it directly in parent components or routes:
import { MyComponent } from './_components/my-component/my-component.component';

@Component({
  imports: [MyComponent],
  standalone: true
})
export class ParentComponent {}

Real-world example: Speech panel component

Here’s a production example from the codebase at src/app/_components/speech-panel/speech-panel.component.ts:1:
import { Component, EventEmitter, Output, Input } from '@angular/core';
import { SpeechService } from 'src/app/_services/__Utils/SpeechService/speech.service';

@Component({
  selector: 'app-speech-panel',
  templateUrl: './speech-panel.component.html',
  styleUrl: './speech-panel.component.css',
  standalone: true
})
export class SpeechPanelComponent {
  ListeningButtonIconOn: string = './assets/images/mic_on.gif';
  ListeningButtonIconOff: string = './assets/images/mic_off.gif';
  SpeakerIcon: string = './assets/images/speaker_on.gif';
  clearFormIcon: string = './assets/images/clearForm.gif';
  
  @Output() clickEventSpeak = new EventEmitter<string>();
  @Output() clickEventClearText = new EventEmitter<void>();

  constructor(public speechService: SpeechService) {}

  speakText(): void {
    this.clickEventSpeak.emit(this.speechService.speakText());
  }

  clearText(): void {
    this.clickEventClearText.emit();
  }
}

Key features demonstrated

  • Standalone component: No NgModule required
  • Service injection: Constructor-based DI for services
  • Event emitters: Parent-child communication with @Output()
  • Type safety: Proper TypeScript typing for events

Using signals for reactive state

Angular 21 introduces signals for reactive state management. Here’s how it’s used in the root component at src/app/app.component.ts:1:
import { Component, OnInit, inject, signal } from '@angular/core';
import { ConfigService } from './_services/__Utils/ConfigService/config.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  standalone: false
})
export class AppComponent implements OnInit {
  // Modern inject() function instead of constructor DI
  private readonly _configService = inject(ConfigService);
  private readonly titleService = inject(Title);

  // Reactive signals
  public readonly title = signal<string>('');
  public readonly appBrand = signal<string>('');
  public readonly appVersion = signal<string>('');

  ngOnInit(): void {
    this.initializeConfig();
  }

  private initializeConfig(): void {
    const brand = this._configService.getConfigValue('appBrand') ?? 'App';
    const version = this._configService.getConfigValue('appVersion') ?? '1.0.0';

    // Update signals
    this.appBrand.set(brand);
    this.appVersion.set(version);
    this.title.set(brand);

    this.titleService.setTitle(`${brand} - ${version}`);
  }
}
Signals provide better performance than traditional change detection and make reactive patterns more explicit.

Component with dependency injection

The landing component shows advanced patterns including @Input() and async initialization at src/app/_components/landing/landing.component.ts:1:
import { AfterViewInit, Component, Input } from '@angular/core';
import { Params, RouterLink } from '@angular/router';
import { ConfigService } from 'src/app/_services/__Utils/ConfigService/config.service';

@Component({
  selector: 'app-landing',
  templateUrl: './landing.component.html',
  styleUrl: './landing.component.css',
  imports: [RouterLink],
  standalone: true
})
export class LandingComponent implements AfterViewInit {
  public _pages_nested: any[] = [];
  
  @Input() LandingPage?: string = undefined;

  constructor(public configService: ConfigService) {}

  ngAfterViewInit(): void {
    this.loadLandingPage();
  }

  loadLandingPage() {
    this.configService._loadMainPages().then(() => {
      const pageData = _environment.mainPageListDictionary?.[PAGE_ANGULAR_DEMO_LANDING];
      const pageName = this.LandingPage ? this.LandingPage : pageData?.page_name;

      if (pageName && typeof pageName === 'string') {
        console.log(`Selected Landing Page: ${pageName}`);
        
        if (_environment.mainPageListDictionary[pageName].pages_nested !== null) {
          this._pages_nested = _environment.mainPageListDictionary[pageName].pages_nested;
          this._pages_nested = this._pages_nested.map(page => ({
            ...page,
            queryParamsObj: this.parseQueryParams(page.queryParams)
          }));
        }
      }
    });
  }

  parseQueryParams(str: string): Params {
    if (typeof str !== 'string' || !str.trim()) {
      return {};
    }
    const cleanStr = str.replace(/{|}/g, '').replace(/['"], /g, '').trim();
    return cleanStr.split(',').reduce((params, pair) => {
      const [key, value] = pair.split(':').map(s => s.trim());
      if (key && value) {
        params[key] = value;
      }
      return params;
    }, {} as Params);
  }
}

Key patterns

  • Input properties: Accept data from parent components
  • Lifecycle hooks: AfterViewInit for post-render initialization
  • Async operations: Promise-based data loading
  • Data transformation: Processing configuration data

Module-based components

For traditional module-based components, set standalone: false at src/app/_components/search/search.component.ts:7:
import { Component, QueryList, ViewChildren } from '@angular/core';
import { Observable } from 'rxjs';
import { SearchService } from 'src/app/_services/__Utils/SearchService/search.service';

@Component({
  selector: 'app-search-custom',
  templateUrl: './search.component.html',
  styleUrl: './search.component.css',
  standalone: false
})
export class SearchComponent {
  public __pages!: Observable<_BaseModel[]>;
  public __total!: Observable<number>;
  
  @ViewChildren(BaseSortableHeader) _headers: QueryList<BaseSortableHeader> | undefined;

  constructor(public searchService: SearchService) {
    this.__pages = this.searchService.Pagelist;
    this.__total = this.searchService.total;
  }

  onSort({ _column, _direction }: _BaseSortEvent) {
    this._headers?.forEach((__header) => {
      if (__header.sortable !== _column) {
        __header.direction = '';
      }
    });
    this.searchService.sortColumn = _column;
    this.searchService.sortDirection = _direction;
  }
}
Module-based components must be declared in an NgModule’s declarations array.

Best practices

Prefer signals over BehaviorSubject for component state. Signals provide better performance and type safety.
Use standalone components for new development. They’re simpler and don’t require NgModule boilerplate.
The inject() function is more flexible than constructor injection and works outside constructors.
Each component should have a single responsibility. Extract complex logic into services.
For better performance, use ChangeDetectionStrategy.OnPush with signals.

Next steps

Services guide

Learn about services and dependency injection

Configuration guide

Configure your application

Build docs developers (and LLMs) love