Introduction

Welcome to Chapter 2 of your comprehensive Angular interview preparation! This chapter delves into the foundational building blocks of any Angular application: Components, Directives, and Pipes. Mastering these concepts is crucial for building robust, maintainable, and efficient user interfaces. Interviewers frequently assess a candidate’s understanding of these core elements, often moving beyond basic definitions to advanced use cases, performance considerations, and custom implementations.

Whether you’re an entry-level developer aiming to grasp the fundamentals, a mid-level professional looking to solidify your knowledge and optimize existing applications, or a senior engineer preparing to discuss architectural patterns and complex custom solutions, this chapter provides targeted questions and in-depth answers. We’ll cover everything from lifecycle hooks and communication strategies to custom directive and pipe creation, ensuring your knowledge is current with Angular versions 13 through 21 and modern best practices as of December 2025.

By the end of this chapter, you will be well-equipped to articulate your understanding, demonstrate practical application, and discuss advanced scenarios related to Components, Directives, and Pipes, making you a confident candidate in your next Angular interview.

Core Interview Questions

1. What is an Angular Component? Describe its core elements.

  • Q: What is an Angular Component? Describe its core elements and how it interacts with the template.
  • A: An Angular Component is the most fundamental building block of an Angular application, responsible for controlling a specific part of the screen called a view. It combines an HTML template for the UI, a TypeScript class for the logic, and CSS styles for presentation.
    • Core Elements:
      1. Template: An HTML file (or inline HTML) that defines the component’s view. It uses Angular template syntax for data binding, directives, and event handling.
      2. Class (Controller): A TypeScript class decorated with @Component() that contains the component’s logic, properties, and methods. It interacts with the template through data binding.
      3. Metadata: Provided by the @Component() decorator, it configures the component. Key properties include:
        • selector: A CSS selector that tells Angular to create and insert an instance of this component wherever it finds the corresponding tag in the HTML.
        • templateUrl (or template): Path to the component’s HTML template file or inline HTML.
        • styleUrls (or styles): Paths to the component’s CSS style files or inline CSS.
        • standalone (Angular v14+): A boolean flag to indicate if the component is standalone, meaning it doesn’t need to be declared in an NgModule.
      4. Styles: CSS definitions scoped specifically to the component’s template, preventing style conflicts with other parts of the application.
  • Key Points:
    • Components are essentially specialized directives.
    • They encapsulate UI and logic.
    • The selector property is crucial for using the component in other templates.
    • Angular’s component-based architecture promotes reusability and modularity.
    • Standalone components (Angular v14+) simplify module organization.
  • Common Mistakes:
    • Confusing components with directives (components are directives with a template).
    • Forgetting to declare a component in an NgModule (for non-standalone components) or import necessary dependencies (for standalone components).
    • Overloading components with too much logic, violating the Single Responsibility Principle.
  • Follow-up: How do standalone components change the way we structure Angular applications?

2. Explain the Angular Component Lifecycle Hooks.

  • Q: Explain the Angular Component Lifecycle Hooks. List the most common ones and describe their typical use cases.
  • A: Angular components have a lifecycle managed by Angular itself. As Angular creates, updates, and destroys components, it invokes specific lifecycle hook methods, allowing developers to tap into these moments to perform actions.
    • Common Lifecycle Hooks (in order of execution):
      1. ngOnChanges(): Called before ngOnInit (and whenever one or more data-bound input properties change). Use it to react to changes in @Input() properties.
      2. ngOnInit(): Called once after Angular initializes the component’s data-bound input properties and sets up the component. Ideal for initialization logic that doesn’t rely on constructor arguments, such as fetching initial data.
      3. ngDoCheck(): Called immediately after ngOnChanges and ngOnInit (and every subsequent change detection run). Use it for custom change detection logic when Angular’s default mechanism is insufficient. (Use with caution due to performance implications).
      4. ngAfterContentInit(): Called once after Angular projects external content into the component’s view. Use it to perform initialization logic on projected content.
      5. ngAfterContentChecked(): Called after ngAfterContentInit and every subsequent ngDoCheck. Use it to respond to changes in the projected content.
      6. ngAfterViewInit(): Called once after Angular initializes the component’s view and child views. Use it to interact with child components or template elements via @ViewChild() or @ViewChildren().
      7. ngAfterViewChecked(): Called after ngAfterViewInit and every subsequent ngDoCheck. Use it to respond to changes in the component’s view and child views.
      8. ngOnDestroy(): Called just before Angular destroys the component. Use it to clean up, such as unsubscribing from Observables, detaching event handlers, or clearing intervals to prevent memory leaks.
  • Key Points:
    • Each hook corresponds to an interface (e.g., OnInit, OnChanges).
    • ngOnInit is the most commonly used for component initialization.
    • ngOnDestroy is vital for preventing memory leaks.
    • The “Checked” hooks (ngAfterContentChecked, ngAfterViewChecked) are called frequently during change detection.
  • Common Mistakes:
    • Performing heavy operations in ngDoCheck() without careful optimization.
    • Not unsubscribing from Observables in ngOnDestroy().
    • Trying to access @ViewChild elements before ngAfterViewInit().
  • Follow-up: When would ngDoCheck() be necessary, and what are its potential pitfalls?

