Angular NotesRohit’s interview study guide
Chapter 02

Components

What a component really is — decorator, selector, host element, class instance vs view, and how parents and children fit together.

8 topics
Parts marked Advanced are extra depth. Skip them on a first read or a quick revision.

#The @Component decorator

A component is an ordinary TypeScript class with @Component({...}) on top. The decorator is how you hand Angular the information a plain class doesn't have: which HTML element it controls (selector), what it renders (template), what that template uses (imports), and how it's styled (styles). The class holds the state and behaviour; the decorator's object — the metadata — tells Angular how to turn that class into something on screen.

@Component is a TypeScript decorator: a function that is applied to the class when the class is defined (classes themselves are covered on the TypeScript site). But Angular's decorators are unusual. You might expect the decorator to run in the browser and register the class somewhere. It doesn't. The Angular compiler reads the decorator at build time, compiles the template, and replaces the decorator with static fields on the class. By the time the code reaches the browser, the decorator is effectively gone.

A mental model: the decorator is a build order form stapled to the class. The compiler reads the form, builds the parts (a factory that creates instances, and compiled template instructions), bolts them onto the class, and throws the form away.

user-card.ts
import { Component, input } from '@angular/core';
import { UpperCasePipe } from '@angular/common';

@Component({
  selector: 'app-user-card',
  imports: [UpperCasePipe],
  template: `
    <h3>{{ name() | uppercase }}</h3>
    <p>{{ role() }}</p>
  `,
  styles: `h3 { margin: 0; }`,
  host: { class: 'card' },
})
export class UserCard {
  readonly name = input.required<string>();
  readonly role = input('Developer');
}
AdvancedCompiled output: ɵfac and ɵcmp

This is what the compiler produced for that class (taken from the CLI's test build with --dump-virtual-files; ɵ is ɵ, and I've trimmed dev-mode-only padding):

user-card.js (compiled by Angular 22.2)
var UserCard = class _UserCard {
  name = input.required(/* debugName */);
  role = input("Developer", /* debugName */);
  static ɵfac = function UserCard_Factory(__ngFactoryType__) {
    return new (__ngFactoryType__ || _UserCard)();
  };
  static ɵcmp = i0.ɵɵdefineComponent({ type: _UserCard, selectors: [["app-user-card"]],
    hostAttrs: [1, "card"], inputs: { name: [1, "name"], role: [1, "role"] },
    decls: 5, vars: 4,
    template: function UserCard_Template(rf, ctx) {
      if (rf & 1) {
        i0.ɵɵdomElementStart(0, "h3");
        i0.ɵɵtext(1);
        i0.ɵɵpipe(2, "uppercase");
        i0.ɵɵdomElementEnd();
        i0.ɵɵdomElementStart(3, "p");
        i0.ɵɵtext(4);
        i0.ɵɵdomElementEnd();
      }
      if (rf & 2) {
        i0.ɵɵadvance();
        i0.ɵɵtextInterpolate(i0.ɵɵpipeBind1(2, 2, ctx.name()));
        i0.ɵɵadvance(3);
        i0.ɵɵtextInterpolate(ctx.role());
      }
    },
    dependencies: [UpperCasePipe],
    styles: ["\nh3[_ngcontent-%COMP%] {\n  margin: 0;\n}"] });
};
// …plus, only when ngDevMode is on: ɵsetClassMetadata(UserCard, [{ type: Component, args: [{…}] }])
What’s happening
  1. ɵfac is the factory: a function that does new UserCard(). Angular always creates components through it, inside an injection context, which is why inject() works in field initialisers (see Component class vs view).
  2. ɵcmp is the component definition. Every metadata field became data: selector → selectors: [["app-user-card"]], host: { class: 'card' } → hostAttrs: [1, "card"], the two input() fields → inputs, imports → dependencies: [UpperCasePipe].
  3. The template string is gone. It was compiled into the function UserCard_Template(rf, ctx). rf & 1 is the creation block: create the <h3>, a text node, the pipe instance, the <p> and its text node — 5 slots, hence decls: 5. rf & 2 is the update block: re-evaluate ctx.name() through the pipe and ctx.role(), and write them into the text nodes. ctx is your component instance.
  4. The styles were rewritten at build time: h3 became h3[_ngcontent-%COMP%]; at runtime %COMP% is replaced with a per-component id. That's style encapsulation (see View encapsulation and styling).
  5. The original decorator arguments survive only inside an ngDevMode check (ɵsetClassMetadata) for tooling and TestBed overrides. In a production build that block is dropped, so there's no runtime cost to the decorator.
