Module Overview
Estimated Time: 3-4 hours | Difficulty: Intermediate | Prerequisites: Module 2
- Structural directives (*ngIf, *ngFor, *ngSwitch)
- Modern control flow syntax (@if, @for, @switch)
- Attribute directives (ngClass, ngStyle)
- Creating custom directives
- Built-in pipes and custom pipe creation
- Pure vs impure pipes
Structural Directives
Structural directives change the DOM layout by adding, removing, or manipulating elements. They are called “structural” because they alter the structure of the DOM tree itself — not just styling or attributes, but whether elements exist at all.Legacy vs Modern syntax: Angular 17 introduced the
@if, @for, and @switch control flow syntax as a replacement for *ngIf, *ngFor, and *ngSwitch. The new syntax is recommended for all new projects — it is more readable, performs better (the @for block is up to 90% faster than *ngFor in benchmarks), and does not require importing directives. This module shows both so you can work with existing codebases.*ngIf (Legacy) vs @if (Modern)
*ngFor (Legacy) vs @for (Modern)
*ngSwitch vs @switch
Attribute Directives
Attribute directives change the appearance or behavior of an element.ngClass
ngStyle
Custom Directives
Attribute Directive
Structural Directive
Using HostBinding and HostListener
Pipes
Pipes transform data for display in templates.Built-in Pipes
Chaining Pipes
Custom Pipe
Filter Pipe
Pure vs Impure Pipes
Deferrable Views (@defer)
Angular 17+ introduces deferrable views for lazy loading content. This is one of Angular’s most powerful performance features — it lets you tell the framework “do not even download the JavaScript for this component until a specific trigger occurs.” The component code stays out of your initial bundle entirely, which can dramatically reduce your Time to Interactive on first page load. Common real-world uses: heavy charting libraries that only load when scrolled into view, comment sections that load on click, and preview cards that load on hover.Practice Exercise
Exercise: Create a Directive and Pipe
- Create a
DebounceClickDirectivethat prevents rapid clicks - Create a
TruncatePipethat shortens text with ellipsis
- Directive should have configurable delay (default 300ms)
- Pipe should accept max length parameter
Solution
Solution
Summary
1
Structural Directives
Use @if, @for, @switch (modern) or *ngIf, *ngFor, *ngSwitch (legacy) to control DOM structure
2
Attribute Directives
ngClass and ngStyle for dynamic styling, create custom with @Directive
3
Pipes
Transform data for display with built-in or custom pipes
4
@defer
Lazy load content with viewport, interaction, or timer triggers
Interview Deep-Dive
Q: Explain the difference between pure and impure pipes. In a list of 500 items, what is the performance impact of using an impure filter pipe versus a pure one?
Q: Explain the difference between pure and impure pipes. In a list of 500 items, what is the performance impact of using an impure filter pipe versus a pure one?
Strong Answer: A pure pipe only re-executes its transform method when the input value reference changes. An impure pipe re-executes on every single change detection cycle, regardless of whether its input changed. This distinction has massive performance implications.With 500 items and an impure filter pipe, the pipe’s transform method runs on every change detection cycle — which could be dozens of times per second during user interaction (typing, scrolling, mouse movement). If each invocation iterates 500 items, you are doing thousands of array iterations per second for no reason. I have seen this cause visible jank in production.A pure pipe with the same 500 items only re-runs when you create a new array reference. So if you use immutable patterns (filter returns new array, spread operator for updates), the pipe runs once per actual data change. The trick is understanding that pushing to an existing array does NOT trigger a pure pipe because the array reference is the same. You must do this.items = […this.items, newItem] instead of this.items.push(newItem).The practical rule: always use pure pipes. If you think you need an impure pipe, you almost certainly need to fix your data flow to use immutable updates instead.Follow-up: How does the new @for block with track affect this equation?
Answer: The @for block’s track expression tells Angular which items changed so it can update only those DOM elements. Combined with a pure pipe that returns a new array, Angular’s diffing algorithm sees exactly which items were added, removed, or moved. Without track, Angular destroys and recreates the entire list on every change. Track does not affect how often the pipe runs — it affects how efficiently Angular updates the DOM after the pipe produces a new array.
Q: You need to create a structural directive that renders content only if the user has a specific permission. Walk me through the implementation and how structural directives work under the hood.
Q: You need to create a structural directive that renders content only if the user has a specific permission. Walk me through the implementation and how structural directives work under the hood.
Strong Answer: Structural directives manipulate the DOM by adding or removing elements. Under the hood, Angular transforms the asterisk syntax into an ng-template. So *appIfPermission=“‘admin’” becomes <ng-template [appIfPermission]=“‘admin’”>…content…</ng-template>. The directive receives a TemplateRef (the template to render) and a ViewContainerRef (where to render it) via dependency injection.For the permission directive, I would inject the AuthService to check the user’s permissions, create or clear the embedded view based on the check, and subscribe to permission changes for dynamic updates. The key implementation detail: you need to track whether the view is currently created (a boolean flag) to avoid creating duplicate views or clearing an already-empty container.Here is the mental model: TemplateRef is the blueprint, ViewContainerRef is the construction site. createEmbeddedView builds the blueprint at the site, and clear demolishes whatever is there. You can even create the same template multiple times (which is how *ngFor works internally — one template, multiple embedded views).Follow-up: How would you handle the case where permissions change after the directive initializes — for example, the user’s role is upgraded while they are on the page?
Answer: I would use an @Input setter that re-evaluates whenever the permission string changes, combined with a subscription to the AuthService’s user observable. When either the required permission or the user’s actual permissions change, I re-evaluate and either create or clear the view. I would use takeUntilDestroyed to clean up the subscription. This makes the directive fully reactive — it responds to both input changes and runtime permission changes.
Q: Explain @defer blocks. How do they differ from lazy-loaded routes, and when would you use one versus the other?
Q: Explain @defer blocks. How do they differ from lazy-loaded routes, and when would you use one versus the other?
Strong Answer: @defer blocks and lazy-loaded routes both defer JavaScript loading, but they operate at different granularities and with different triggers. Lazy-loaded routes defer entire feature modules until the user navigates to that route. @defer blocks defer individual components within a page based on triggers like viewport visibility, user interaction, idle time, or custom conditions.The key difference: lazy routes are navigation-driven, @defer is rendering-driven. A lazy route only loads when the URL changes. A @defer block can load when a component scrolls into view (on viewport), when the user hovers over a placeholder (on hover), or when the browser is idle (on idle). This is much more granular.I use lazy routes for feature-level code splitting — the admin section, the settings page, the checkout flow. I use @defer for component-level optimization within a page — a heavy chart library that only loads when scrolled into view, a comment section that loads on click, or a rich text editor that loads when the user starts interacting with the form. The two are complementary, not competing. A lazy-loaded route page can itself contain @defer blocks for its heavy components.Follow-up: What happens if a @defer block fails to load — say the network drops?
Answer: That is what the @error block is for. You can define @defer, @loading, @placeholder, and @error blocks. @placeholder shows before loading starts, @loading shows during the network fetch (with an optional minimum display time to prevent flickering), and @error shows if the chunk fails to load. You can also combine prefetching with trigger — for example, prefetch on idle but trigger on viewport, so the code is likely already cached by the time the user scrolls to it.
Next Steps
Next: Services & Dependency Injection
Learn about injectable services and Angular’s powerful DI system