3. Differentiate between @Input() and @Output() decorators.

  • Q: Differentiate between @Input() and @Output() decorators in Angular component communication. Provide a scenario where both are used.

  • A: @Input() and @Output() are decorators used for communication between parent and child components.

    • @Input():
      • Purpose: Allows a child component to receive data from its parent component.
      • Mechanism: Marks a property in the child component as an input property. The parent component can then bind to this property using property binding [] in its template.
      • Direction: Data flows down from parent to child.
      • Example: A UserDetailComponent receiving a userId from its parent.
    • @Output():
      • Purpose: Allows a child component to send data or events up to its parent component.
      • Mechanism: Marks a property in the child component as an output property. This property must be an instance of EventEmitter<T>, which emits custom events. The parent component listens to these events using event binding () in its template.
      • Direction: Events/data flow up from child to parent.
      • Example: A SaveButtonComponent emitting a saved event to its parent when clicked.

    Scenario: A ProductListComponent (parent) displays a list of products. Each product is rendered by a ProductCardComponent (child).

    • ProductListComponent passes product data to ProductCardComponent using @Input().
      // product-list.component.html
      <app-product-card *ngFor="let product of products" [product]="product" (addToCart)="onAddToCart(product)"></app-product-card>
      
    • ProductCardComponent emits an addToCart event when a button is clicked, sending the product back to ProductListComponent using @Output().
      // product-card.component.ts
      import { Component, Input, Output, EventEmitter } from '@angular/core';
      
      @Component({
        selector: 'app-product-card',
        template: `
          <div>
            <h3>{{ product.name }}</h3>
            <p>{{ product.price | currency }}</p>
            <button (click)="addToCart.emit(product)">Add to Cart</button>
          </div>
        `,
        standalone: true // Example for Angular v14+
      })
      export class ProductCardComponent {
        @Input() product: any;
        @Output() addToCart = new EventEmitter<any>();
      }
      
  • Key Points:

    • @Input() uses property binding [].
    • @Output() uses event binding () and EventEmitter.
    • Essential for creating reusable and modular components.
  • Common Mistakes:

    • Trying to send data from parent to child using @Output().
    • Trying to send data from child to parent directly without an EventEmitter.
    • Not calling .emit() on the EventEmitter to trigger the event.
  • Follow-up: What are alternative methods for component communication, especially for unrelated components or global state?

4. How do you optimize change detection in Angular components?

  • Q: How do you optimize change detection in Angular components, especially in large applications? Discuss OnPush strategy and potentially signals (Angular v17+).
  • A: Optimizing change detection is crucial for application performance. Angular’s default change detection strategy (Default) checks all components from top to bottom on every browser event.
    • ChangeDetectionStrategy.OnPush: This is the primary optimization technique. When a component uses OnPush, Angular only checks it (and its subtree) if:
      1. One of its @Input() properties has changed (specifically, if the reference to the input object/array has changed, not just a mutation within it).
      2. An event originated from the component or one of its children (e.g., button click).
      3. async pipe is used in the template and emits a new value.
      4. ChangeDetectorRef.detectChanges() or ChangeDetectorRef.markForCheck() is explicitly called.
      • Benefit: Significantly reduces the number of checks, improving performance, especially in large component trees.
    • Immutability: To effectively use OnPush, data passed via @Input() should be immutable. Instead of mutating objects/arrays, create new instances when data changes. This ensures Angular detects the reference change.
    • Async Pipe (| async): Using the async pipe with Observables or Promises automatically marks the component for check when a new value is emitted, without needing manual detectChanges(). It also handles subscription/unsubscription, preventing memory leaks.
    • Signals (Angular v17+): Signals offer a more granular and efficient way to manage reactivity and change detection. When a component uses signals, Angular’s change detection can be optimized further, only re-rendering parts of the template that depend on changed signals, potentially moving away from zone.js-based change detection for those specific reactive elements. While still evolving, signals are a significant step towards more fine-grained reactivity and can lead to highly optimized applications.
  • Key Points:
    • OnPush is the go-to strategy for performance.
    • Immutability is key for OnPush to work effectively.
    • async pipe is a best practice for handling asynchronous data.
    • Signals (v17+) represent a future direction for even more precise change detection.
  • Common Mistakes:
    • Using OnPush but mutating input objects/arrays, leading to components not updating.
    • Calling ChangeDetectorRef.detectChanges() indiscriminately, negating OnPush benefits.
    • Not understanding when OnPush actually triggers a check.
  • Follow-up: How does ChangeDetectorRef.markForCheck() differ from ChangeDetectorRef.detectChanges()?

5. When would you use ng-content and ng-template?

  • Q: Explain the purpose of ng-content and ng-template in Angular. Provide use cases for each.
  • A: Both ng-content and ng-template are powerful tools for creating flexible and reusable components, but they serve different purposes.
    • ng-content (Content Projection):
      • Purpose: Used to project content (HTML, other components) from a parent component into a specific slot within a child component’s template. It allows for component composition.
      • Mechanism: The <ng-content></ng-content> tag acts as a placeholder in the child component’s template. When the child component is used, any content placed between its opening and closing tags in the parent’s template will be rendered where ng-content is located.
      • Use Cases:
        • Layout Components: Creating a generic CardComponent that accepts varying content for its header, body, and footer.
        • Wrapper Components: A ModalComponent that displays custom content inside its modal body.
        • Library Components: Providing flexible slots for users to inject their own UI.
      • Selector (select attribute): You can use select="CSS_SELECTOR" with ng-content to project specific pieces of content based on CSS selectors (e.g., select="[header]", select="p"). This is called multi-slot content projection.
    • ng-template:
      • Purpose: A template element that Angular uses to render structural directives (like *ngIf, *ngFor) or for custom template rendering logic. It is not rendered directly into the DOM unless explicitly told to by a directive.
      • Mechanism: It defines a block of HTML that can be instantiated and rendered dynamically. It is often used implicitly by structural directives, which transform *ngIf="condition" into <ng-template [ngIf]="condition">...</ng-template>.
      • Use Cases:
        • Custom Structural Directives: When creating your own *myStructuralDirective, you’ll work with ng-template to define the content to be rendered.
        • Conditional Rendering with ngIf/else:
          <div *ngIf="loggedIn; else guestBlock">
            Welcome, User!
          </div>
          <ng-template #guestBlock>
            Please log in.
          </ng-template>
          
        • ngFor with trackBy: While not directly ng-template, ngFor internally uses ng-template to stamp out items.
        • Dynamic Component Loading: Using ViewContainerRef.createEmbeddedView() to programmatically instantiate a template.
  • Key Points:
    • ng-content is for content projection (passing content into a component).
    • ng-template is for template definition (a blueprint for rendering content conditionally or repeatedly).
    • ng-template itself is never rendered; it’s a structural instruction.
  • Common Mistakes:
    • Confusing the roles of ng-content and ng-template.
    • Trying to directly display ng-template content without a structural directive or programmatic instantiation.
    • Forgetting that ng-content is typically used in the child component’s template to receive content from the parent.
  • Follow-up: How does content projection (ng-content) relate to @ContentChild() and @ContentChildren()?