AdvancedInspecting metadata with reflectComponentType

At runtime you can confirm what's on the class:

user-card.spec.ts
const mirror = reflectComponentType(UserCard)!;
mirror.selector;      // 'app-user-card'
mirror.isStandalone;  // true
mirror.inputs;        // [{ propName: 'name', templateName: 'name', isSignal: true }, { propName: 'role', … }]
Object.getOwnPropertyNames(UserCard).filter((k) => k.startsWith('ɵ')); // ['ɵfac', 'ɵcmp']
Advanced@Component metadata fields reference

The metadata fields you'll actually use, from interface Component extends Directive in @angular/core 22.2:

FieldWhat it does
selectorCSS selector that decides where the component is created (see Selectors)
template / templateUrlthe view, inline or in a file (see Inline vs external templates and styles)
styles / styleUrl / styleUrlscomponent styles, inline or from files
importscomponents, directives, pipes and NgModules the template uses (see The modern import model)
hostbindings and listeners on the host element (see The host element)
providers / viewProvidersservices scoped to this component and its children (see Component-level providers)
changeDetectionOnPush or Eager; OnPush is the default in Angular 22 (see OnPush)
encapsulationEmulated (default), None, ShadowDom
inputs / outputsthe old array form of inputs and outputs; today you use input() / output() fields
exportAsa name for #ref="name" template references
hostDirectivesdirectives applied to the host automatically (composition)
schemase.g. CUSTOM_ELEMENTS_SCHEMA to allow unknown elements such as Web Components
standalonedefaults to true; set false only for NgModule-declared components
animationsdeprecated since 20.2 in favour of animate.enter / animate.leave
AdvancedVersion note: OnPush by default

#Selectors

The selector is a CSS selector that tells the compiler where to create a component: on every element in a template, among the ones that imported it, that matches. It uses ordinary CSS selector syntax, but only the parts that can be checked against a single element. The .d.ts lists what's supported: element name, .class, [attribute], [attribute=value], :not(…), and comma-separated alternatives — and nothing that "crosses element boundaries" (no descendant or child combinators).

From my notes: the square brackets ("rectangular bracket" in my notes) in a selector like [appCallout] mean an attribute selector, exactly as in CSS — "any element that has an appCallout attribute".

selectors.ts
@Component({ selector: 'app-badge', template: `[badge]` })
export class Badge {}

@Component({ selector: '[appCallout]', template: `Note: <ng-content />` })
export class Callout {}

@Component({ selector: 'button[appPrimary]', template: `<ng-content />`, host: { class: 'primary' } })
export class PrimaryButton {}

@Component({ selector: '.app-chip', template: `[chip]` })
export class Chip {}

@Component({
  selector: 'app-selector-demo',
  imports: [Badge, Callout, PrimaryButton, Chip],
  template: `
    <app-badge />
    <div appCallout>Save often</div>
    <button appPrimary>Save</button>
    <a appPrimary>Not a button</a>
    <span class="app-chip"></span>
    <span [class.app-chip]="true"></span>
  `,
})
export class SelectorDemo {}
AdvancedSelector kinds rendered side by side
Rendered
<app-badge>[badge]</app-badge>
<div appcallout="">Note: Save often</div>
<button appprimary="" class="primary">Save</button>
<a appprimary="">Not a button</a>
<span class="app-chip">[chip]</span>
<span class="app-chip"></span>
What’s happening
  1. Element selector app-badge: matches <app-badge>, and Angular renders Badge's template inside it. This is the normal choice for components — the custom tag makes it obvious in the template that a component lives there.
  2. Attribute selector [appCallout]: matches the <div appCallout>. The <div> stays a <div> (keeping its semantics and default styles) but becomes Callout's host. <ng-content /> projected the div's own content, giving Note: Save often.
  3. Compound selector button[appPrimary]: element and attribute must both match. The <button> got the component (note class="primary" from its host); the <a appPrimary> did not match, so it's left untouched. This is the pattern UI libraries use for buttons, so you keep a real <button> with built-in keyboard and form behaviour.
  4. Class selector .app-chip: matched the first <span class="app-chip">, which rendered [chip]. Class selectors are allowed but rare for components.
  5. The last <span> has the class too — added by [class.app-chip]="true" — but no component: it's empty. Selectors are matched by the compiler against the static attributes and classes written in the template. Bindings are applied at runtime, long after matching, so they can't create a component.
AdvancedSelector conventions and rules

