Skip to content

Components & Templates

An Angular component has three parts: a TypeScript class (logic), an HTML template (view), and CSS (styling). Components are the building blocks of Angular apps. Data flows down via @Input() and up via @Output() EventEmitters. Directives extend HTML — structural (*ngIf, *ngFor) add/remove DOM, attribute directives ([ngClass], [ngStyle]) change appearance.


Component Structure

A component is defined with @Component decorator specifying selector (custom HTML tag), templateUrl (or inline template), and styleUrls. The class contains properties and methods. Components must be declared in an NgModule or be standalone.

Deep Dive: Example
@Component({
    selector: 'app-user-card',
    templateUrl: './user-card.component.html',
    styleUrls: ['./user-card.component.css']
})
export class UserCardComponent {
    @Input() user!: User;               // from parent
    @Output() delete = new EventEmitter<number>();  // to parent

    onDelete() {
        this.delete.emit(this.user.id);
    }
}
<!-- user-card.component.html -->
<div class="card">
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    <button (click)="onDelete()">Delete</button>
</div>
<!-- Parent usage -->
<app-user-card
    [user]="selectedUser"
    (delete)="handleDelete($event)">
</app-user-card>

Data binding types: | Syntax | Direction | Example | |--------|-----------|---------| | {{ }} | Component → Template | {{ user.name }} | | [property] | Component → Template | [disabled]="isLoading" | | (event) | Template → Component | (click)="save()" | | [(ngModel)] | Two-way | [(ngModel)]="search" |


@Input and @Output

@Input() passes data from parent to child (property binding). @Output() emits events from child to parent using EventEmitter. This is Angular's one-way data flow — parent owns the data, child receives and notifies.

Deep Dive: Communication Patterns
// Child component
@Component({ selector: 'app-counter' })
export class CounterComponent {
    @Input() count: number = 0;                   // receives from parent
    @Output() countChange = new EventEmitter<number>();  // emits to parent

    increment() {
        this.count++;
        this.countChange.emit(this.count);
    }
}

// Parent template — two-way binding shorthand
<app-counter [(count)]="totalCount"></app-counter>

// Equivalent to:
<app-counter [count]="totalCount" (countChange)="totalCount = $event"></app-counter>

@Input with setter (react to changes):

private _name = '';

@Input()
set name(value: string) {
    this._name = value.trim().toUpperCase();  // transform on set
}
get name(): string { return this._name; }


Lifecycle Hooks

Key hooks in order: ngOnChanges (input property changes), ngOnInit (initialization — fetch data here), ngDoCheck (custom change detection), ngAfterViewInit (view rendered), ngOnDestroy (cleanup — unsubscribe observables, remove listeners). Most commonly used: ngOnInit and ngOnDestroy.

Deep Dive: All Hooks
export class UserComponent implements OnInit, OnChanges, OnDestroy {
    @Input() userId!: number;
    private subscription!: Subscription;

    ngOnChanges(changes: SimpleChanges) {
        // Called BEFORE ngOnInit and whenever @Input properties change
        if (changes['userId']) {
            console.log('Previous:', changes['userId'].previousValue);
            console.log('Current:', changes['userId'].currentValue);
        }
    }

    ngOnInit() {
        // Called ONCE after first ngOnChanges
        // Best place to fetch data, initialize logic
        this.subscription = this.userService.getUser(this.userId)
            .subscribe(user => this.user = user);
    }

    ngOnDestroy() {
        // Cleanup: unsubscribe, clear timers, detach listeners
        this.subscription.unsubscribe();
    }
}
Hook When Use For
ngOnChanges Input changes React to input property changes
ngOnInit After first change detection Fetch data, initialize
ngDoCheck Every change detection Custom change detection
ngAfterContentInit After content projection Access <ng-content>
ngAfterViewInit After view renders Access @ViewChild
ngOnDestroy Before component removed Cleanup subscriptions

Change Detection

Angular checks component tree for changes after every async event (click, HTTP response, timer). Default strategy: checks entire component tree. OnPush strategy: only checks when @Input reference changes or an event fires in the component. Use OnPush for performance — forces immutable data patterns.

Deep Dive: Default vs OnPush
@Component({
    selector: 'app-user',
    changeDetection: ChangeDetectionStrategy.OnPush,  // ← OnPush
    template: `<p>{{ user.name }}</p>`
})
export class UserComponent {
    @Input() user!: User;
}

OnPush triggers re-render only when: 1. @Input() reference changes (not mutation!) 2. Event handler fires in the component 3. Observable used with async pipe emits 4. ChangeDetectorRef.markForCheck() is called manually