6. What are Directives in Angular? Differentiate between Component, Structural, and Attribute Directives.

  • Q: What are Directives in Angular? Differentiate between Component, Structural, and Attribute Directives, providing an example for each type.
  • A: Directives are classes that add extra behavior to elements in Angular applications. They allow you to manipulate the DOM, either by changing its structure or by altering the appearance and behavior of existing elements.
    • Types of Directives:
      1. Component Directives: These are directives with a template. As discussed, components are the most common type of directive. They control a specific view on the screen.
        • Example: AppComponent, UserListComponent.
        • Syntax: @Component({ selector: 'app-root', template:

          }) class AppComponent {}
      2. Structural Directives: These directives change the DOM layout by adding, removing, or manipulating elements and their subtrees. They are identified by a leading asterisk (*).
        • Example: *ngIf, *ngFor, *ngSwitchCase.
        • Use Case: Conditionally rendering an element (*ngIf), iterating over a collection (*ngFor).
        • Mechanism: Structural directives typically work with an <ng-template> internally. For instance, *ngIf="condition" is syntactic sugar for <ng-template [ngIf]="condition">...</ng-template>.
      3. Attribute Directives: These directives change the appearance or behavior of an existing DOM element, component, or another directive. They are applied as attributes to HTML elements.
        • Example: ngClass, ngStyle, ngModel.
        • Use Case: Highlighting an element, changing its style, adding tooltip behavior.
        • Mechanism: They interact with the host element using ElementRef and Renderer2 (or directly with HostBinding/HostListener).
  • Key Points:
    • All directives are classes decorated with @Directive(). Components are special directives with @Component().
    • Structural directives manipulate the DOM structure (*).
    • Attribute directives modify element appearance/behavior (no *).
  • Common Mistakes:
    • Confusing *ngIf (structural) with [ngClass] (attribute).
    • Trying to apply structural directive syntax (*) to an attribute directive.
    • Not understanding that structural directives operate on <ng-template>.
  • Follow-up: When would you create a custom attribute directive versus a custom structural directive?

7. How do you create a custom Structural Directive? Provide an example.

  • Q: Describe the process of creating a custom Structural Directive in Angular. Provide a practical example, like an *ngIfRole directive that only renders content if the user has a specific role.

  • A: Creating a custom structural directive involves using TemplateRef and ViewContainerRef to control the rendering of a template.

    • Process:
      1. Define the Directive: Create a class and decorate it with @Directive(), providing a selector.
      2. Inject Dependencies: Inject TemplateRef<any> and ViewContainerRef into the constructor.
        • TemplateRef: Represents the <ng-template> that the structural directive is attached to (the content to be rendered).
        • ViewContainerRef: Represents the container where the template view can be embedded or removed.
      3. Implement Input Property: Define an @Input() property with the same name as the directive’s selector (e.g., ngIfRole). This property will receive the condition for rendering.
      4. Control View: Inside the setter of the input property, use ViewContainerRef.createEmbeddedView(this.templateRef) to render the content or ViewContainerRef.clear() to remove it based on the condition.

    Example: *ngIfRole Directive (Angular v13-v21) This directive will only render the content if the user has one of the specified roles.

    // src/app/directives/if-role.directive.ts
    import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
    import { AuthService } from '../services/auth.service'; // Assume an AuthService exists
    
    @Directive({
      selector: '[ngIfRole]',
      standalone: true // For Angular v14+
    })
    export class IfRoleDirective {
      private hasView = false;
    
      constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef,
        private authService: AuthService // Inject a service to get user roles
      ) {}
    
      @Input() set ngIfRole(allowedRoles: string[]) {
        const userRoles = this.authService.getUserRoles(); // Get current user roles
    
        // Check if the user has any of the allowed roles
        const hasPermission = userRoles.some(role => allowedRoles.includes(role));
    
        if (hasPermission && !this.hasView) {
          this.viewContainer.createEmbeddedView(this.templateRef);
          this.hasView = true;
        } else if (!hasPermission && this.hasView) {
          this.viewContainer.clear();
          this.hasView = false;
        }
      }
    }
    

    Usage:

    <!-- app.component.html -->
    <button *ngIfRole="['admin', 'editor']">Edit Content</button>
    <p *ngIfRole="['admin']">Admin Dashboard Access</p>
    <div *ngIfRole="['user']">Regular User Content</div>
    
  • Key Points:

    • Requires TemplateRef (what to render) and ViewContainerRef (where to render).
    • The input property setter is where the rendering logic typically resides.
    • createEmbeddedView() inserts the template, clear() removes it.
  • Common Mistakes:

    • Forgetting to inject TemplateRef and ViewContainerRef.
    • Not using the input setter to trigger rendering logic.
    • Incorrectly handling the hasView flag, leading to multiple views or no view.
  • Follow-up: How would you handle an else block for a custom structural directive, similar to *ngIf="condition; else otherBlock"?