Selector conventions and rules:

  • Prefix your selectors (app-, or a team/library prefix like ts- for techStatus). The CLI's prefix in angular.json drives the generated selectors. It avoids clashes with future HTML elements and third-party components, and custom elements need a hyphen anyway.
  • Components: element selectors by default; attribute selectors when you're enhancing a native element that must keep its semantics (button[appPrimary], table[appDataTable]).
  • Directives: attribute selectors ([appHighlight]), because a directive adds behaviour to an element someone else owns (see Attribute directives).
  • Only one component per element. If two imported components match the same element, the compiler refuses: NG8023: Multiple components match node with tagname app-badge: 'Badge', 'Callout'. (from <app-badge appCallout /> with both imported). Any number of directives can match the same element (see Components vs directives).
  • A component without a selector can't be used in a template, but can still be rendered by the router or created dynamically. Routed pages often have one anyway, for readability in DevTools.
Which elements get the component?

A component has selector: 'input[type=text][appMask]'. Which of these does it match: <input appMask>, <input type="text" appMask>, <input [type]="'text'" appMask>, <textarea type="text" appMask>?

Show answer

Only <input type="text" appMask>.

What’s happening
  1. The selector requires three things on one element: tag input, attribute type equal to text, and attribute appMask.
  2. <input appMask> has no type attribute in the template (the browser treats it as text, but the compiler only sees what's written), so it doesn't match.
  3. <input type="text" appMask> has all three statically — match.
  4. <input [type]="'text'" appMask> sets type with a binding, which is applied at runtime; the compiler matches against static attributes only, so no match.
  5. <textarea …> fails the element part. Selectors are evaluated per element, at compile time, against the template as written.

#The host element

From my notes: "Host Element (Very important)". The host element is the DOM element that a component is attached to — the <app-toggle> in the parent's template. It belongs to both worlds: the parent creates it (it's in the parent's template, and the parent can put attributes, classes and bindings on it), and the component owns everything inside it (its template renders as the host's children) and can also control the host's own attributes, classes, styles and events.

Get this picture right and a lot falls into place: the component's template is not wrapped around the host — it goes inside it. There's no extra wrapper element unless you write one. :host in the component's CSS means "my host element". And when the component needs a click handler, an ARIA role or a CSS class on itself, it puts them on the host rather than on an inner <div>.

In modern Angular all of that goes in the host metadata object. Keys in quotes and brackets/parentheses mean the same as in templates: [prop] binds, (event) listens, a plain key is a static attribute.

toggle.ts
@Component({
  selector: 'app-toggle',
  template: `{{ on() ? 'ON' : 'OFF' }}`,
  styles: `
    :host { display: inline-block; padding: 4px 8px; border: 1px solid; }
    :host(.is-on) { background: lightgreen; }
  `,
  host: {
    role: 'switch',
    tabindex: '0',
    '[class.is-on]': 'on()',
    '[attr.aria-checked]': 'on()',
    '(click)': 'toggle()',
    '(keydown.space)': 'toggle(); $event.preventDefault()',
  },
})
export class Toggle {
  protected readonly on = signal(false);
  toggle() {
    this.on.update((v) => !v);
  }
}
AdvancedHost attributes, bindings and listeners rendered
Used in a parent, rendered
<!-- parent template: <app-toggle class="big" /> -->
<app-toggle role="switch" tabindex="0" class="big" _nghost-ng-c1817490615="" aria-checked="false">OFF</app-toggle>
What’s happening
  1. The parent's template has <app-toggle class="big" />. When the parent's view is created, Angular creates the <app-toggle> element (the host), applies the parent's static class="big", creates a Toggle instance for it, and renders Toggle's template — the text OFF — inside it.
  2. role: 'switch' and tabindex: '0' are static host attributes: written once when the host is created, so the element is focusable and announced as a switch by screen readers.
  3. '[class.is-on]': 'on()' and '[attr.aria-checked]': 'on()' are host bindings, evaluated in the component's context on every check of this component. Initially on() is false: no is-on class, aria-checked="false". The component's class binding is merged with the parent's class="big" — neither overwrites the other.
  4. '(click)': 'toggle()' and '(keydown.space)' are host listeners — the whole element is the click target, no inner button needed. In the spec, one click plus one space press flipped on false → true → false, so it still read OFF; one more click gave class="big is-on" and aria-checked="true". A host listener also marks the component for check, which is why the DOM updated with no extra code.
  5. :host { display: inline-block } styles the host element itself. In the generated CSS it became [_nghost-ng-c1817490615] { … }, and :host(.is-on) became .is-on[_nghost-ng-c1817490615] — "the host, when it has class is-on".