// ❌ Won't work with OnPush (mutation)
this.user.name = "New Name";

// ✅ Works with OnPush (new reference)
this.user = { ...this.user, name: "New Name" };

Structural Directives

Structural directives add/remove DOM elements: *ngIf (conditional rendering), *ngFor (loop), *ngSwitch (switch-case). The * is syntactic sugar for <ng-template>. Only one structural directive per element.

Deep Dive: Examples
<!-- *ngIf — conditional rendering -->
<div *ngIf="isLoggedIn; else loginTemplate">
    Welcome, {{ user.name }}!
</div>
<ng-template #loginTemplate>
    <button (click)="login()">Log In</button>
</ng-template>

<!-- *ngFor — loop with trackBy for performance -->
<ul>
    <li *ngFor="let user of users; let i = index; trackBy: trackByUserId">
        {{ i + 1 }}. {{ user.name }}
    </li>
</ul>

<!-- *ngSwitch -->
<div [ngSwitch]="status">
    <p *ngSwitchCase="'active'">Active</p>
    <p *ngSwitchCase="'inactive'">Inactive</p>
    <p *ngSwitchDefault>Unknown</p>
</div>
// trackBy function — prevents re-rendering entire list
trackByUserId(index: number, user: User): number {
    return user.id;
}

Attribute Directives

Attribute directives change appearance or behavior without adding/removing elements: [ngClass] (dynamic CSS classes), [ngStyle] (dynamic styles). You can create custom attribute directives with @Directive.

Deep Dive: Built-in & Custom
<!-- ngClass — conditional CSS classes -->
<div [ngClass]="{ 'active': isActive, 'error': hasError }">Status</div>

<!-- ngStyle — dynamic inline styles -->
<p [ngStyle]="{ 'color': isError ? 'red' : 'green', 'font-size': fontSize + 'px' }">
    Message
</p>

Custom directive:

@Directive({ selector: '[appHighlight]' })
export class HighlightDirective {
    @Input() appHighlight = 'yellow';

    constructor(private el: ElementRef) {}

    @HostListener('mouseenter') onMouseEnter() {
        this.el.nativeElement.style.backgroundColor = this.appHighlight;
    }

    @HostListener('mouseleave') onMouseLeave() {
        this.el.nativeElement.style.backgroundColor = '';
    }
}

// Usage: <p [appHighlight]="'cyan'">Hover me</p>


Dependency Injection

Angular has a built-in DI container. Services are decorated with @Injectable({ providedIn: 'root' }) — making them singletons app-wide. Inject via constructor. DI hierarchy: root (single instance), module-level (per module), component-level (per component instance).

Deep Dive: Providers & Injection
// Service
@Injectable({ providedIn: 'root' })  // singleton, tree-shakable
export class UserService {
    constructor(private http: HttpClient) {}

    getUsers(): Observable<User[]> {
        return this.http.get<User[]>('/api/users');
    }
}

// Component — inject via constructor
@Component({ selector: 'app-users' })
export class UsersComponent {
    constructor(private userService: UserService) {}  // injected!
}

// Component-level provider — new instance per component
@Component({
    selector: 'app-counter',
    providers: [CounterService]  // scoped to this component
})
export class CounterComponent { }

Pipes

Pipes transform displayed values in templates: {{ value | pipeName }}. Built-in: date, currency, uppercase, json, async. Custom pipes with @Pipe. Pure pipes (default) only run when input reference changes. Impure pipes run on every change detection — use sparingly.

Deep Dive: Built-in & Custom Pipes
{{ user.createdAt | date:'short' }}
{{ price | currency:'USD' }}
{{ name | uppercase }}
{{ data | json }}
{{ users$ | async }}  <!-- auto-subscribes & unsubscribes! -->

Custom pipe:

@Pipe({ name: 'truncate' })
export class TruncatePipe implements PipeTransform {
    transform(value: string, limit: number = 50): string {
        return value.length > limit ? value.substring(0, limit) + '...' : value;
    }
}

// Usage: {{ description | truncate:100 }}


Common Interview Questions

Common Interview Questions
  • What is a component in Angular? What are its parts?
  • How do you pass data between parent and child components?
  • What are lifecycle hooks? Which ones do you use most?
  • What is change detection? Default vs OnPush?
  • What is the difference between structural and attribute directives?
  • What is dependency injection in Angular?
  • What is trackBy in *ngFor and why is it important?
  • What are pipes? Pure vs impure?
  • What is the async pipe?