8. What are Pipes in Angular? Give examples of built-in pipes.

  • Q: What are Pipes in Angular? Describe their purpose and provide at least three examples of commonly used built-in pipes.
  • A: Pipes are simple functions that transform data directly within your Angular templates. They take data as input and transform it into a desired output format, making your templates cleaner and more readable by separating presentation logic from component logic.
    • Purpose:
      • Data Transformation: Format dates, currencies, numbers, strings, etc.
      • Readability: Keep component logic focused on business rules, not presentation.
      • Reusability: Apply the same transformation consistently across multiple templates.
    • Syntax: {{ value | pipeName[:param1[:param2]] }}
    • Examples of Built-in Pipes:
      1. DatePipe: Formats a date value according to locale rules.
        • {{ birthday | date }} // ‘Dec 23, 2025’
        • {{ birthday | date:'shortTime' }} // ‘12:00 PM’
        • {{ birthday | date:'fullDate' }} // ‘Tuesday, December 23, 2025’
      2. CurrencyPipe: Transforms a number to a currency string, formatted according to locale rules.
        • {{ price | currency }} // ‘$1,234.56’ (default locale)
        • {{ price | currency:'EUR':'symbol':'1.2-2' }} // ‘€1,234.56’
      3. DecimalPipe / PercentPipe: Formats a number with decimal points or as a percentage.
        • {{ pi | number:'1.0-2' }} // ‘3.14’ (min 1 integer digit, min 0 max 2 fractional digits)
        • {{ 0.75 | percent }} // ‘75%’
      4. JsonPipe: Converts a JavaScript value to a JSON string. Useful for debugging.
        • {{ user | json }} // {"name": "Alice", "age": 30}
      5. AsyncPipe: Subscribes to an Observable or Promise and returns its latest value. It automatically unsubscribes when the component is destroyed, preventing memory leaks.
        • {{ data$ | async }} (where data$ is an Observable)
  • Key Points:
    • Used for display-only transformations.
    • Enhance template readability.
    • AsyncPipe is crucial for reactive programming in templates.
  • Common Mistakes:
    • Using pipes for complex logic that should reside in the component class.
    • Misunderstanding AsyncPipe’s role in subscription management.
    • Not providing correct format parameters to pipes.
  • Follow-up: What’s the difference between a pipe and a method call in a template? When should you prefer one over the other?

9. Explain Pure vs. Impure Pipes and when to use each.

  • Q: Explain the concepts of Pure and Impure Pipes in Angular. When would you choose one over the other, and what are the performance implications?
  • A: Angular pipes are categorized as either pure or impure, which dictates how Angular’s change detection mechanism processes them.
    • Pure Pipes:
      • Behavior: A pure pipe is executed only when its input value (or any of its arguments) changes. Angular performs a shallow check for primitive inputs and a reference check for object/array inputs.
      • Default: All built-in pipes are pure by default (e.g., DatePipe, CurrencyPipe). Custom pipes are also pure by default.
      • Performance: Highly efficient. Since they only re-execute on input changes, they don’t impact performance during every change detection cycle.
      • Use Case: Ideal for transformations that produce the same output for the same input and do not rely on internal state or external factors (like DatePipe or UpperCasePipe).
    • Impure Pipes:
      • Behavior: An impure pipe is executed during every change detection cycle, regardless of whether its input value has changed.
      • Declaration: To make a custom pipe impure, set pure: false in the @Pipe() decorator: @Pipe({ name: 'myImpurePipe', pure: false }).
      • Performance: Can significantly impact performance if they perform complex calculations, as they run very frequently. Use with caution.
      • Use Case: Necessary when the output depends on mutable data, internal component state, or external factors that Angular’s change detection won’t automatically detect (e.g., a pipe that filters an array based on a component’s internal filter criteria, or a pipe that uses Date.now() to update a “time ago” display). The AsyncPipe is a built-in impure pipe because its output depends on an external Observable’s emission, not just its input reference.
  • Key Points:
    • Pure pipes are default and performant; only run on input reference change.
    • Impure pipes run on every change detection cycle; can be a performance bottleneck.
    • Prefer pure pipes whenever possible.
    • AsyncPipe is a common example of a useful impure pipe.
  • Common Mistakes:
    • Making a pipe impure unnecessarily, leading to performance degradation.
    • Expecting a pure pipe to detect changes within mutable objects/arrays.
    • Using impure pipes for complex filtering/sorting without considering alternatives (e.g., performing filtering in the component and passing the filtered array to a pure pipe).
  • Follow-up: Can you describe a scenario where using an impure pipe would be justified despite its performance implications?