AdvancedOlder syntax: @HostBinding and @HostListener

The older form, still supported and all over techStatus-era code, uses decorators on class members:

toggle.ts (legacy form)
@Component({
  selector: 'app-legacy-toggle',
  template: `{{ on ? 'ON' : 'OFF' }}`,
})
export class LegacyToggle {
  on = false;
  @HostBinding('attr.role') role = 'switch';
  @HostBinding('class.is-on') get isOn() { return this.on; }
  @HostBinding('attr.aria-checked') get checked() { return this.on; }
  @HostListener('click') toggle() { this.on = !this.on; }
}
AdvancedLegacy toggle before and after a click
Before and after one click
<app-legacy-toggle role="switch" aria-checked="false">OFF</app-legacy-toggle>
<app-legacy-toggle role="switch" aria-checked="true" class="is-on">ON</app-legacy-toggle>
What’s happening
  1. @HostBinding('class.is-on') on a getter is the same as '[class.is-on]': 'isOn' in host; Angular reads the getter on every check of the component.
  2. @HostListener('click') on toggle() registers a click listener on the host, the same as '(click)': 'toggle()'.
  3. After one click, on is true, the listener marked the component dirty, and the next render wrote aria-checked="true", class="is-on" and ON — even though on is a plain field, not a signal, because events handled by the component schedule a check of it.
  4. Both forms compile to the same host bindings. Prefer host: {} in new code (the Angular style guide recommends it): everything about the host is in one place, its expressions are type-checked like template expressions, and it works the same in components and directives.
AdvancedStyling pitfall: inline host elements
AdvancedParent vs component host attributes

#Component class vs view

My notes have this as "Component instance vs view" and "Component instantiated as a class", with a "nice explanation below". The distinction:

  • The component instance is a normal JavaScript object — new Toggle() — holding state (on, count, injected services) and methods. There's one instance per host element.
  • The view is what Angular builds from the template for that instance: the actual DOM nodes inside the host, plus Angular's bookkeeping about which node each binding writes to and which listeners call which methods. A component's view is created once and then updated many times.

The class never touches the view directly. The compiled template function receives the instance as ctx and reads from it (ctx.name() in the compiled output above). So "instance" is the data, "view" is the rendering of that data, and change detection is the process of re-running the template's update block against the instance.

probe.ts
@Component({
  selector: 'app-probe',
  template: `<span>{{ clicks() }}</span><button (click)="clicks.set(clicks() + 1)">+</button>`,
})
export class Probe {
  private readonly host: HTMLElement = inject(ElementRef).nativeElement;
  protected readonly clicks = signal(0);

  constructor() {
    probeLog.push(`constructor: host <${this.host.tagName.toLowerCase()}> has ${this.host.childNodes.length} child nodes`);
    afterNextRender(() =>
      probeLog.push(`afterNextRender: host innerHTML = ${this.host.innerHTML}`),
    );
  }
}

@Component({
  selector: 'app-probe-pair',
  imports: [Probe],
  template: `<app-probe /><app-probe />`,
})
export class ProbePair {}
AdvancedProbe output: host exists before the view
Output
constructor: host <app-probe> has 0 child nodes
constructor: host <app-probe> has 0 child nodes
-- createComponent returned
afterNextRender: host innerHTML = <span>0</span><button>+</button>
afterNextRender: host innerHTML = <span>0</span><button>+</button>
two instances? 2 | same object? false
rendered: <app-probe><span>2</span><button>+</button></app-probe><app-probe><span>1</span><button>+</button></app-probe>
What’s happening
  1. TestBed.createComponent(ProbePair) creates ProbePair and runs its template's creation pass. That pass meets <app-probe /> twice: for each one it creates the host element, then calls Probe's factory. So both Probe constructors run during the parent's creation pass — before createComponent returns.
  2. Inside the constructor, inject(ElementRef) already gives the host element <app-probe> — the host exists before the instance. But it has 0 child nodes: the instance exists, the view doesn't yet. Don't touch the DOM in a constructor.
  3. Each Probe view is then created and updated: <span>0</span><button>+</button>. afterNextRender callbacks fire after that first render; only then is the view's DOM there to measure or focus (see afterNextRender and afterEveryRender).
  4. queryAll(By.directive(Probe)) finds two different instances (same object? false). Clicking the first button twice and the second once gives 2 and 1: each instance has its own clicks signal, and each view reflects only its own instance. One class, many instances, one view per instance.
AdvancedWhy new Probe() throws NG0203

You can't skip Angular and create a component yourself:

probe.spec.ts
new Probe();
// NG0203: The `ElementRef` token injection failed. `inject()` function must be called from an
// injection context such as a constructor, a factory function, a field initializer, or a
// function used with `runInInjectionContext`.
What’s happening
  1. new Probe() is legal JavaScript, so the constructor starts running — but outside Angular's factory.
  2. The first field initialiser calls inject(ElementRef). inject() only works while Angular is creating something (an injection context), because it needs to know which injector to ask and, for ElementRef, which element.
  3. There's no context, so it throws NG0203 (see The injection context). Even without inject(), a hand-made instance would have no view, no host element and no change detection — it would just be an object.
  4. That's the "component instantiated as a class" point from my notes: Angular instantiates the class for you, through ɵfac, every time a matching element appears, and owns its lifetime from then on.

#Parent and child components

A component becomes a child simply by being used in another component's template: if TodoList's template contains <app-todo-item>, then each TodoItem created there is a child of that TodoList. That's the whole mechanism. The component tree is the result: the root component's template creates children, their templates create grandchildren, and so on. The tree mirrors the templates, not the file structure or the imports.

Three things follow from "each element in the parent template is a child":

  1. One child instance per element. <app-todo-item> written three times, or inside an @for over three items, means three separate TodoItem instances, each with its own state.
  2. The parent controls the child's existence. When an @if turns false or an @for item is removed, Angular destroys that child — instance, view and all.
  3. Data flows along the tree. Parents pass data down with inputs and get events up with outputs (see Parent to child with input() and Child to parent with output()). Children don't reach into parents.
todo.ts
@Component({
  selector: 'app-todo-item',
  template: `<p>{{ text() }}</p>`,
})
export class TodoItem implements OnDestroy {
  readonly text = input.required<string>();
  constructor() {
    treeLog.push('TodoItem constructor');
  }
  ngOnDestroy() {
    treeLog.push(`TodoItem destroyed: ${this.text()}`);
  }
}

@Component({
  selector: 'app-todo-list',
  imports: [TodoItem],
  template: `
    <h2>{{ todos().length }} todos</h2>
    @for (todo of todos(); track todo) {
      <app-todo-item [text]="todo" />
    }
  `,
})
export class TodoList {
  readonly todos = signal(['Learn components', 'Learn signals']);
  constructor() {
    treeLog.push('TodoList constructor');
  }
}
AdvancedChild creation and destruction order with @for
Output
TodoList constructor
-- createComponent returned
TodoItem constructor
TodoItem constructor
-- first render done
TodoItem constructor
-- added one
TodoItem destroyed: Learn components
-- removed one
<h2>2 todos</h2><app-todo-item><p>Learn signals</p></app-todo-item><app-todo-item><p>Learn DI</p></app-todo-item>
after fixture.destroy(): TodoItem destroyed: Learn signals | TodoItem destroyed: Learn DI
What’s happening
  1. TestBed.createComponent(TodoList) creates the parent (TodoList constructor) and runs its creation pass. Unlike the static <app-probe /> children in the previous example, no child is created yet: the @for block only creates its rows when its collection is evaluated, which happens in the update pass.
  2. The first render (whenStable) evaluates todos() → two items, so @for creates two embedded views, each with an <app-todo-item> host and a TodoItem instance. Both constructors run, then each child gets its text input and renders its <p>.
  3. Adding 'Learn DI' creates a new array (update with spread). On the next render, @for compares by track todo: the two existing items are kept (no new constructors for them), and one new TodoItem is constructed for 'Learn DI'.
  4. Filtering out 'Learn components' makes @for remove that row: Angular destroys its embedded view and the TodoItem in it, calling ngOnDestroy (TodoItem destroyed: Learn components). The other two instances are untouched.
  5. fixture.destroy() destroys the parent, and destruction cascades down the tree: both remaining children run ngOnDestroy. A child never outlives the view that created it.
AdvancedCommon parent-child surprises

Things that trip people up:

  • Children are created in template order, after the parent. The parent's constructor always runs first, because the parent's view has to exist to contain the child's host element. Parent ngOnInit also runs before the children's (the full order is in The initial lifecycle order).
  • Static children vs control-flow children. Children written directly in the template are created during the parent's creation pass; children inside @if/@for/@switch are created when that block first renders. Both are children of the same parent component.
  • The DOM tree and the component tree differ. Content you project with <ng-content> sits inside the child's host element in the DOM, but it was created by — and belongs to — the component whose template wrote it (see Content projection with ng-content).
  • Getting hold of a child. Inputs and outputs cover most needs. When a parent really needs the child's instance — to call focus() or reset() — it queries it with viewChild(TodoItem) (see Querying a child component).

