Hone logo
Hone
Problems

Implementing a Type-Safe @switch Directive in Angular

Angular's template syntax is powerful and expressive. However, sometimes we encounter scenarios where we need to conditionally render content based on multiple specific values of a variable. While *ngIf is excellent for boolean conditions and *ngSwitch exists, a more declarative and type-safe approach inspired by other languages' switch statements could enhance readability and maintainability. This challenge asks you to implement a custom directive that mimics the behavior of a switch statement within Angular templates.

Problem Description

Your task is to create a set of custom Angular directives that, when combined, provide a switch statement-like structure in your templates. This structure should allow you to conditionally render different template content based on the value of a given expression.

What needs to be achieved:

You need to create three directives:

  1. A @switch directive: This directive will wrap the entire switch block and will hold the value to be matched against.
  2. A @case directive: This directive will be applied to individual template blocks. Each @case will specify a value to match against the @switch value. If the values match, the content within the @case will be rendered.
  3. A @default directive: This directive will be applied to a template block that should be rendered if no @case value matches the @switch value.

Key requirements:

  • Type Safety: The directives should ideally leverage Angular's type checking to ensure that the values provided to @case are compatible with the type of the value provided to @switch.
  • Declarative Syntax: The directive usage should be intuitive and closely resemble a traditional switch statement.
  • Single Match: Only the first matching @case or the @default should be rendered. Subsequent matching @case directives should be ignored.
  • Efficiency: The directive should not introduce significant performance overhead. Unmatched @case blocks should be detached from the DOM efficiently.
  • Reactivity: The directives should react to changes in the @switch value. If the @switch value changes, the appropriate @case or @default should be rendered.

Expected behavior:

When a component using these directives is rendered:

  • The @switch directive will receive an expression whose value will be the target for matching.
  • Each @case directive will receive a specific value.
  • If the @switch value strictly equals (===) any of the @case values, the content of the first matching @case will be rendered.
  • If no @case value matches the @switch value, the content of the @default directive (if present) will be rendered.
  • If the @switch value changes at runtime, the previously rendered content will be removed, and the new matching content will be rendered.

Important edge cases to consider:

  • No @case directives: The @default directive should be rendered if present.
  • Multiple matching @case directives: Only the first one in the DOM order should be rendered.
  • null or undefined values: The directives should handle these values correctly in comparisons.
  • No @default directive: If no @case matches and no @default is provided, nothing should be rendered.

Examples

Example 1: Consider a component with a userStatus property.

// component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-user-profile',
  template: `
    <div *appSwitch="userStatus">
      <ng-container *appCase="'active'">
        <p>User is active and can log in.</p>
      </ng-container>
      <ng-container *appCase="'pending'">
        <p>User is pending approval.</p>
      </ng-container>
      <ng-container *appDefault>
        <p>User status is unknown or inactive.</p>
      </ng-container>
    </div>
  `
})
export class UserProfileComponent {
  userStatus: 'active' | 'pending' | 'inactive' = 'active';
}

Input: userStatus = 'active' Output:

<div>
  <p>User is active and can log in.</p>
</div>

Explanation: The userStatus ('active') matches the value provided to the first *appCase directive. Therefore, its content is rendered.

Example 2:

// component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-dashboard',
  template: `
    <div *appSwitch="notificationType">
      <ng-container *appCase="'info'">
        <p>Information message received.</p>
      </ng-container>
      <ng-container *appCase="'warning'">
        <p>Warning: Please review this notification.</p>
      </ng-container>
      <ng-container *appCase="'error'">
        <p>Error occurred! Action required.</p>
      </ng-container>
    </div>
  `
})
export class DashboardComponent {
  notificationType: 'info' | 'warning' | 'error' | 'success' = 'warning';
}

Input: notificationType = 'warning' Output:

<div>
  <p>Warning: Please review this notification.</p>
</div>

Explanation: The notificationType ('warning') matches the value of the second *appCase directive. Its content is rendered.

Example 3: (Edge Case - No Match, with Default)

// component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-settings',
  template: `
    <div *appSwitch="theme">
      <ng-container *appCase="'dark'">
        <p>Dark theme applied.</p>
      </ng-container>
      <ng-container *appDefault>
        <p>Default theme applied.</p>
      </ng-container>
    </div>
  `
})
export class SettingsComponent {
  theme: 'light' | 'dark' = 'light';
}

Input: theme = 'light' Output:

<div>
  <p>Default theme applied.</p>
</div>

Explanation: The theme ('light') does not match the *appCase value ('dark'). Since a *appDefault directive is present, its content is rendered.

Example 4: (Edge Case - No Match, No Default)

// component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-items',
  template: `
    <ul *appSwitch="itemType">
      <li *appCase="'book'">Book details</li>
      <li *appCase="'movie'">Movie details</li>
    </ul>
  `
})
export class ItemsComponent {
  itemType: 'book' | 'movie' | 'game' = 'game';
}

Input: itemType = 'game' Output:

<ul></ul>

Explanation: The itemType ('game') does not match any of the *appCase values. Since no *appDefault directive is present, nothing is rendered within the <ul>.

Constraints

  • The solution must be implemented using TypeScript and Angular.
  • The directives should be designed to be reusable across different components.
  • The solution should avoid relying on third-party libraries for the core switch logic.
  • The *appCase directive should accept a literal value or a variable expression.
  • The *appSwitch directive should accept a variable expression.

Notes

  • Consider using TemplateRef and ViewContainerRef to manage the rendering of template content for each case.
  • Think about how to efficiently detach and attach views when the switch value changes.
  • The comparison between the switch value and the case value should be a strict equality check (===).
  • The *appDefault directive does not need to accept any input values; its presence signifies the default behavior.
  • For type safety, Angular's compiler will implicitly provide some benefits if you structure your component and directive types correctly. For example, if the @switch expression has a union type, and you try to use a case value that is not part of that union, you might get a compile-time warning if you pass a literal value.
  • The directives should be designed to work with ng-container for wrapping template content, which is a common practice for structural directives that don't render their own DOM element.
Loading editor...
typescript