10. How do you create a custom Pipe?

  • Q: Describe the steps to create a custom pipe in Angular. Provide an example of a pipe that truncates a string and adds an ellipsis.

  • A: Creating a custom pipe involves defining a class that implements the PipeTransform interface and decorating it with @Pipe().

    • Steps:
      1. Create a Pipe Class: Define a TypeScript class.
      2. Implement PipeTransform: Implement the PipeTransform interface, which requires a transform() method.
      3. Implement transform() method: This method will contain the logic for your data transformation. It takes the input value as its first argument and any additional parameters as subsequent arguments.
      4. Decorate with @Pipe(): Use the @Pipe() decorator to register your class as a pipe. Provide a name property for the pipe’s selector.
      5. Register the Pipe:
        • For non-standalone components (Angular < v14 or using NgModule): Declare the pipe in the declarations array of an NgModule (e.g., AppModule).
        • For standalone components (Angular v14+): Set standalone: true in the @Pipe() decorator and import it directly into the imports array of the component or another standalone pipe/directive.

    Example: TruncatePipe (Angular v13-v21) This pipe truncates a string to a specified length and adds ‘…’ at the end.

    // src/app/pipes/truncate.pipe.ts
    import { Pipe, PipeTransform } from '@angular/core';
    
    @Pipe({
      name: 'truncate',
      standalone: true // For Angular v14+
    })
    export class TruncatePipe implements PipeTransform {
      transform(value: string, limit: number = 50, ellipsis: string = '...'): string {
        if (!value) {
          return '';
        }
        if (value.length <= limit) {
          return value;
        }
        return value.substring(0, limit) + ellipsis;
      }
    }
    

    Usage:

    <!-- app.component.html -->
    <p>{{ longText | truncate: 10 }}</p> <!-- Output: "This is a..." -->
    <p>{{ anotherText | truncate: 20 : ' READ MORE' }}</p> <!-- Output: "This is another text READ MORE" -->
    
  • Key Points:

    • Must implement PipeTransform.
    • transform method defines the logic.
    • @Pipe() decorator with a unique name.
    • Remember to declare/import the pipe.
  • Common Mistakes:

    • Forgetting to implement PipeTransform or its transform method.
    • Not providing a unique name in the @Pipe() decorator.
    • Incorrectly handling default values for pipe parameters.
  • Follow-up: How would you unit test this TruncatePipe?

11. Discuss common component communication strategies beyond @Input() and @Output().

  • Q: Beyond @Input() and @Output(), what are other common strategies for component communication in Angular, especially for complex or unrelated components?
  • A: While @Input() and @Output() are excellent for parent-child communication, Angular offers several other patterns for more complex scenarios:
    1. Service (Shared Service Pattern):
      • Mechanism: Create an injectable service (using @Injectable({ providedIn: 'root' }) for a singleton instance across the app). Components inject this service and use it to share data or trigger actions via properties, methods, or Observables (e.g., Subject, BehaviorSubject).
      • Use Case: Communication between unrelated components, global state management (e.g., user authentication status, shared data models), cross-component events.
    2. ViewChild / ViewChildren (Parent-to-Child, Direct Access):
      • Mechanism: A parent component can get a reference to a child component or a DOM element in its own view using @ViewChild() (for a single instance) or @ViewChildren() (for a collection). This allows the parent to directly call methods or access properties of the child.
      • Use Case: Calling a specific method on a child component (e.g., childForm.submit(), childModal.open()), accessing a native DOM element.
      • Caution: Breaks encapsulation; use sparingly. Access should typically be done after ngAfterViewInit.
    3. ContentChild / ContentChildren (Parent-to-Child, Projected Content):
      • Mechanism: Similar to ViewChild, but used when a component needs to query content that has been projected into it via ng-content.
      • Use Case: A TabsComponent querying for TabPanelComponent children projected into it to manage their visibility.
      • Caution: Similar to ViewChild, breaks encapsulation; access after ngAfterContentInit.
    4. Router (URL Parameters, Query Parameters, State):
      • Mechanism: Pass data between routed components using URL parameters (/users/:id), query parameters (/search?query=angular), or router state (history.state).
      • Use Case: Navigating to a detail page with an ID, filtering results based on URL parameters, passing small, non-sensitive data during navigation.
    5. State Management Libraries (Ngrx, NgRx Signals, Akita, Elf):
      • Mechanism: For large-scale applications, dedicated state management libraries provide a centralized store for application state. Components dispatch actions to modify the state, and subscribe to state changes.
      • Use Case: Complex, highly reactive applications with many components sharing global state, ensuring predictable state changes, debugging.
      • Modern Trend (2025): NgRx Signals is gaining traction as a more modern, signal-based approach to state management, offering improved performance and developer experience compared to traditional NgRx.
  • Key Points:
    • Choose the simplest method first (Input/Output).
    • Services are great for unrelated components and global state.
    • ViewChild/ContentChild for direct interaction (use with care).
    • Router for navigation-related data.
    • State management libraries for large, complex apps.
  • Common Mistakes:
    • Overusing ViewChild for general communication, leading to tightly coupled components.
    • Creating multiple instances of a service when a singleton is needed (by not providing providedIn: 'root').
    • Not unsubscribing from service Observables, causing memory leaks.
  • Follow-up: When would you consider introducing a state management library like NgRx into an Angular project?