#Components vs directives

From my notes: "Component vs Directive" and "Some more info on directives". A directive is a class that Angular attaches to an element matched by its selector, to add behaviour: it can bind to the element's properties, attributes, classes and styles, listen to its events, and take inputs. A component is a directive with a template — the .d.ts says it literally: interface Component extends Directive. Everything a directive can do (selector, inputs, outputs, host, providers) a component can do too; the component adds its own view.

That one difference drives the rest:

ComponentDirective
Template / viewyes — renders its own DOM inside the hostno — works on an element someone else rendered
Per elementat most oneany number, plus one component
Typical selectorelement: app-user-cardattribute: [appTint]
Typical joba piece of UI: card, form, pagereusable behaviour: tint, trim, tooltip, autofocus
Exampleseverything you build with @ComponentRouterLink, NgModel, NgClass, NgTemplateOutlet

The "one component per element" rule exists because a component owns the element's content: two templates can't both render into the same host. Directives don't render content, so they can stack.

tint.ts
@Directive({
  selector: '[appTint]',
  host: { '[style.backgroundColor]': 'color()' },
})
export class Tint {
  readonly color = input('yellow', { alias: 'appTint' });
}

@Directive({
  selector: '[appTrim]',
  host: { '(blur)': 'trim()' },
})
export class Trim {
  private readonly input: HTMLInputElement = inject(ElementRef).nativeElement;
  trim() {
    this.input.value = this.input.value.trim();
  }
}

@Component({
  selector: 'app-directive-demo',
  imports: [Tint, Trim, Badge],
  template: `
    <p appTint="lightyellow">tinted paragraph</p>
    <p appTint>bare attribute</p>
    <input appTint="pink" appTrim value="  spaced  " />
    <app-badge appTint="lightblue" />
  `,
})
export class DirectiveDemo {}
AdvancedDirective and component on one element
Rendered
<p apptint="lightyellow" style="background-color: lightyellow;">tinted paragraph</p>
<p apptint="">bare attribute</p>
<input apptint="pink" apptrim="" value="  spaced  " style="background-color: pink;">
<app-badge apptint="lightblue" style="background-color: lightblue;">[badge]</app-badge>
What’s happening
  1. Tint has no template. Its selector matches any element with an appTint attribute, and the input is aliased to the same name, so appTint="lightyellow" both applies the directive and sets its input. Its host binding writes style.backgroundColor — the first <p> became light yellow.
  2. The <input> has two directives: Tint (pink background) and Trim. In the spec, input.injector.get(Tint) and .get(Trim) both returned instances for the same element. Blurring the input ran Trim.trim() and the value went from " spaced " to "spaced".
  3. <app-badge appTint="lightblue"> has a component and a directive on one element: Badge renders [badge] inside the host, and Tint colours the host. That's legal; what's illegal is two components (NG8023, see Selectors).
  4. The second <p appTint> is not yellow, even though the input's default is 'yellow'. A bare attribute is a static attribute with value "", and because it matches the input's alias Angular sets the input to "" — which overrides the default and clears the background. Directives whose selector doubles as an input need to handle the empty string (for example a transform: (v: string) => v || 'yellow').
AdvancedChoosing a component or a directive

When to choose which:

  • If it renders markup (anything with its own HTML), it's a component.
  • If it decorates an element someone else renders — behaviour, attributes, styles, events — it's a directive. You'd make a component only if the thing needs its own DOM.
  • If you need the same behaviour on many components, write a directive and either put it in each template or attach it with hostDirectives in the components' metadata.

Angular has three kinds of directives: components (directives with templates), attribute directives (change the look or behaviour of an element, like Tint), and structural directives (add or remove DOM by controlling an <ng-template>, like the old *ngIf). Built-in control flow (@if, @for) has replaced most structural directives in everyday code. Attribute directives are covered properly in Attribute directives, and structural ones in *ngIf and ng-template.

#Inline vs external templates and styles

A component's template can be written inline in the decorator (template: `…`) or in a separate file (templateUrl: './greeting.html'). Styles likewise: styles inline, or styleUrl (one file) / styleUrls (several). It's purely a source-organisation choice — the compiler inlines external files at build time, so the compiled component is identical either way and there's no extra HTTP request at runtime.

greeting.ts
// External: template and styles in their own files.
@Component({
  selector: 'app-greeting',
  templateUrl: './greeting.html',
  styleUrl: './greeting.css',
})
export class Greeting {
  protected readonly name = signal('Rohit');
}

