Component Basics
Component Structure
import { Component } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.css']
})
export class MyComponent {
// Component properties and methods
}
Creating Components
# Generate a new component
ng generate component my-component
# Shorthand
ng g c my-component
# Create in specific folder
ng g c features/user/user-profile
# With inline template
ng g c my-component --inline-template
# or
ng g c my-component -t
# With inline styles
ng g c my-component --inline-style
# or
ng g c my-component -s
# Without test file
ng g c my-component --skip-tests
# Standalone component (Angular 14+)
ng g c my-component --standalone
Component Selector Types
@Component({
// Element selector (most common)
selector: 'app-my-component',
// Attribute selector
selector: '[app-my-component]',
// Class selector
selector: '.app-my-component'
})
Usage in templates:
<!-- Element selector -->
<app-my-component></app-my-component>
<!-- Attribute selector -->
<div app-my-component></div>
<!-- Class selector -->
<div class="app-my-component"></div>
Inline Templates and Styles
@Component({
selector: 'app-my-component',
// Inline template
template: `
<h1>{{ title }}</h1>
<p>This is an inline template</p>
`,
// Inline styles
styles: [`
h1 { color: blue; }
p { font-size: 16px; }
`]
})
Component Data Binding
Interpolation
<h1>{{ title }}</h1>
<p>Welcome, {{ user.name }}</p>
<div>{{ getFullName() }}</div>
<span>{{ 2 + 2 }}</span>
Property Binding
<!-- Binding to element property -->
<img [src]="imageUrl">
<button [disabled]="isDisabled">Click me</button>
<!-- Binding to a custom property of a component -->
<app-child [data]="parentData"></app-child>
<!-- Alternative syntax (canonical form) -->
<img bind-src="imageUrl">
Event Binding
<!-- Basic event binding -->
<button (click)="onClick()">Click me</button>
<!-- With event object -->
<input (keyup)="onKeyUp($event)">
<form (submit)="onSubmit($event)">
<!-- Alternative syntax (canonical form) -->
<button on-click="onClick()">Click me</button>
Two-way Binding
<!-- Using ngModel (requires FormsModule) -->
<input [(ngModel)]="name">
<!-- "Banana in a box" syntax -->
<app-counter [(count)]="parentCount"></app-counter>
<!-- Alternative syntax (canonical form) -->
<input bindon-ngModel="name">
<!-- Expanded form (equivalent to ngModel) -->
<input [ngModel]="name" (ngModelChange)="name = $event">
Component Lifecycle Hooks
import {
Component, OnInit, OnChanges, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked,
OnDestroy, SimpleChanges
} from '@angular/core';
@Component({...})
export class LifecycleComponent implements
OnInit, OnChanges, DoCheck, AfterContentInit,
AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
// Called before ngOnInit and when input properties change
ngOnChanges(changes: SimpleChanges): void {
console.log('ngOnChanges', changes);
}
// Called once after the first ngOnChanges
ngOnInit(): void {
console.log('ngOnInit');
// Ideal for initialization logic, API calls
}
// Called during every change detection cycle
ngDoCheck(): void {
console.log('ngDoCheck');
}
// Called after content projection is initialized
ngAfterContentInit(): void {
console.log('ngAfterContentInit');
}
// Called after every check of projected content
ngAfterContentChecked(): void {
console.log('ngAfterContentChecked');
}
// Called after component's view is initialized
ngAfterViewInit(): void {
console.log('ngAfterViewInit');
// Safe to interact with view DOM elements
}
// Called after every check of component's view
ngAfterViewChecked(): void {
console.log('ngAfterViewChecked');
}
// Called before component is destroyed
ngOnDestroy(): void {
console.log('ngOnDestroy');
// Cleanup: unsubscribe from observables, clear timers
}
}
Lifecycle Execution Order
ngOnChanges
– First and when input properties changengOnInit
– Once after first ngOnChangesngDoCheck
– After ngOnChanges and ngOnInit, and during every change detectionngAfterContentInit
– After projected content is initializedngAfterContentChecked
– After content checkngAfterViewInit
– After component’s view initializationngAfterViewChecked
– After view checkngOnDestroy
– Before component destruction
Component Communication
Parent to Child: @Input
Child component:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
template: `<p>Message from parent: {{ message }}</p>`
})
export class ChildComponent {
@Input() message: string = '';
// With alias
@Input('userData') user: any;
// With setter for intercepting changes
private _count = 0;
@Input()
set count(value: number) {
console.log(`Count changed from ${this._count} to ${value}`);
this._count = value;
}
get count(): number {
return this._count;
}
// With OnChanges hook
@Input() data: any;
ngOnChanges(changes: SimpleChanges) {
if (changes['data']) {
console.log('Previous:', changes['data'].previousValue);
console.log('Current:', changes['data'].currentValue);
console.log('First change:', changes['data'].firstChange);
}
}
}
Parent component:
@Component({
selector: 'app-parent',
template: `
<app-child
[message]="parentMessage"
[userData]="user"
[count]="counter"
[data]="parentData">
</app-child>
<button (click)="updateData()">Update Data</button>
`
})
export class ParentComponent {
parentMessage = 'Hello from parent';
user = { name: 'John', age: 30 };
counter = 0;
parentData = { value: 'initial' };
updateData() {
this.counter++;
this.parentData = { value: 'updated' };
}
}
Child to Parent: @Output and EventEmitter
Child component:
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',
template: `
<button (click)="sendMessage()">Send to Parent</button>
<button (click)="incrementCounter()">Increment</button>
`
})
export class ChildComponent {
// Basic event emitter
@Output() messageEvent = new EventEmitter<string>();
// With alias
@Output('counterChange') countChanged = new EventEmitter<number>();
// Event with object data
@Output() userEvent = new EventEmitter<{name: string, action: string}>();
private count = 0;
sendMessage() {
this.messageEvent.emit('Hello from child');
}
incrementCounter() {
this.count++;
this.countChanged.emit(this.count);
}
sendUserAction(action: string) {
this.userEvent.emit({name: 'User', action: action});
}
}
Parent component:
@Component({
selector: 'app-parent',
template: `
<p>Message from child: {{ message }}</p>
<p>Counter value: {{ counter }}</p>
<p>User action: {{ userAction }}</p>
<app-child
(messageEvent)="receiveMessage($event)"
(counterChange)="updateCounter($event)"
(userEvent)="handleUserEvent($event)">
</app-child>
`
})
export class ParentComponent {
message = '';
counter = 0;
userAction = '';
receiveMessage(msg: string) {
this.message = msg;
}
updateCounter(count: number) {
this.counter = count;
}
handleUserEvent(event: {name: string, action: string}) {
this.userAction = `${event.name} performed ${event.action}`;
}
}
Two-way Binding Custom Properties
Child component:
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<button (click)="decrement()">-</button>
<span>{{ value }}</span>
<button (click)="increment()">+</button>
</div>
`
})
export class CounterComponent {
@Input() value: number = 0;
@Output() valueChange = new EventEmitter<number>();
increment() {
this.value++;
this.valueChange.emit(this.value);
}
decrement() {
this.value--;
this.valueChange.emit(this.value);
}
}
Parent component:
@Component({
selector: 'app-parent',
template: `
<p>Counter: {{ count }}</p>
<app-counter [(value)]="count"></app-counter>
`
})
export class ParentComponent {
count = 0;
}
Parent-Child Interaction via Template Reference
Parent template:
<div>
<app-child #childComponent></app-child>
<button (click)="childComponent.doSomething()">Call Child Method</button>
</div>
Child component:
@Component({
selector: 'app-child',
template: `<p>Child component</p>`
})
export class ChildComponent {
doSomething() {
console.log('Child method called from parent');
}
}
Accessing Child Components with @ViewChild
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
template: `
<app-child></app-child>
<button (click)="callChildMethod()">Call Child Method</button>
`
})
export class ParentComponent implements AfterViewInit {
@ViewChild(ChildComponent) childComponent!: ChildComponent;
// With template reference variable
@ViewChild('childRef') childWithRef!: ChildComponent;
// Static query - available in ngOnInit
@ViewChild(ChildComponent, { static: true })
childComponentStatic!: ChildComponent;
ngAfterViewInit() {
// Access child after view is initialized
this.childComponent.doSomething();
}
callChildMethod() {
this.childComponent.doSomething();
}
}
Accessing Multiple Children with @ViewChildren
import { Component, ViewChildren, QueryList, AfterViewInit } from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
template: `
<app-child *ngFor="let i of [1,2,3]"></app-child>
<button (click)="callAllChildMethods()">Call All Child Methods</button>
`
})
export class ParentComponent implements AfterViewInit {
@ViewChildren(ChildComponent)
childrenComponents!: QueryList<ChildComponent>;
ngAfterViewInit() {
this.childrenComponents.forEach(child => {
child.doSomething();
});
}
callAllChildMethods() {
this.childrenComponents.forEach(child => {
child.doSomething();
});
}
}
Content Projection (ng-content)
Child component (with content projection):
@Component({
selector: 'app-card',
template: `
<div class="card">
<div class="card-header">
<ng-content select="[card-header]"></ng-content>
</div>
<div class="card-body">
<ng-content></ng-content>
</div>
<div class="card-footer">
<ng-content select="[card-footer]"></ng-content>
</div>
</div>
`,
styles: [`
.card {
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
}
.card-header {
background-color: #f5f5f5;
padding: 10px;
border-bottom: 1px solid #ccc;
}
.card-body {
padding: 10px;
}
.card-footer {
background-color: #f5f5f5;
padding: 10px;
border-top: 1px solid #ccc;
}
`]
})
export class CardComponent { }
Parent component (using content projection):
<app-card>
<div card-header>
<h3>Card Title</h3>
</div>
<p>This is the main content of the card.</p>
<p>Multiple elements can be projected.</p>
<div card-footer>
<button>Action</button>
</div>
</app-card>
Accessing Projected Content with @ContentChild
import { Component, ContentChild, AfterContentInit } from '@angular/core';
import { ProjectedComponent } from './projected.component';
@Component({
selector: 'app-wrapper',
template: `
<div class="wrapper">
<ng-content></ng-content>
</div>
<button (click)="accessProjectedContent()">
Access Projected Content
</button>
`
})
export class WrapperComponent implements AfterContentInit {
@ContentChild(ProjectedComponent)
projectedComponent!: ProjectedComponent;
// With template reference variable
@ContentChild('projectedRef')
projectedComponentWithRef!: ProjectedComponent;
ngAfterContentInit() {
// Access projected content after it's initialized
if (this.projectedComponent) {
console.log('Projected content initialized');
this.projectedComponent.doSomething();
}
}
accessProjectedContent() {
if (this.projectedComponent) {
this.projectedComponent.doSomething();
}
}
}
Parent component:
<app-wrapper>
<app-projected #projectedRef></app-projected>
</app-wrapper>
Accessing Multiple Projected Contents with @ContentChildren
import { Component, ContentChildren, QueryList, AfterContentInit } from '@angular/core';
import { ProjectedItemComponent } from './projected-item.component';
@Component({
selector: 'app-list-wrapper',
template: `
<div class="list-wrapper">
<ng-content></ng-content>
</div>
<button (click)="logAllItems()">Log All Items</button>
`
})
export class ListWrapperComponent implements AfterContentInit {
@ContentChildren(ProjectedItemComponent)
projectedItems!: QueryList<ProjectedItemComponent>;
ngAfterContentInit() {
console.log(`Projected ${this.projectedItems.length} items`);
this.projectedItems.forEach(item => {
console.log(item.data);
});
}
logAllItems() {
this.projectedItems.forEach(item => {
console.log(item.data);
});
}
}
Parent component:
<app-list-wrapper>
<app-projected-item [data]="'Item 1'"></app-projected-item>
<app-projected-item [data]="'Item 2'"></app-projected-item>
<app-projected-item [data]="'Item 3'"></app-projected-item>
</app-list-wrapper>
Component Styles
Style Encapsulation Modes
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.css'],
// ViewEncapsulation options:
// Emulated (default) - styles are scoped to component
encapsulation: ViewEncapsulation.Emulated,
// None - styles leak out to global scope
// encapsulation: ViewEncapsulation.None,
// ShadowDom - uses browser's native Shadow DOM
// encapsulation: ViewEncapsulation.ShadowDom
})
Special Selectors
/* styles.css (component stylesheet) */
/* :host - targets the element hosting the component */
:host {
display: block;
margin: 10px;
}
/* :host with condition */
:host(.active) {
border: 2px solid blue;
}
/* :host-context - applies styles when some ancestor has a CSS class */
:host-context(.theme-dark) {
background-color: #333;
color: white;
}
/* ::ng-deep - forces styles to apply to child components */
/* Use carefully - will be deprecated eventually */
:host ::ng-deep .child-element {
color: red;
}
Dynamic Components
Creating Components Dynamically
import {
Component, ViewChild, ViewContainerRef,
ComponentFactoryResolver, ComponentRef
} from '@angular/core';
import { DynamicComponent } from './dynamic.component';
@Component({
selector: 'app-dynamic-host',
template: `
<div>
<ng-container #container></ng-container>
<button (click)="createComponent()">Create Component</button>
<button (click)="removeComponent()">Remove Component</button>
</div>
`
})
export class DynamicHostComponent {
@ViewChild('container', { read: ViewContainerRef })
container!: ViewContainerRef;
private componentRef: ComponentRef<DynamicComponent> | null = null;
constructor(private cfr: ComponentFactoryResolver) { }
createComponent() {
// Clear container first
this.container.clear();
// Create component factory
const factory = this.cfr.resolveComponentFactory(DynamicComponent);
// Create component
this.componentRef = this.container.createComponent(factory);
// Set inputs
this.componentRef.instance.data = 'Dynamic data';
// Subscribe to outputs
this.componentRef.instance.action.subscribe((data: string) => {
console.log('Dynamic component action:', data);
});
}
removeComponent() {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
}
}
Alternative Approach in Angular 13+
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
import { DynamicComponent } from './dynamic.component';
@Component({
selector: 'app-dynamic-host',
template: `
<div>
<ng-container #container></ng-container>
<button (click)="createComponent()">Create Component</button>
<button (click)="removeComponent()">Remove Component</button>
</div>
`
})
export class DynamicHostComponent {
@ViewChild('container', { read: ViewContainerRef })
container!: ViewContainerRef;
private componentRef: any = null;
createComponent() {
// Clear container first
this.container.clear();
// Create component directly (Angular 13+)
this.componentRef = this.container.createComponent(DynamicComponent);
// Set inputs
this.componentRef.instance.data = 'Dynamic data';
// Subscribe to outputs
this.componentRef.instance.action.subscribe((data: string) => {
console.log('Dynamic component action:', data);
});
}
removeComponent() {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = null;
}
}
}
Standalone Components (Angular 14+)
Creating a Standalone Component
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AnotherStandaloneComponent } from './another-standalone.component';
@Component({
selector: 'app-standalone',
standalone: true,
imports: [
CommonModule,
AnotherStandaloneComponent
],
template: `
<h1>{{ title }}</h1>
<app-another-standalone></app-another-standalone>
`
})
export class StandaloneComponent {
title = 'I am a standalone component';
}
Using Standalone Components in NgModule
import { NgModule } from '@angular/core';
import { StandaloneComponent } from './standalone.component';
@NgModule({
imports: [
StandaloneComponent
],
exports: [
StandaloneComponent
]
})
export class FeatureModule { }
Bootstrapping with Standalone Component
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { provideRouter } from '@angular/router';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes)
]
}).catch(err => console.error(err));
Change Detection
OnPush Change Detection
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-optimized',
templateUrl: './optimized.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
@Input() data: any;
// Component is re-rendered only when:
// 1. @Input reference changes (not properties inside the object)
// 2. Event originates from the component or its children
// 3. Using async pipe which emits new value
// 4. Manually triggering change detection
}
Manually Triggering Change Detection
import { Component, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-optimized',
templateUrl: './optimized.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
data = { value: 'initial' };
constructor(private cdr: ChangeDetectorRef) {}
updateWithoutDetection() {
// This won't update the view with OnPush
this.data.value = 'updated';
}
updateWithDetection() {
// Update data
this.data.value = 'updated';
// Manually mark for check
this.cdr.markForCheck();
}
forceDetection() {
// Force a change detection cycle
this.cdr.detectChanges();
}
detachAndReattach() {
// Detach from change detection
this.cdr.detach();
// Make changes
this.data.value = 'updated while detached';
// Reattach to change detection
this.cdr.reattach();
}
}
Component Templates
Structural Directives in Components
@Component({
selector: 'app-conditional',
template: `
<!-- ngIf -->
<div *ngIf="isVisible; else notVisible">Content is visible</div>
<ng-template #notVisible>Content is not visible</ng-template>
<!-- ngFor -->
<ul>
<li *ngFor="let item of items; let i = index; trackBy: trackById">
{{ i }}: {{ item.name }}
</li>
</ul>
<!-- ngSwitch -->
<div [ngSwitch]="status">
<div *ngSwitchCase="'active'">Active content</div>
<div *ngSwitchCase="'inactive'">Inactive content</div>
<div *ngSwitchDefault>Unknown status</div>
</div>
`
})
export class ConditionalComponent {
isVisible = true;
items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
];
status = 'active';
trackById(index: number, item: any): number {
return item.id;
}
}
Template Reference Variables
@Component({
selector: 'app-template-refs',
template: `
<!-- Reference to DOM element -->
<input #nameInput type="text">
<button (click)="greet(nameInput.value)">Greet</button>
<!-- Reference to component -->
<app-child #childComp></app-child>
<button (click)="childComp.doSomething()">Call Child Method</button>
<!-- Reference to directive -->
<div #ngIfRef *ngIf="show">This is conditionally shown</div>
<button (click)="show = !show">Toggle</button>
`
})
export class TemplateRefsComponent {
show = true;
greet(name: string) {
alert(`Hello, ${name}!`);
}
}
Using ng-template and ng-container
@Component({
selector: 'app-templates',
template: `
<!-- ng-template - template that doesn't render by default -->
<ng-template #myTemplate>
<p>Template content</p>
</ng-template>
<!-- Use template programmatically -->
<button (click)="showTemplate()">Show Template</button>
<!-- Container to hold template content -->
<ng-container #container></ng-container>
<!-- ng-container with structural directive -->
<ng-container *ngIf="isVisible">
<h2>Title</h2>
<p>Description</p>
</ng-container>
<!-- Cleaner ngFor with ng-container -->
<table>
<tbody>
<ng-container *ngFor="let item of items">
<tr>
<td>{{ item.name }}</td>
</tr>
<tr>
<td>{{ item.description }}</td>
</tr>
</ng-container>
</tbody>
</table>
`
})
export class TemplatesComponent implements AfterViewInit {
@ViewChild('myTemplate') myTemplate!: TemplateRef<any>;
@ViewChild('container', { read: ViewContainerRef }) container!: ViewContainerRef;
isVisible = true;
items = [
{ name: 'Item 1', description: 'Description 1' },
{ name: 'Item 2', description: 'Description 2' }
];
ngAfterViewInit() {
// Template is not rendered initially
}
showTemplate() {
this.container.clear();
this.container.createEmbeddedView(this.myTemplate);
}
}
Contextual Template Variables
@Component({
selector: 'app-context',
template: `
<!-- Template with context -->
<ng-template #greet let-name="name" let-message="message">
<h3>Hello, {{ name }}!</h3>
<p>{{ message }}</p>
</ng-template>
<!-- Using template with ngTemplateOutlet -->
<ng-container
[ngTemplateOutlet]="greet"
[ngTemplateOutletContext]="{ name: 'John', message: 'Welcome!' }">
</ng-container>
<!-- Using it again with different context -->
<ng-container
[ngTemplateOutlet]="greet"
[ngTemplateOutletContext]="{ name: 'Jane', message: 'Nice to see you!' }">
</ng-container>
`
})
export class ContextComponent { }
Component Best Practices
Component Organization
src/app/
├── core/ # Singleton services, app-level components
│ ├── header/
│ ├── footer/
│ └── core.module.ts
├── shared/ # Reusable components, directives, pipes
│ ├── components/
│ ├── directives/
│ ├── pipes/
│ └── shared.module.ts
└── features/ # Feature modules and components
├── feature1/
│ ├── components/
│ ├── services/
│ └── feature1.module.ts
└── feature2/
Component Design Guidelines
- Single Responsibility: Each component should do one thing well
- Small Components: Keep components small and focused
- Smart vs. Presentational:
- Smart components: Manage state, fetch data, contain logic
- Presentational components: Display data, emit events
- OnPush Change Detection: Use for better performance
- Proper Unsubscription: Clean up subscriptions in ngOnDestroy
- Use Interfaces: Define interfaces for input and output data
- Component Selectors: Use prefix and meaningful names
- Avoid Logic in Templates: Keep complex logic in component class
- Avoid Direct DOM Manipulation: Use Angular’s binding system
- Input Setters for Side Effects: Use setters for reacting to input changes
Performance Tips
- Use trackBy with ngFor:
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }}
</div>
trackByFn(index: number, item: any): number {
return item.id; // Unique identifier
}
OnPush Change Detection
Pure Pipes instead of Computed Properties:
// Instead of a getter
get filteredItems() {
return this.items.filter(item => item.active);
}
// Use a pure pipe
@Pipe({
name: 'filterItems',
pure: true
})
export class FilterItemsPipe implements PipeTransform {
transform(items: any[], isActive: boolean): any[] {
return items.filter(item => item.active === isActive);
}
}
Lazy Load Components:
- Use lazy loading with RouterModule for feature modules
- Consider using dynamic imports for standalone components
Use async pipe:
<div *ngFor="let item of items$ | async">
{{ item.name }}
</div>