12. How does Angular handle DOM manipulation safely, especially with custom directives?

  • Q: How does Angular handle DOM manipulation safely, especially when creating custom directives? Discuss ElementRef, Renderer2, and DomSanitizer.
  • A: Direct DOM manipulation in web applications can lead to security vulnerabilities (like XSS attacks) and can bypass Angular’s change detection mechanism, leading to unpredictable behavior. Angular provides several abstractions to handle DOM interaction safely and efficiently.
    • ElementRef:
      • Purpose: A wrapper around the host element of a component or directive. It provides direct access to the underlying native DOM element (nativeElement).
      • Safety: While it provides direct access, using nativeElement directly is generally discouraged, especially for modifying the DOM, as it can expose the application to XSS attacks and bypass Angular’s rendering pipeline. It’s primarily used for accessing properties or calling methods on the native element that Renderer2 doesn’t cover (e.g., focusing an input).
    • Renderer2 (Preferred for DOM Manipulation):
      • Purpose: An abstraction layer that allows you to manipulate the DOM in a platform-agnostic and secure way. It’s the recommended way to interact with the DOM within directives and components.
      • Safety: Renderer2 sanitizes values and ensures that DOM modifications are performed in a way that is compatible with different rendering environments (e.g., browser, web worker, server-side rendering). It prevents direct access to nativeElement, forcing you to use its methods for operations like setStyle(), addClass(), appendChild(), listen().
      • Use Case: Creating custom attribute directives to add/remove classes, styles, or attributes dynamically.
    • DomSanitizer:
      • Purpose: Helps protect against Cross-Site Scripting (XSS) attacks by sanitizing untrusted values (HTML, styles, URLs, resources) that might be directly inserted into the DOM.
      • Mechanism: Angular automatically sanitizes most values bound into the DOM. However, if you must bind an untrusted value (e.g., dynamic HTML from an API) that Angular would normally block, you can explicitly tell Angular to trust it using DomSanitizer.bypassSecurityTrustHtml(), bypassSecurityTrustStyle(), etc.
      • Caution: Bypassing sanitization should only be done when you are absolutely certain the source of the value is safe, as it opens up a potential XSS vulnerability.
    • HostBinding and HostListener (Declarative DOM Interaction):
      • Purpose: Decorators that provide a declarative way for a directive to interact with its host element.
      • @HostBinding('property'): Binds a host element property to a directive/component property. (e.g., @HostBinding('class.active') isActive: boolean;).
      • @HostListener('event'): Subscribes to events of the host element. (e.g., @HostListener('click') onClick() { ... }).
      • Safety: These decorators use Renderer2 internally, ensuring safe DOM interaction.
  • Key Points:
    • Avoid direct nativeElement manipulation with ElementRef unless absolutely necessary.
    • Renderer2 is the secure, platform-agnostic way to modify the DOM.
    • DomSanitizer is for explicitly trusting otherwise untrusted content (use with extreme caution).
    • HostBinding/HostListener offer a clean, declarative approach for common host interactions.
  • Common Mistakes:
    • Directly manipulating ElementRef.nativeElement for style or structure changes.
    • Bypassing DomSanitizer without fully understanding the security implications.
    • Not using Renderer2 when programmatic DOM changes are required.
  • Follow-up: Can you describe a scenario where you would absolutely need to use DomSanitizer.bypassSecurityTrustHtml()?

MCQ Section

Choose the best answer for each question.

1. Which lifecycle hook is called only once after Angular initializes the component’s data-bound input properties and sets up the component? A) ngOnChanges B) ngDoCheck C) ngOnInit D) ngAfterViewInit

*   **Correct Answer:** C
*   **Explanation:**
    *   A) `ngOnChanges` is called before `ngOnInit` and whenever input properties change.
    *   B) `ngDoCheck` is called after `ngOnChanges` and `ngOnInit`, and then frequently during change detection.
    *   C) `ngOnInit` is the standard hook for component initialization logic after inputs are set.
    *   D) `ngAfterViewInit` is called after the component's view and child views are initialized.

2. A component uses ChangeDetectionStrategy.OnPush. If an @Input() property user (an object) is mutated (e.g., this.user.name = 'New Name';) instead of being replaced with a new object reference, what will happen? A) The component will automatically detect the change and update its view. B) The component will only update its view if ChangeDetectorRef.detectChanges() is manually called. C) The component will not update its view because OnPush only checks for reference changes. D) An error will be thrown because mutation is not allowed with OnPush.

*   **Correct Answer:** C
*   **Explanation:** `OnPush` change detection relies on *reference equality* for input objects. If an object is mutated without its reference changing, Angular will not detect the change, and the component's view will not update unless manually forced.

3. Which of the following is a Structural Directive in Angular? A) ngClass B) ngStyle C) ngIf D) ngModel

*   **Correct Answer:** C
*   **Explanation:**
    *   A) `ngClass` is an Attribute Directive (modifies element's classes).
    *   B) `ngStyle` is an Attribute Directive (modifies element's styles).
    *   C) `ngIf` is a Structural Directive (adds/removes elements from the DOM).
    *   D) `ngModel` is an Attribute Directive (enables two-way data binding).

4. When creating a custom structural directive, which two services are typically injected to control the rendering of content? A) ElementRef and Renderer2 B) TemplateRef and ViewContainerRef C) ChangeDetectorRef and NgZone D) HttpClient and Router

*   **Correct Answer:** B
*   **Explanation:** `TemplateRef` represents the template to be rendered, and `ViewContainerRef` is where the template will be embedded or removed from the DOM.

5. What is the primary benefit of using the async pipe in Angular templates with Observables? A) It automatically converts an Observable to a Promise. B) It ensures the component’s view is always updated on every change detection cycle. C) It automatically subscribes to the Observable and unsubscribes when the component is destroyed. D) It allows for direct manipulation of the DOM elements that display the Observable’s data.

*   **Correct Answer:** C
*   **Explanation:** The `async` pipe handles the subscription and unsubscription lifecycle of Observables and Promises, preventing memory leaks and simplifying template logic.

6. Which type of pipe is executed during every change detection cycle, regardless of whether its input value has changed? A) Pure Pipe B) Impure Pipe C) Async Pipe (only when it emits) D) Built-in Pipe

*   **Correct Answer:** B
*   **Explanation:** Impure pipes (like `AsyncPipe` internally) are designed to run on every change detection cycle. Pure pipes only run when their input reference changes.

7. In Angular v14 and later, what property is set in the @Component() or @Directive() decorator to indicate that it doesn’t need to be declared in an NgModule? A) moduleless: true B) isolated: true C) standalone: true D) independent: true

