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:
- A
@switchdirective: This directive will wrap the entire switch block and will hold the value to be matched against. - A
@casedirective: This directive will be applied to individual template blocks. Each@casewill specify a value to match against the@switchvalue. If the values match, the content within the@casewill be rendered. - A
@defaultdirective: This directive will be applied to a template block that should be rendered if no@casevalue matches the@switchvalue.
Key requirements:
- Type Safety: The directives should ideally leverage Angular's type checking to ensure that the values provided to
@caseare compatible with the type of the value provided to@switch. - Declarative Syntax: The directive usage should be intuitive and closely resemble a traditional
switchstatement. - Single Match: Only the first matching
@caseor the@defaultshould be rendered. Subsequent matching@casedirectives should be ignored. - Efficiency: The directive should not introduce significant performance overhead. Unmatched
@caseblocks should be detached from the DOM efficiently. - Reactivity: The directives should react to changes in the
@switchvalue. If the@switchvalue changes, the appropriate@caseor@defaultshould be rendered.
Expected behavior:
When a component using these directives is rendered:
- The
@switchdirective will receive an expression whose value will be the target for matching. - Each
@casedirective will receive a specific value. - If the
@switchvalue strictly equals (===) any of the@casevalues, the content of the first matching@casewill be rendered. - If no
@casevalue matches the@switchvalue, the content of the@defaultdirective (if present) will be rendered. - If the
@switchvalue changes at runtime, the previously rendered content will be removed, and the new matching content will be rendered.
Important edge cases to consider:
- No
@casedirectives: The@defaultdirective should be rendered if present. - Multiple matching
@casedirectives: Only the first one in the DOM order should be rendered. nullorundefinedvalues: The directives should handle these values correctly in comparisons.- No
@defaultdirective: If no@casematches and no@defaultis 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
switchlogic. - The
*appCasedirective should accept a literal value or a variable expression. - The
*appSwitchdirective should accept a variable expression.
Notes
- Consider using
TemplateRefandViewContainerRefto 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
*appDefaultdirective 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
@switchexpression 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-containerfor wrapping template content, which is a common practice for structural directives that don't render their own DOM element.