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>