*   **Correct Answer:** C
*   **Explanation:** The `standalone: true` flag was introduced in Angular v14 (stable in v15) to enable standalone components, directives, and pipes, simplifying module organization.

Mock Interview Scenario: Refactoring a Product Display for Reusability and Performance

Scenario Setup: You are interviewing for a Senior Frontend Developer position. The interviewer presents you with a legacy Angular (v13) application’s ProductDisplayComponent. This component currently handles displaying product details, managing a “favorite” state, and has some hardcoded rendering logic. Your task is to discuss how you would refactor this component to adhere to modern Angular (v17+) best practices, focusing on reusability, performance, and maintainability using custom directives and pipes where appropriate.

Interviewer: “Alright, we have this ProductDisplayComponent in an older part of our application. It’s quite monolithic. Let’s imagine you’re tasked with modernizing it. Here’s a simplified version of its template and class:”

// product-display.component.ts (Simplified v13 example)
import { Component, Input, OnInit } from '@angular/core';
import { Product } from '../models/product.model'; // Assume Product interface

@Component({
  selector: 'app-product-display',
  template: `
    <div class="product-card">
      <img [src]="product.imageUrl" alt="{{ product.name }}">
      <h2>{{ product.name }}</h2>
      <p class="price">{{ product.price | currency }}</p>
      <p *ngIf="product.description && product.description.length > 100" class="description">
        {{ product.description.substring(0, 100) }}...
        <a href="#" (click)="showFullDescription = !showFullDescription">
          {{ showFullDescription ? 'Show Less' : 'Show More' }}
        </a>
      </p>
      <p *ngIf="product.description && product.description.length <= 100" class="description">
        {{ product.description }}
      </p>

      <button (click)="toggleFavorite()">
        {{ isFavorite ? 'Remove from Favorites' : 'Add to Favorites' }}
      </button>

      <div *ngIf="product.availableStock < 5" class="stock-warning">
        Low Stock! Only {{ product.availableStock }} left.
      </div>

      <div *ngIf="!product.availableStock || product.availableStock === 0" class="out-of-stock">
        Out of Stock
      </div>
    </div>
  `,
  styles: [`
    .product-card { border: 1px solid #ccc; padding: 15px; margin: 10px; }
    .price { font-weight: bold; color: green; }
    .stock-warning { color: orange; }
    .out-of-stock { color: red; font-weight: bold; }
  `]
})
export class ProductDisplayComponent implements OnInit {
  @Input() product: Product;
  isFavorite: boolean = false;
  showFullDescription: boolean = false;

  constructor(/* private favoriteService: FavoriteService */) {} // Assume FavoriteService

  ngOnInit() {
    // this.isFavorite = this.favoriteService.checkFavorite(this.product.id);
  }

  toggleFavorite() {
    this.isFavorite = !this.isFavorite;
    // this.favoriteService.updateFavorite(this.product.id, this.isFavorite);
  }
}

