AcademyAngular
Compound Components and Dynamic Outlets
A compound component's parts coordinate through a shared, injected service instead of local state; NgComponentOutlet renders whichever component a mapping picks at runtime.
2 min readUpdated 3 exercises
A compound component coordinates its parts through DI, not local booleans
A tab group's individual <app-tab> elements have no way to know about each other directly — each one only knows its own state, unless something ties them together. The pattern that scales is a shared service, scoped to the parent's providers, that every child tab injects:
@Injectable()
export class TabsService {
private activeId = signal<string | null>(null);
activate(id: string): void {
this.activeId.set(id);
}
isActive(id: string): boolean {
return this.activeId() === id;
}
}Each <app-tab> injects TabsService and calls .activate(this.id) on click, then reads .isActive(this.id) for its own styling. A tab group where "two tabs can end up active at once" is what happens without that shared coordinator — each tab tracking its own isActive boolean has no way to deactivate a sibling, because nothing connects them; the service is what makes "only one active at a time" an invariant the group actually enforces, instead of a hope resting on every tab behaving.
NgComponentOutlet renders whichever component type a mapping resolves to
Rendering "the icon for this status" — a different concrete component per case — does not need a giant @switch in the template if the mapping is a plain function returning a component type:
iconFor(status: Status): Type<unknown> {
return status === 'success' ? CheckIcon : status === 'error' ? ErrorIcon : SpinnerIcon;
}<ng-container *ngComponentOutlet="iconFor(status)" />A status icon that "never changes" is frequently a template that hardcoded one component directly — <app-check-icon /> — instead of routing through ngComponentOutlet and the mapping function, so no matter what status becomes, the markup was never wired to ask the mapping in the first place.
What interviewers check
Whether you reach for a shared, injected service the moment sibling components need to coordinate rather than duplicating local state across them, and whether NgComponentOutlet is a pattern you recognize for "which component, decided at runtime" instead of reaching for an oversized conditional block in the template.
Now practice it
1 quiz · 2 drillsReading this page is the cheap half. These are the exercises that make you use it.
- A DI-powered Compound ComponentProFix a tab group where two tabs can end up "active" at once, because nothing coordinates their local state.DrillSolve · HardDrillSolve · Hard
- Dynamic Components with NgComponentOutletProFix a status icon that never changes, because the template hardcodes one component instead of rendering the one a mapping picks.DrillSolve · HardDrillSolve · Hard
- Router · Functional GuardsFunctional guard questions at the level where runtime-decided rendering and navigation start to overlap.QuizTake · HardQuizTake · Hard
Read next
OnPush Change Detection and model()
OnPush skips a subtree until an @Input() reference actually changes; model() gives a component a two-way custom binding without a separate @Output() wiring.
2 min
Interpolation and Property Binding
{{ }} renders text; [prop] sets a DOM property directly. The same distinction as textContent vs. a property assignment, and it decides which one a template needs.
2 min