Skip to main content

Part 2 — Structuring User Interfaces with Components


Structuring User Interfaces with Components

Angular applications are built from components —components: small, isolatedfocused unitsbuilding responsibleblocks forthat renderingeach own a part of the UIuser interface and handling its logic. Each component controls a section of the screen, can receive data, can output events, and participates in a hierarchical tree of parent and child components.behavior.

In this chapter you will learn:

  • How toan createAngular components20 component is structured
  • How tothe connectCLI agenerates componentcomponents towith itsthe HTMLnew templatenaming and CSSscheme
  • How to display dynamic data
  • How toand control whatdata isin showntemplates using(with Angular’sthe modern control-flow@if, syntax@for, @switch syntax)
  • How components communicatetalk withto each other using inputs and outputs
  • How to style components and encapsulate component-specificmanage CSS encapsulation
  • How to work with component lifecycle hooks
  • How Angular handlesand change detection work at a high level
  • Where older syntax (*ngIf, @Input, etc.) still appears and how to read it

You will also see short notes explaining how older Angular versions handled these tasks, so you understand both modern best practice and past conventions.


Anatomy of an Angular Component (Angular 20 Style)

AIn componentAngular consists20, of:when you generate a component, the CLI now uses simpler file names:

Example:The idea is to reduce redundancy: the file name already tells you what this unit is.

A minimal root component looks like this:

// src/app/app.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrl: './app.component.css',
  imports: [RouterOutlet],
  standalone: true
})
export class AppComponentApp {
  title = 'World';
}

Key

Explanation:

metadata
    properties

  • selector:
    Property'app-root' Purpose
    `selector`the Tagtag nameyou usedwill use in templatesindex.html.
    `
  • templateUrl`

    /

    styleUrl <span class="editor-theme-code">template</span>

    point
  • Definesto the UIexternal
    `styleUrl`HTML

    /

    and

    CSS files.

  • <spanimports: class="editor-theme-code">styles</span>[RouterOutlet]

  • Defines– because Angular 16+ uses standalone components, every component CSSexplicitly
    `imports` Standalonewhat componentsit needs (other components, directives, pipes).
  • standalone: true – tells Angular that this oneclass dependsstands on
  • its

    Modernown and is not declared in an NgModule.

  • The class is named App (not AppComponent) to match the new naming style.
  • Legacy note: In older Angular (v16+)versions, noteyou

    Standalonewould componentstypically aresee:

    now
      the
    • File: standard.
      app.component.ts
    • They
    • Class: useAppComponent
    • the
    • No <spanstandalone: class="editor-theme-code">true and no imports</span> array instead(components ofwere NgModules.

      Legacy note

      Older projects used NgModules and did not include the <span class="editor-theme-code">imports</span> arraydeclared in theNgModules).

    • <span class="editor-theme-code">@Component</span> decorator.


    Creating a Component with the CLI

    InsideTo yourgenerate a feature component in Angular project folder:20:

    ng generate component product-list
    

    ThisWith generates:the new naming convention, this will create:

    src/app/product-list/
      product-list.component.ts
    src/app/product-list/product-list.component.html
    src/app/product-list/product-list.component.css
    src/app/product-list/product-list.component.spec.ts
    

    The TypeScript file might look like this:

    // src/app/product-list/product-list.ts
    import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-product-list',
      templateUrl: './product-list.html',
      styleUrl: './product-list.css',
      standalone: true
    })
    export class ProductList {
    }
    

    Explanation:

    • The class name is ProductList (not ProductListComponent), consistent with the updated Angular 20 style guide.(Ninja Squad Blog)
    • This component does not import anything yet; we will add imports later when needed.
    • standalone: true makes this component directly usable in other components via the imports array.

    To use yourthis newcomponent component,inside App, you import it intoand anotheradd component:it to the imports array:

    // src/app/app.ts
    import { ProductListComponentComponent } from '@angular/core';
    import { RouterOutlet } from '@angular/router';
    import { ProductList } from './product-list/product-list.component'list';
    
    @Component({
      selector: 'app-root',
      imports: [RouterOutlet, ProductListComponent],
      templateUrl: './app.component.html',
      styleUrl: './app.component.css',
      standalone: true,
      imports: [RouterOutlet, ProductList]
    })
    export class AppComponentApp {
      title = 'World';
    }
    

    And placein itsapp.html:

    <!-- src/app/app.html -->
    <div class="content">
      <app-product-list></app-product-list>
    </div>
    

    Explanation:

    • Importing ProductList in App and putting it into imports makes Angular aware of the app-product-list selector in thethis HTML:

      template.
    • The template then simply uses <app-product-list>, />which Angular binds to the ProductList
    •  class.

    Displaying Data in the Template

    AngularComponent templates supportcan multiplerender waysvalues to bring component data intofrom the UI.class using interpolation or property binding.

    Interpolation

    <h1>Hello, {{ title }}</h1>
    

    Explanation:

    • {{ title }} is interpolation; Angular evaluates title in the component instance and inserts its string value into the DOM.

    Property Bindingbinding

    <h1 [innerText]="title"></h1>
    

    Important

    Explanation:

    Property

    • [innerText]="title" binds the DOM property innerText of the <h1> to the title property of the component.
    • The square brackets indicate one-way binding targetsfrom DOMthe properties,component notto HTMLthe attributes.

      DOM.

    Angular’s Modern Control FlowFlow: Syntax (@if, @for, @switch)@switch

    Angular 1717+ introduced a new built-in control-flow syntax that is:

    is
    • Moremore readable
    • Faster
    • and
    • Closermore toefficient JavaScript
    • than
    • Smallerthe bundle size compared toolder directive-based templates
    approach.

    Conditional rendering with @if

    (conditional

    Example rendering)with a product list:

    // product-list.ts
    import { Component } from '@angular/core';
    
    interface Product {
      id: number;
      title: string;
    }
    
    @Component({
      selector: 'app-product-list',
      templateUrl: './product-list.html',
      styleUrl: './product-list.css',
      standalone: true
    })
    export class ProductList {
      products: Product[] = [];
    }
    
    <!-- product-list.html -->
    @if (products.length > 0) {
      <h1>Products ({{ products.length }})</h1>
    } @else {
      <p>No products found!</p>
    }
    

    Explanation:

    • @if decides whether the block of HTML should exist in the DOM at all.
    • If products.length > 0, Angular adds the <h1> to the DOM; otherwise, it adds the <p>.

    Legacy note (pre-Angularolder 17)

    <span class="editor-theme-code">*ngIf</span> was used:Angular):

    <h1 *ngIf="products.length > 0">Products ({{ products.length }})</h1>
    <p *ngIf="products.length === 0">No products found!</p>
    

    *ngIf is still supported, but the new @if syntax is the recommended style going forward.


    Looping over data with @for

    (looping)

    Let’s populate some mock products:

    // product-list.ts
    export class ProductList {
      products: Product[] = [
        { id: 1, title: 'Keyboard' },
        { id: 2, title: 'Microphone' },
        { id: 3, title: 'Web camera' },
        { id: 4, title: 'Tablet' }
      ];
    }
    

    Now, use @for in the template:

    <!-- product-list.html -->
    <ul class="pill-group">
      @for (product of products; track product.id) {
        <li class="pill">{{ product.title }}</li>
      } @empty {
        <p>No products foundfound!</p>
      }
    </ul>
    

    Why

    Explanation:

    • <span@for class="editor-theme-code">(product of products; track</span> product.id) iterates over products and exposes each item as product.
    • track product.id tells Angular to use the id field to keep DOM nodes stable when items change, improving performance.
    • @empty defines what to show when products is important

    an empty array.

    It helps Angular identify which DOM element corresponds to which data item, improving rendering performance.

    Legacy note

    Older Angular templates used <span class="editor-theme-code">*ngFor</span>:note:

    <li *ngFor="let product of products">{{ product.title }}</li>
    

    *ngFor is the older syntax with similar behavior.


    Switching templates with @switch

    You can pick different content based on a value:

    <!-- product-list.html -->
    <ul class="pill-group">
      @for (product of products; track product.id) {
        <li class="pill">
          @switch (product.title) {
            @case ('Keyboard') { 🎹 }
            @case ('Microphone') { 🎤 }
            @default { 📦 }
          }
          {{ product.title }}
        </li>
      } @empty {
        <p>No products found!</p>
      }
    </ul>
    

    Explanation:

    • @switch (product.title) compares the title for each product.
    • @case defines what to render when the expression matches a specific value.
    • @default is rendered when no case matches.

    Legacy note

    Previously:note:

    <div [ngSwitch]="product.title">
      <pspan *ngSwitchCase="'Keyboard'">🎹</pspan>
      <pspan *ngSwitchCase="'Microphone'">🎤</span>
      <span *ngSwitchDefault>📦</pspan>
    </div>
    

    Again, [ngSwitch] and *ngSwitchCase are the older equivalents.


    Adding Interactivity: Events andHandling User InputInteraction (Event Binding)

    To reactsend information from the template back to userthe actions,component, Angular uses event bindingbindings.

    Extend the ProductList class:

    // product-list.ts
    export class ProductList {
      products: Product[] = [
        { id: 1, title: 'Keyboard' },
        { id: 2, title: 'Microphone' },
        { id: 3, title: 'Web camera' },
        { id: 4, title: 'Tablet' }
      ];
    
      selectedProduct: Product | undefined;
    }
    

    Update the template:

    <!-- product-list.html -->
    <ul class="pill-group">
      @for (product of products; track product.id) {
        <li class="pill" (click)="selectedProduct = product">
          {{ product.title }}
        </li>
      } @empty {
        <p>No products found!</p>
      }
    </ul>
    
    @if (selectedProduct) {
      <p>You selected: <strong>{{ selectedProduct.title }}</strong></p>
    }
    

    Explanation:

    • (click)="selectedProduct = product" listens for the browser’s click event and executes the assignment in the component instance.
    • selectedProduct becomes the currently clicked product, and the @if block below reacts by showing its title.

    Styling Components and View Encapsulation

    Angular lets you bind classes and styles dynamically.

    Class binding

    <li
      class="pill"
      [class.selected]="selectedProduct && selectedProduct.id === product.id"
    >
      {{ product.title }}
    </li>
    

    Explanation:

    • <span class="editor-theme-code">(click)</span> — target event
    • The right-hand expression — TypeScript code executed when clicked

    All browser events are supported.


    Styling Components

    Class Binding

    <p [class.selected]="isSelected"></p>...condition..." will add or remove the selected
     class based on whether the condition evaluates to true or false.

    You can passalso bind an entire object with boolean conditions:object:

    currentClasses// =product-list.ts
    isSelected(product: Product) {
      selected:return true,this.selectedProduct?.id highlighted:=== falseproduct.id;
    };
    
    <pli
      class="pill"
      [class]class.selected]="currentClasses"isSelected(product)"
    >
      {{ product.title }}
    </pli>
    

    Style Bindingbinding

    <p [style.color]="selectedProduct ? 'red'green' : 'inherit'"></p>
      <p{{ [style.width.px]="120">selectedProduct ? 'Product chosen' : 'No product selected' }}
    </p>
    

    Object syntax:

    currentStyles = {
      color: 'red',
      width: '100px'
    };
    
    <p [style]="currentStyles"></p>
    

    View Encapsulation

    Angular normally isolates component CSS:

    encapsulation: ViewEncapsulation.Emulated // default
    

    Other modes:

    Mode Description
    `Emulated` Scoped CSS (default)
    `None` CSS leaks globally
    `ShadowDom` Uses browser’s native Shadow DOM

    Example turning isolation off:

    encapsulation: ViewEncapsulation.None
    

    Use this carefully.


    Component Communication

    Components interact via:Explanation:

    • Inputs[style.color] (data goes down intocontrols a child)
    • single
    • Outputsstyle (eventsproperty godynamically, upbased fromon acomponent child to a parent)state.

    PassingView Data Down (Input Binding)encapsulation

    ChildBy component:default, Angular scopes CSS per component (Emulated mode), so styles from product-list.css will only affect that component’s template.

    import { Component, inputViewEncapsulation } from '@angular/core';
    import { Product } from '../product';
    
    @Component({
      selector: 'app-product-detail',
      templateUrl: './product-detail.component.html',
      styleUrl: './product-detail.css',
      standalone: true,
      encapsulation: ViewEncapsulation.Emulated // default
    })
    export class ProductDetailComponentProductDetail {
    }
    

    If you explicitly set:

    encapsulation: ViewEncapsulation.None
    

    then styles defined in product-detail.css can leak into other parts of the app. This can be useful for global styling, but must be used carefully.


    Passing Data Between Components (Inputs and Outputs)

    Real-world applications rarely keep all UI in a single component. Often, a parent component owns the data and passes a piece of it down to a child component.

    Passing data down with input()

    Create a detail component:

    // src/app/product-detail/product-detail.ts
    import { Component, input } from '@angular/core';
    import type { Product } from '../product-list/product-list';
    
    @Component({
      selector: 'app-product-detail',
      templateUrl: './product-detail.html',
      styleUrl: './product-detail.css',
      standalone: true
    })
    export class ProductDetail {
      product = input<Product>();
    }
    

    Parent template:Template:

    <!-- product-detail.html -->
    @if (product()) {
      <p>
        You selected:
        <strong>{{ product()!.title }}</strong>
      </p>
    }
    

    Explanation:

    • product = input<Product>() defines an input signal for this component.
    • In the template, product() reads the current value of that input.
    • The @if guard ensures we only render details when a product is actually provided.

    Now, use ProductDetail in ProductList:

    // product-list.ts
    import { Component } from '@angular/core';
    import { ProductDetail } from '../product-detail/product-detail';
    
    @Component({
      selector: 'app-product-list',
      templateUrl: './product-list.html',
      styleUrl: './product-list.css',
      standalone: true,
      imports: [ProductDetail]
    })
    export class ProductList {
      products: Product[] = [ /* ... */ ];
      selectedProduct: Product | undefined;
    }
    
    <!-- product-list.html -->
    <ul class="pill-group">
      @for (product of products; track product.id) {
        <li class="pill" (click)="selectedProduct = product">
          {{ product.title }}
        </li>
      } @empty {
        <p>No products found!</p>
      }
    </ul>
    
    <app-product-detail [product]="selectedProduct" ></app-product-detail>
    

    Required

    Explanation:

    inputs

    • [product]="selectedProduct" binds the parent’s selectedProduct property into the child’s product input.
    • Angular takes care of updating the child when the parent selection changes.

    Legacy note: Previously, you would see:

    @Input() product!: Product;
    

    instead of product = input.requiredinput<Product>(); .


    Sending Eventsevents Upup (Outputwith Binding)output()

    Child:Let the detail component notify the parent that the user wants to add the product to a cart.

    In ProductDetail:

    import { Component, input, output } from '@angular/core';
    import type { Product } from '../product-list/product-list';
    
    @Component({
      selector: 'app-product-detail',
      templateUrl: './product-detail.html',
      styleUrl: './product-detail.css',
      standalone: true
    })
    export class ProductDetail {
      product = input<Product>();
      added = output<Product>();
    
      addToCart() {
        if (this.product()) {
          this.added.emit(this.product()!);
        }
      }
    }
    

    Parent:Template:

    <!-- product-detail.html -->
    @if (product()) {
      <div>
        <p>
          You selected:
          <strong>{{ product()!.title }}</strong>
        </p>
        <button (click)="addToCart()">Add to cart</button>
      </div>
    }
    

    Explanation:

    • added = output<Product>() declares an output event that can carry a Product payload.
    • this.added.emit(...) triggers the event.

    In the parent (ProductList):

    // product-list.ts
    onAdded(product: Product) {
      alert(`${product.title} added to the cart!`);
    }
    
    <!-- product-list.html -->
    <app-product-detail
      [product]="selectedProduct"
      (added)="onAdded($event)"
    ></app-product-detail>
    

    ParentExplanation:

    TS:
    • (added)="onAdded($event)" listens to the child’s added output.
    • $event contains the product emitted by addToCart().
    • The parent can now update a cart, fire analytics, or display a message.

    Legacy note: Older Angular projects use:

    onAdded(product: Product) {
      alert(`${product.title} added to cart!`);
    }
    

    Legacy note

    In older Angular:

    @Input() product!: Product;
    @Output() added = new EventEmitter<Product>();
    

    instead of added = output<Product>().


    Template Reference Variables and viewChild

    UsedSometimes you need direct access to accessa achild component orinstance.

    element

    Template instance:

    reference variable

    <!-- product-list.html -->
    <app-product-detail
      #detail
      [product]="selectedProduct"
      (added)="onAdded($event)"
    ></app-product-detail>
    
    <p *ngIf="detail.product()">
      Detail says: {{ detail.product()!.title }}
    </p>
    

    AExplanation:

    • #detail creates a template reference variableto the ProductDetail instance.
    • This reference exposes the public API of theProductDetail component.

      (here:
      product()).
    • Accessing

    Child Components from TypeScript

    Instead of using

    Querying a templatechild variable,in weTypeScript with viewChild

    You can queryalso get the child viainstance <spanfrom class="editor-theme-code">viewChild</span>.

    the

    Parentparent component:class:

    // product-list.ts
    import { viewChild,Component, AfterViewInitAfterViewInit, viewChild } from '@angular/core';
    import { ProductDetailComponentProductDetail } from '../product-detail/product-detail.component'detail';
    
    @Component({
      selector: 'app-product-list',
      templateUrl: './product-list.html',
      styleUrl: './product-list.css',
      standalone: true,
      imports: [ProductDetail]
    })
    export class ProductListComponentProductList implements AfterViewInit {
      productDetail = viewChild(ProductDetailComponent)ProductDetail);
    
      ngAfterViewInit(): void {
        console.log('ChildDetail product:', this.productDetail()?.product());
      }
    }
    

    Explanation:

    • viewChild(ProductDetail) tells Angular to look for a ProductDetail in this component’s view.
    • ngAfterViewInit is the hook where the child is guaranteed to be created and accessible.

    Legacy note

    note:

    Previously:

    @ViewChild(ProductDetailComponent)ProductDetail) productDetail!: ProductDetailComponent;ProductDetail;
    

    Change Detection Strategy

    Angular automatically refreshes views when data changes. By default, it runs change detection for the UIentire whencomponent ittree detectson changes.
    each Tworelevant strategies are available:event.

    Strategy

    You

    Behaviorcan
    `Default`optimize Checksthis allwith components often
    `ChangeDetectionStrategy.OnPush` Only updates when @Input references change or events occur

    Enable <span class="editor-theme-code">OnPush</span>:

    import { Component, ChangeDetectionStrategy } from '@angular/core';
    
    @Component({
      selector: 'app-product-detail',
      templateUrl: './product-detail.html',
      styleUrl: './product-detail.css',
      standalone: true,
      changeDetection: ChangeDetectionStrategy.OnPush
    })
    export class ProductDetail {
      // ...
    }
    

    Explanation:

    • With <spanOnPush, class="editor-theme-code">OnPush</span>Angular dramaticallywill only re-check this component when:

      • An input reference changes
      • An event handler on this component runs
      • An observable bound in the template emits (via async pipe), etc.
    • This significantly improves performance for:in large and complex UIs.

      • Large lists
      • Data-heavy UIs
      • Dashboard-like interfaces

      Component Lifecycle Hooks Overview

      Lifecycle hooks letallow you to run codecustom logic at specific moments.moments in a component’s life.

      Hook

      Common

      Whenhooks:

      it
      • ngOnInit – runs
      `ngOnInit`after Afterthe component’s inputs are set;first set.
    • ngOnDestroy – runs right before Angular removes the component initializedfrom
    • `ngOnDestroy`the BeforeDOM. component
    • ngOnChanges is removedruns
    • `ngOnChanges` Wheneverwhenever an input valuebinding changes.
    • ngAfterViewInit – runs after the view and child views have been initialized.
    • Example:

      import {
        Component,
        OnInit,
        OnDestroy,
        OnChanges,
        SimpleChanges,
        AfterViewInit
      } from '@angular/core';
      
      @Component({
        selector: 'app-product-detail',
        templateUrl: './product-detail.html',
        styleUrl: './product-detail.css',
        standalone: true
      })
      export class ProductDetail
        implements OnInit, OnDestroy, OnChanges, AfterViewInit {
      
        ngOnInit(): void {
          // Good place to fetch data or initialize values
          console.log('ProductDetail initialized');
        }
      
        ngOnChanges(changes: SimpleChanges): void {
          // React to input changes
          
      `ngAfterViewInit`console.log('Changes:', Afterchanges); child} ngAfterViewInit(): void { // Child components and viewsview are ready

      ngOnInit

      export class ProductDetailComponent implements OnInit {
        ngOnInit() {
          console.log('Product:',View this.product()initialized');
        }
      }
      

      Why not use the constructor?

      Inputs are not yet assigned when the constructor runs.


      ngOnDestroy

      Used for cleanup:

      export class ProductDetailComponent implements OnDestroy {
      
        ngOnDestroy(): void {
          // Cleanup: timers, subscriptions, listeners, etc.
          console.log('ComponentProductDetail destroyed');
        }
      }
      

      Common use cases:Explanation:

      • Clearing timers

        Each hook gives you a predictable place to put specific kinds of logic:

        • ngOnInit instead of doing heavy work in the constructor.
        • UnsubscribingngOnDestroy fromto RxJSrelease streamsresources.
        • ReleasingngOnChanges eventto listenersreact to new input values.
        • ngAfterViewInit to work with child components or DOM elements that weren’t available earlier.

        Alternative:

      • DestroyRef (modern, recommended)
        constructor(destroyRef: DestroyRef) {
          destroyRef.onDestroy(() => {
            // cleanup
          });
        }
        

        ngOnChanges

        Triggered when an input changes:

        ngOnChanges(changes: SimpleChanges): void {
          const product = changes['product'];
          if (!product.isFirstChange()) {
            console.log('Old:', product.previousValue);
            console.log('New:', product.currentValue);
          }
        }
        

        Modern alternative

        Signals can track changes more elegantly (Chapter 7).


        ngAfterViewInit

        Useful for reading child component data:

        ngAfterViewInit() {
          console.log(this.productDetail()?.product());
        }
        

      Summary

      In this chapter,chapter you learned:have seen how, in Angular 20:

      • HowComponents are generated with simpler file names like product-list.ts, product-list.html, product-list.css.
      • Standalone components (standalone: true) and explicit imports have become the default way to createstructure standalonean componentsapp.
      • HowThe modern control-flow syntax (@if, @for, @switch) replaces older structural directives in new code, while you still need to connectunderstand templates,*ngIf, classes,*ngFor and stylesngSwitch for legacy templates.
      • HowData toflows useinto moderncomponents controlvia flowinput() (and out via <span class="editor-theme-code">@if</span>output(), replacing <span@Input class="editor-theme-code">@for</span>and @Output, <spanin class="editor-theme-code">@switch</span>)new code.
      • How to displayClass and updatestyle data
      • bindings,
      • How to handle events and user interactions
      • How components communicate (inputs & outputs)
      • How to use template reference variables
      • How to manage CSSalong with view encapsulationencapsulation, give you fine control over component-level CSS.
      • HowTemplate toreference optimizevariables updatesand withviewChild changelet detection
      • you
      • Howreach to hookdeeper into the component tree when necessary.
      • Change detection strategies and lifecycle hooks help you tune both performance and behavior.