Interviewer Questions & Expected Flow:

  1. Initial Assessment & Strategy:

    • Interviewer: “Looking at this component, what are your initial thoughts on areas for improvement in terms of reusability, maintainability, and performance, considering modern Angular (v17+)?”
    • Expected Answer:
      • Decomposition: Too much logic and UI in one component. Break it down into smaller, focused components (e.g., ProductImageComponent, ProductPriceComponent, FavoriteButtonComponent, StockStatusComponent).
      • Change Detection: Default change detection is likely inefficient. Suggest ChangeDetectionStrategy.OnPush for ProductDisplayComponent and its children.
      • Pipes: The description truncation and currency formatting are good candidates for pipes.
      • Directives: Stock status logic (*ngIf conditions) can be abstracted into a custom attribute or structural directive.
      • Standalone Components (v14+): Mention converting to standalone for better modularity and tree-shaking.
      • Signals (v17+): For the isFavorite state, consider using signals for more granular reactivity.
  2. Custom Pipe Implementation:

    • Interviewer: “You mentioned the description truncation. How would you implement that as a custom pipe, and why is that a better approach than the current substring logic in the template?”
    • Expected Answer:
      • Why Pipe: Separates presentation logic from component logic, reusability across other components, cleaner template.
      • Pipe Definition: Create TruncatePipe (as shown in Q10 above) implementing PipeTransform, with value, limit, and ellipsis parameters.
      • Usage: {{ product.description | truncate:100 }}
      • Pure Pipe: Emphasize that it should be a pure pipe as its output only depends on its input string and parameters.
  3. Custom Directive for Stock Status:

    • Interviewer: “The stock warning logic (*ngIf="product.availableStock < 5") is repetitive and tightly coupled. How would you abstract this into a custom directive to make it more reusable and declarative?”
    • Expected Answer:
      • Directive Type: Suggest an Attribute Directive ([appStockStatus]) if we want to add classes/styles to an existing element, or a Structural Directive (*appStockStatus) if we want to conditionally render the entire stock message. A structural directive like *appStockStatus="product.availableStock" seems more flexible here.
      • Structural Directive Design:
        • Selector: *appStockStatus
        • @Input() appStockStatus: number;
        • Inject TemplateRef and ViewContainerRef.
        • Logic in ngOnChanges or input setter:
          • If availableStock is 0, render ‘Out of Stock’ template.
          • If availableStock is < 5, render ‘Low Stock’ template.
          • Else, clear the view.
        • Alternative (Attribute Directive): If the requirement was to style the text, an attribute directive could bind classes: [appStockStatus]="product.availableStock".
      • Example (Conceptual Structural):
        <!-- In ProductDisplayComponent's refactored template -->
        <ng-container *appStockStatus="product.availableStock">
            <!-- Content for low/out of stock will be projected here -->
        </ng-container>
        
        (The directive itself would manage which specific template to render based on stock, or it could be simpler and just render if stock is low/out.)
      • Example (Attribute Directive for styling):
        // stock-status.directive.ts
        @Directive({ selector: '[appStockStatus]', standalone: true })
        export class StockStatusDirective {
          constructor(private el: ElementRef, private renderer: Renderer2) {}
          @Input() set appStockStatus(stock: number) {
            this.renderer.removeClass(this.el.nativeElement, 'low-stock');
            this.renderer.removeClass(this.el.nativeElement, 'out-of-stock');
            if (stock === 0) {
              this.renderer.addClass(this.el.nativeElement, 'out-of-stock');
            } else if (stock < 5) {
              this.renderer.addClass(this.el.nativeElement, 'low-stock');
            }
          }
        }
        
        <!-- Usage -->
        <p [appStockStatus]="product.availableStock">Stock: {{ product.availableStock }}</p>
        
  4. Component Decomposition & Communication:

    • Interviewer: “Let’s focus on the FavoriteButtonComponent. How would you extract this into its own component, and what would be the communication pattern between this new component and the ProductDisplayComponent?”
    • Expected Answer:
      • New Component: Create FavoriteButtonComponent (standalone).
      • @Input(): It would need to receive the isFavorite state and potentially the productId from ProductDisplayComponent.
        // favorite-button.component.ts
        @Input() isFavorite: boolean = false;
        @Input() productId: string; // Or Product ID
        
      • @Output(): It would emit an event when the favorite status changes.
        // favorite-button.component.ts
        @Output() favoriteToggled = new EventEmitter<boolean>();
        // ... (click)="favoriteToggled.emit(!isFavorite)"
        
      • Usage in ProductDisplayComponent:
        <app-favorite-button
          [isFavorite]="isFavorite"
          [productId]="product.id"
          (favoriteToggled)="onFavoriteToggled($event)">
        </app-favorite-button>
        
      • Service: Mention that FavoriteService would ideally be injected into FavoriteButtonComponent to handle the actual API calls, separating concerns.
  5. Performance Considerations (Change Detection):

    • Interviewer: “Given these changes, how would you ensure optimal change detection performance for the ProductDisplayComponent and its children?”
    • Expected Answer:
      • ChangeDetectionStrategy.OnPush: Apply ChangeDetectionStrategy.OnPush to ProductDisplayComponent and all its newly created child components (e.g., FavoriteButtonComponent).
      • Immutability: Emphasize that product input should be treated immutably. If product details change, a new Product object reference should be passed down.
      • Async Pipe: If product data comes from an Observable, use the async pipe in the template to automatically handle subscriptions and trigger change detection.
      • Signals (v17+): For the isFavorite state, if converted to a signal (isFavorite = signal(false)), updates to this signal would directly trigger re-renders only where isFavorite() is used, providing highly optimized change detection for that specific piece of state.

Red Flags to Avoid:

  • Suggesting direct DOM manipulation with ElementRef.nativeElement.
  • Not considering OnPush for performance.
  • Creating an overly complex solution when a simpler pipe or directive would suffice.
  • Not discussing how components communicate after decomposition.
  • Ignoring memory leak possibilities (e.g., not unsubscribing from Observables if not using async pipe).

Practical Tips

  1. Hands-on Practice: The best way to understand Components, Directives, and Pipes is to build them. Create small, isolated Angular projects to experiment with custom implementations. Try building:
    • A custom *ngIfAuth structural directive.
    • A HighlightDirective attribute directive.
    • A TimeAgoPipe or a SearchFilterPipe.
  2. Understand the “Why”: Don’t just memorize definitions. Understand why a particular pattern (e.g., OnPush, services for communication) is recommended. This helps you apply concepts to novel problems.
  3. Read Official Documentation: The Angular documentation is the most authoritative and up-to-date resource. Pay special attention to the “Fundamentals” and “Techniques” sections for components, directives, and pipes.
  4. Explore Source Code (Optional but Recommended): For advanced learners, looking at the source code of Angular’s built-in directives (e.g., ngIf, ngFor) can provide deep insights into how they leverage TemplateRef and ViewContainerRef.
  5. Focus on Reusability and Modularity: When designing components, directives, or pipes, always think about how they can be made generic and reusable across different parts of an application or even in other projects.
  6. Performance Awareness: Always consider the performance implications, especially for pipes (pure vs. impure) and change detection (OnPush strategy). Explain your choices when asked.
  7. Stay Updated: Angular evolves rapidly. Regularly check for new features and best practices (e.g., standalone components, signals, functional interceptors, etc.) on the official blog and reputable community resources.

Summary

This chapter has provided a deep dive into the core Angular concepts of Components, Directives, and Pipes, essential for any Angular developer. We’ve covered their fundamental definitions, lifecycle management, communication patterns, and practical applications, including the creation of custom solutions. Understanding the nuances of ChangeDetectionStrategy.OnPush, the differences between structural and attribute directives, and the power of pure vs. impure pipes is critical for building high-performance, maintainable Angular applications.

As you continue your interview preparation, remember that theoretical knowledge combined with practical implementation experience is key. Practice explaining these concepts clearly, providing real-world examples, and discussing trade-offs. The mock interview scenario highlighted how these concepts are tested in a practical context, emphasizing problem-solving and architectural thinking.

Your next step should be to continue practicing, perhaps by implementing some of the custom directives and pipes discussed, and then moving on to understanding Angular Services, Dependency Injection, and Routing, which will be covered in the next chapter.


This interview preparation guide is AI-assisted and reviewed. It references official documentation and recognized interview preparation resources.