Angular Component Basics
Angular components serve as UI building blocks similar to React components, but with significantly different structure and mechanisms.
1. Basic Component Structure
An Angular component is a TypeScript class decorated with @Component.
@Component Decorator
import { Component } from '@angular/core';
@Component({
selector: 'app-hello', // HTML tag name for this component
templateUrl: './hello.component.html', // Template file
styleUrl: './hello.component.scss', // Style file
})
export class HelloComponent {
name = 'Angular';
}
| Property | Description |
|---|---|
selector | The HTML tag name used in templates (e.g., <app-hello>) |
templateUrl | Path to the HTML template file |
template | Inline HTML (mutually exclusive with templateUrl) |
styleUrl | Path to the style file (Angular 17+, singular form) |
styleUrls | Array of style file paths |
styles | Array of inline styles |
standalone | When true, usable without NgModule (default since Angular 17) |
File Structure: 3-file Split vs JSX All-in-One
By default, Angular splits one component into 3 files.
hello/
├── hello.component.ts # Logic (TypeScript)
├── hello.component.html # Template (HTML)
└── hello.component.scss # Styles (SCSS)
React typically uses a single file (JSX), but Angular's separation makes the concerns more explicit.
// React: All-in-one JSX
export function Hello() {
const name = 'React';
return <h1>Hello, {name}!</h1>;
}
<!-- Angular template (hello.component.html) -->
<h1>Hello, {{ name }}!</h1>
Generating Components with ng generate
# Generate a component (creates 3 files + spec file)
ng generate component hello
# Or shorthand
ng g c hello
# Generate in a subdirectory
ng g c features/user-profile
# Generate with inline template and inline style
ng g c hello --inline-template --inline-style
# Generate without spec file
ng g c hello --skip-tests
2. Templates and Inline Templates
There are two ways to define a template: external file or inline.
templateUrl (External File)
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html', // Reference external HTML file
styleUrl: './user-card.component.scss',
})
export class UserCardComponent { }
Keeping the HTML in a separate file gives better editor support and is recommended for larger templates.
template (Inline)
@Component({
selector: 'app-badge',
template: `
<span class="badge">{{ label }}</span>
`,
styles: [`
.badge { padding: 4px 8px; border-radius: 4px; }
`],
})
export class BadgeComponent {
label = 'New';
}
Inline templates are convenient for small, simple components.
| Feature | Angular Template | React JSX |
|---|---|---|
| Variable interpolation | {{ value }} | {value} |
| Conditional rendering | @if (cond) { } / *ngIf | {cond && <...>} |
| List rendering | @for (item of items; track item.id) { } / *ngFor | {items.map(...)} |
| Class binding | [class.active]="isActive" | className={isActive ? 'active' : ''} |
3. The Four Forms of Data Binding
Angular has four types of data binding.
3-1. Interpolation: {{ }}
Displays values in the template.
@Component({
selector: 'app-greeting',
template: `
<p>Hello, {{ username }}!</p>
<p>Today is {{ today | date:'yyyy/MM/dd' }}</p>
<p>Total: {{ price * quantity }}</p>
`,
})
export class GreetingComponent {
username = 'Taro';
today = new Date();
price = 1000;
quantity = 3;
}
3-2. Property Binding: [property]="value"
Binds a value to a DOM element or component property (TypeScript → HTML).
@Component({
selector: 'app-image',
template: `
<!-- Binding to DOM properties -->
<img [src]="imageUrl" [alt]="imageAlt" />
<button [disabled]="isLoading">Submit</button>
<input [value]="inputValue" />
<!-- Special class and style bindings -->
<div [class.highlight]="isSelected">Selected</div>
<p [style.color]="textColor">Colored text</p>
`,
})
export class ImageComponent {
imageUrl = 'https://example.com/photo.jpg';
imageAlt = 'Sample image';
isLoading = false;
inputValue = 'initial value';
isSelected = true;
textColor = 'blue';
}
// React: Write expressions directly in JSX attributes
<img src={imageUrl} alt={imageAlt} />
<button disabled={isLoading}>Submit</button>
Angular's [src] is equivalent to React's src={...}.
3-3. Event Binding: (event)="handler()"
Calls methods in response to user actions (HTML → TypeScript).
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count }}</p>
<button (click)="increment()">+1</button>
<button (click)="decrement()">-1</button>
<input (input)="onInput($event)" (keydown.enter)="submit()" />
`,
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
onInput(event: Event) {
const value = (event.target as HTMLInputElement).value;
console.log(value);
}
submit() {
console.log('Submitted');
}
}
// React: Pass handlers via onXxx attributes
<button onClick={increment}>+1</button>
<input onChange={e => console.log(e.target.value)} />
Angular's (click) is equivalent to React's onClick.
3-4. Two-way Binding: [(ngModel)]="value"
Simultaneously reads and writes a value, ideal for form inputs.
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-text-input',
imports: [FormsModule], // Required for standalone components
template: `
<input [(ngModel)]="message" />
<p>Input value: {{ message }}</p>
`,
})
export class TextInputComponent {
message = '';
}
[(ngModel)] is called the "banana in a box" syntax. It is syntactic sugar combining [ngModel] (property binding for display) and (ngModelChange) (event binding for updates).
// React: Controlled component using useState + onChange
const [message, setMessage] = useState('');
<input value={message} onChange={e => setMessage(e.target.value)} />
4. Input and Output (@Input / @Output)
@Input: Passing Data from Parent to Child
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `
<div class="card">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
</div>
`,
})
export class UserCardComponent {
@Input() name!: string;
@Input() email!: string;
@Input({ required: true }) userId!: number; // Required input (Angular 16+)
@Input() role = 'user'; // With default value
}
<app-user-card
[name]="user.name"
[email]="user.email"
[userId]="user.id"
role="admin"
/>
@Output: Sending Events from Child to Parent
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-confirm-dialog',
template: `
<div class="dialog">
<p>Are you sure you want to delete?</p>
<button (click)="onConfirm()">Yes</button>
<button (click)="onCancel()">Cancel</button>
</div>
`,
})
export class ConfirmDialogComponent {
@Output() confirmed = new EventEmitter<void>();
@Output() cancelled = new EventEmitter<void>();
onConfirm() {
this.confirmed.emit();
}
onCancel() {
this.cancelled.emit();
}
}
<app-confirm-dialog
(confirmed)="deleteItem()"
(cancelled)="closeDialog()"
/>
| Angular | React |
|---|---|
@Input() value | props.value |
@Input({ required: true }) | Required via TypeScript type |
@Output() clicked = new EventEmitter() | onClicked?: () => void prop |
this.clicked.emit(data) | props.onClicked(data) |
Signal Input / Output (Angular 17.1+)
Starting with Angular 17.1, a new Signal-based API for inputs and outputs is available.
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-product-card',
template: `
<div>
<h3>{{ name() }}</h3>
<p>{{ price() }}</p>
<button (click)="buy()">Buy</button>
</div>
`,
})
export class ProductCardComponent {
// signal input (read as a Signal function call)
name = input.required<string>();
price = input<number>(0);
// output function (replacement for EventEmitter)
purchased = output<{ name: string; price: number }>();
buy() {
this.purchased.emit({ name: this.name(), price: this.price() });
}
}
Benefits of Signal Input:
- No need for the
!non-null assertion - Treated as a Signal in templates, enabling optimized change detection
- Supports reactive operations like
computed()
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-price-display',
template: `<p>Tax included: {{ priceWithTax() }}</p>`,
})
export class PriceDisplayComponent {
price = input<number>(0);
taxRate = input<number>(0.1);
// Derive a value from Signals using computed()
priceWithTax = computed(() => Math.floor(this.price() * (1 + this.taxRate())));
}
5. Content Projection
ng-content allows callers of a component to inject content into it, similar to React's children prop.
Basic Content Projection
import { Component } from '@angular/core';
@Component({
selector: 'app-card',
template: `
<div class="card">
<ng-content></ng-content>
</div>
`,
styles: [`.card { border: 1px solid #ccc; padding: 16px; border-radius: 8px; }`],
})
export class CardComponent {}
<app-card>
<h2>Title</h2>
<p>This content is projected into ng-content.</p>
</app-card>
Multiple Slots (Named Content Projection)
You can distribute content to multiple slots using CSS selectors in the select attribute.
@Component({
selector: 'app-panel',
template: `
<div class="panel">
<div class="panel-header">
<ng-content select="[slot=header]"></ng-content>
</div>
<div class="panel-body">
<ng-content></ng-content>
</div>
<div class="panel-footer">
<ng-content select="[slot=footer]"></ng-content>
</div>
</div>
`,
})
export class PanelComponent {}
<app-panel>
<h2 slot="header">Panel Title</h2>
<p>Main body content</p>
<button slot="footer">Close</button>
</app-panel>
// React: children prop
function Card({ children }: { children: React.ReactNode }) {
return <div className="card">{children}</div>;
}
// React: Multiple slots (passed as separate props)
function Panel({ header, children, footer }: PanelProps) {
return (
<div>
<div>{header}</div>
<div>{children}</div>
<div>{footer}</div>
</div>
);
}
6. Style Scoping
Angular component styles are scoped by default, meaning they do not affect other components. This behavior is controlled by ViewEncapsulation.
The Three ViewEncapsulation Modes
import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-example',
template: `<p class="text">Sample</p>`,
styles: [`.text { color: red; }`],
encapsulation: ViewEncapsulation.Emulated, // Default
})
export class ExampleComponent {}
| Mode | Description | Use Case |
|---|---|---|
ViewEncapsulation.Emulated (default) | Angular adds unique attributes (e.g., _ngcontent-xxx) to scope CSS | Normal development |
ViewEncapsulation.ShadowDom | Uses the browser's native Shadow DOM for scoping | When complete isolation is required |
ViewEncapsulation.None | No scoping (styles treated as global CSS) | When intentionally defining global styles |
How Emulated Mode Works
<!-- Angular renders this: -->
<p _ngcontent-abc-c123 class="text">Sample</p>
/* Generated CSS (scoped) */
.text[_ngcontent-abc-c123] { color: red; }
:host Selector
To apply styles to the component's host element (the <app-example> tag itself), use the :host selector.
/* Apply styles to the component host element */
:host {
display: block;
padding: 16px;
}
:host(.active) {
background-color: #e0f7fa;
}
| Angular | React |
|---|---|
ViewEncapsulation.Emulated (default) | CSS Modules (import styles from './Component.module.css') |
ViewEncapsulation.None | Global CSS / createGlobalStyle (styled-components) |
ViewEncapsulation.ShadowDom | Web Components + Shadow DOM |
:host | Direct styling of the component root element |
React requires explicitly choosing a tool (CSS Modules, styled-components, etc.), while Angular has scoping built in by default.