In an Angular component, how do <ng-content> and its select attribute give a card separate header, body and footer slots?
answer
- a placeholder, not an element
- component-selector syntax in select
- first matching slot wins
- the bare ng-content catches the rest
- only the host's direct children
basics
~10 sEach <ng-content select="..."> is a compile-time placeholder that receives the host's direct children matching its selector; an <ng-content> without select takes everything unmatched, and without one, unmatched content is not rendered at all.
solid answer
~40 s`<ng-content>` marks where content a parent writes between the component's tags is rendered. Adding `select` turns it into a named slot using the same selector syntax as component selectors: an element name (`card-header`), an attribute (`[card-footer]`, or an exact value like `[slot="footer"]`), a class (`.title`), `:not()` and comma-separated lists, but no combinators. Angular distributes each **direct child** of the host to the first slot, in template order, whose selector matches; an `<ng-content>` without `select` receives everything that matched nothing. If there is no such catch-all, unmatched nodes are not rendered. `<ng-content>` is processed by the compiler: you cannot bind to it, add directives or classes to it, or create it at runtime.
code
ts · 16 linesimport {Component} from '@angular/core';
@Component({
selector: 'app-card',
template: `
<ng-content select="[card-header]" />
<ng-content select="[card-footer]" />
`,
})
export class Card {}
// <app-card>
// <h2 card-header>Title</h2>
// <p>Body text</p> <!-- no catch-all slot: not rendered -->
// <div><button card-footer>OK</button></div> <!-- div is the distributed node: not rendered -->
// </app-card>go deeper
Remember that select picks content by selector, the bare ng-content catches the rest, and nothing unmatched is shown without it.
Explain first-match-wins, direct-children-only distribution and the selector subset that select supports.
Design slot markers consumers cannot misuse, choosing between attribute markers and imported marker directives.
Define a consistent slot vocabulary across a component library so every container component projects the same way.
## Content versus view In Angular, the nodes a parent writes between a component's opening and closing tags are the component's **content**; the nodes in the component's own template are its **view**. `<ng-content>` is the placeholder in the view that says where content is rendered, or **projected**. `<ng-content>` is neither a component nor a DOM element. The compiler processes it at build time, so you cannot add directives, bindings, classes or styles to it, and you cannot insert or remove it at runtime. In the rendered DOM it leaves nothing behind except the projected nodes. ## Named slots with select A card with three regions declares one placeholder per region: ```ts import {Component} from '@angular/core'; @Component({ selector: 'app-card', template: ` <header><ng-content select="card-header, [card-header]" /></header> <section><ng-content /></section> <footer><ng-content select="[card-footer]" /></footer> `, }) export class Card {} ``` A consumer writes: ```html <app-card> <h2 card-header>Invoice #42</h2> <p>Total due: 120 EUR</p> <button card-footer>Pay</button> </app-card> ``` The `h2` lands in the header, the `button` in the footer, and the paragraph, which matches neither selector, in the bare `<ng-content />` in the section. ## What select accepts `select` supports the same limited selector syntax as component selectors: | Selector kind | Example | Matches | |---|---|---| | Element | `card-header` | `<card-header>` elements | | Attribute | `[card-footer]` | any element with that attribute | | Attribute with exact value | `[slot="footer"]` | only that value; no other operators | | Class | `.card-title` | elements with that class | | Negation | `[card-footer]:not(button)` | the attribute, except on buttons | | List | `card-header, [card-header]` | either form | Combinators such as descendant (`a b`) or child (`a > b`) are not supported. ## How nodes are distributed 1. Angular looks only at the **direct children** of the host element in the consumer's template. A `card-footer` button nested inside a `<div>` is not matched on its own; the `<div>` is the node that gets distributed. 2. Each direct child is compared with the `select` values in the order the `<ng-content>` elements appear in the template. The **first** matching slot captures it. 3. A node that matches no selector goes to the `<ng-content>` without `select`. If there are several, the last one is the catch-all. 4. If the template has no `<ng-content>` without `select`, unmatched nodes are **not rendered**. 5. Text nodes, including stray text between elements, match no selector, so they also go to the catch-all. ## Attribute slots versus wrapper components - **Attribute markers** (`[card-footer]`) let consumers use any element, which keeps the card flexible and needs no extra imports. - **Marker components or directives** (`<card-header>`) give a typo-proof API when declared as real components or directives the consumer imports, and they can carry their own styles or inputs. An undeclared custom element name would otherwise be reported as an unknown element. - Class selectors are the least robust: a consumer adding the class for styling reasons can accidentally move content between slots. ## Reading the rendered DOM Projection leaves no wrapper of its own. For the invoice example, the browser sees `<app-card>` containing the card's `<header>`, `<section>` and `<footer>`, with the consumer's `h2`, `p` and `button` moved inside them. There is no `<ng-content>` element in the output and no extra slot element, unlike the web platform's `<slot>` under shadow DOM. That matters for CSS: a selector such as `header > h2` written in global styles will match, because the `h2` really is a child of the `header` in the DOM, even though it was declared in the consumer's template. ## Rules that surprise people - `select` is static. The value cannot be bound, and the distribution is decided when the host's content is created. - Projected nodes still belong to the consumer's template: their bindings are evaluated in the consumer's context, and styles from the card's emulated encapsulation do not apply to them unless written for projected content. - A node can be projected only once; the placeholder moves nodes, it does not copy them. Those last two points are the start of the deeper questions about ownership and conditional projection.
- What happens when one element matches two select values, say [card-header] and h2?It goes to the first `<ng-content>` in template order whose selector matches, and nowhere else. A node is projected into exactly one slot, so ordering the placeholders is how you break ties.
- Why can you not write <ng-content [select]="slotName">?`<ng-content>` is resolved by the compiler, and the slot selectors are fixed at build time. It accepts no bindings, directives or classes; any variation has to be expressed as separate static placeholders or through a template passed as an input.
The card's template is a mail sorting rack with labelled pigeonholes. Each letter from the parent goes into the first pigeonhole whose label matches; letters with no matching label go into the unlabelled tray, and if there is no such tray they are simply not delivered.
saying these in an interview costs you the question
- select accepts any CSS selector, including descendant combinators
- Unmatched content is always appended at the end of the component
- A node matching two slots is rendered in both
- You can bind select or toggle ng-content with a class binding
- Nested elements are matched individually, not through their top-level parent