// Inline: the same component in one file.
@Component({
  selector: 'app-greeting-inline',
  template: `<p class="greeting">Hello, {{ name() }}!</p>`,
  styles: `.greeting { font-weight: bold; }`,
})
export class GreetingInline {
  protected readonly name = signal('Rohit');
}
AdvancedExternal template file greeting.html
greeting.html
<p class="greeting">Hello, {{ name() }}!</p>
AdvancedInline and external templates rendered
Rendered
external: <p _ngcontent-a-c3877310805="" class="greeting">Hello, Rohit!</p>
inline:   <p _ngcontent-a-c2595531065="" class="greeting">Hello, Rohit!</p>
What’s happening
  1. For Greeting, the compiler reads greeting.html and greeting.css from disk at build time, relative to the .ts file. The template is type-checked against the class just like an inline one — a typo in greeting.html is a build error that points at the .html line.
  2. For GreetingInline the template is a string literal in the decorator. It must be a literal (not a variable or a computed string), because the compiler reads it statically.
  3. Both render the same <p>, with the same encapsulated style. The only difference in the output is the per-component id in _ngcontent-… — two different component classes, two different ids. (The prefix is a in TestBed; in a real app it's ng, e.g. _ngcontent-ng-c3045686696.)
  4. A missing file fails the build with NG2008: Could not find template file './does-not-exist.html'.
AdvancedChoosing inline or external templates

How to choose:

  • Inline for small components — a handful of lines of template and a few style rules. Everything is in one file, which is pleasant to read and review. The Angular language service still gives completion and type errors inside the template string.
  • External once the template is big, or when designers edit the HTML/CSS, or when you want SCSS tooling and HTML formatting to work on real files. The CLI generates external files by default (ng g c creates .html and .css); --inline-template and --inline-style (-t, -s) switch it.
  • Be consistent within a project. Mixing is fine technically; a codebase where similar components use different styles is harder to scan.
AdvancedPitfall: setting both template sources

#View encapsulation and styling

Added

By default, CSS you write in a component applies only to that component's own template. A .msg { color: red } in one component won't turn another component's .msg red, even though CSS is normally global. Angular does this with view encapsulation, set per component with encapsulation:

  • ViewEncapsulation.Emulated (default): the compiler rewrites every selector to also require a unique attribute, and Angular stamps that attribute on the elements of this component's template. Scoped styles, no Shadow DOM.
  • ViewEncapsulation.None: styles are added to the page as written — global. They affect every matching element in the app.
  • ViewEncapsulation.ShadowDom: Angular attaches a real shadow root to the host and renders the template and styles inside it, so the browser itself enforces the boundary. (22.2 also has ExperimentalIsolatedShadowDom, marked experimental, which additionally keeps other component styles out.)

A mental model for Emulated: every component gets a name tag. Its elements wear the tag (_ngcontent-ng-c123), its host wears a host tag (_nghost-ng-c123), and its CSS rules say "only elements wearing my tag".

encapsulation.ts
@Component({
  selector: 'app-emulated',
  template: `<p class="msg">emulated</p>`,
  styles: `
    :host { display: block; }
    .msg { color: red; }
    :host ::ng-deep .msg { font-style: italic; }
  `,
})
export class Emulated {}

@Component({
  selector: 'app-unscoped',
  encapsulation: ViewEncapsulation.None,
  template: `<p class="msg">none</p>`,
  styles: `.msg { color: blue; }`,
})
export class Unscoped {}

@Component({
  selector: 'app-shadow',
  encapsulation: ViewEncapsulation.ShadowDom,
  template: `<p class="msg">shadow</p>`,
  styles: `.msg { color: green; }`,
})
export class Shadow {}

@Component({
  selector: 'app-encapsulation-demo',
  imports: [Emulated, Unscoped, Shadow],
  template: `
    <p class="msg">parent paragraph</p>
    <app-emulated />
    <app-unscoped />
    <app-shadow />
  `,
})
export class EncapsulationDemo {}
AdvancedFour encapsulation modes in the DOM
DOM and the <style> tags Angular added to <head>
<p class="msg">parent paragraph</p>
<app-emulated _nghost-a-c3045686696=""><p _ngcontent-a-c3045686696="" class="msg">emulated</p></app-emulated>
<app-unscoped><p class="msg">none</p></app-unscoped>
<app-shadow></app-shadow>

[_nghost-a-c3045686696] { display: block; }
.msg[_ngcontent-a-c3045686696] { color: red; }
[_nghost-a-c3045686696]     .msg { font-style: italic; }
---
.msg { color: blue; }
What’s happening
  1. Emulated. At build time the compiler rewrote .msg to .msg[_ngcontent-%COMP%] and :host to [_nghost-%COMP%]. At runtime %COMP% became this component's id (a-c3045686696 in TestBed; a bootstrapped app printed _nghost-ng-c3045686696), the <p> in its template got the _ngcontent-… attribute and the host got _nghost-…. So color: red matches only this component's <p> — the parent's .msg paragraph is not red.
  2. ::ng-deep. :host ::ng-deep .msg became [_nghost-…] .msg — the content attribute requirement was dropped after ::ng-deep, so it matches any .msg anywhere inside this host, including inside child components. That's how people style third-party components' internals, and why it's dangerous: it's a global rule in disguise.
  3. None. Unscoped's rule was added unchanged: .msg { color: blue; }. Its own <p> has no special attribute, and the rule applies to every .msg on the page — including the parent's paragraph and, unless a more specific rule wins, the emulated one. That's what "None" means.
  4. ShadowDom. <app-shadow> looks empty in the light DOM because its template is inside shadowRoot. The shadow root contained a <style> with .msg { color: green; } — unrewritten, since the shadow boundary does the scoping — plus <p class="msg">shadow</p>. It also contained copies of the other components' styles (the Emulated rules and the global .msg { color: blue }): Angular adds the component styles it manages into each shadow root so None styles still reach it. Your global styles.css, loaded with a <link>, does not cross the boundary.
AdvancedPractical styling rules

Practical rules:

  • Keep the default. Emulated gives you scoped styles with no runtime cost beyond an attribute per element, and works with global styles from styles.css.
  • Style the host with :host, not a wrapper <div>: :host { display: block }, :host(.active), :host-context(.dark-theme) (matches when an ancestor has the class).
  • Global things go in styles.css (resets, typography, CSS variables), not in a None component. A None component's styles are global but only added when that component is first rendered — in my test the rule wasn't in <head> while the component sat behind a false @if, and appeared once it rendered — so it's an unpredictable place for global CSS.
  • Theming across boundaries: CSS custom properties. Variables like --card-bg inherit through emulated boundaries and shadow roots. Let parents set --card-bg and have the child use background: var(--card-bg, white) — a public styling API that needs no ::ng-deep.
  • ::ng-deep is deprecated in Angular's documentation and kept only for compatibility. If you must style a library's internals, scope it under :host (as above) so it doesn't leak out of your component, and prefer the library's own theming variables or inputs.
Who turns red?

A parent component (Emulated) has p { color: red; } in its styles and this template: <p>one</p><app-child />. The child's template is <p>two</p>. The parent also projects nothing. Which paragraphs are red? What if the parent's rule were :host ::ng-deep p { color: red; }?

Show answer

Only one is red. With the ::ng-deep version, both are.

What’s happening
  1. The parent's rule compiles to p[_ngcontent-ng-cP], where cP is the parent's id.
  2. <p>one</p> is written in the parent's template, so Angular gives it _ngcontent-ng-cP → it matches → red.
  3. <p>two</p> is written in the child's template, so it never gets the parent's attribute → no match → not red. (In my test the child had no styles, so Angular switched it to None and its <p> got no attribute at all; with styles it would get the child's own _ngcontent-ng-cC.) The child's host element <app-child> does carry the parent's _ngcontent-ng-cP, because the host is written in the parent's template — the rendered DOM was <p _ngcontent-…>one</p><app-child _ngcontent-…><p>two</p></app-child>.
  4. With :host ::ng-deep p, the rule compiles to [_nghost-ng-cP] p: any <p> that's a descendant of the parent's host, whatever its own attributes. Both paragraphs are inside the parent's host, so both turn red — encapsulation is switched off for everything after ::ng-deep.

Built from Rohit’s “Angular interview prep” doc. Examples target Angular 22 and are compiled and tested with the Angular CLI and Vitest.

RohitDownloads · All chapters

Sync progress across devices

Type the same private phrase on your Mac and your phone, and your Learned ticks follow you between them — on every study site.

The phrase never leaves this device: only a fingerprint of it is sent, and the server stores a fingerprint of that. Anyone who knows the phrase could see or change your ticks, so pick something you don’t use elsewhere.

Sync is on in this browser.

Sync ID

This ID must be the same on every device. If another device shows a different one, its phrase is different (capital letters count): tap “Turn off here” on it and type the phrase again exactly.