DEV Community

Atilla Baspinar
Atilla Baspinar

Posted on

Core Concepts


1. Template Member Access

Any public member of the class can be used in the template. Private members are not accessible and will cause a TypeScript error in strict mode.


2. Standalone Components

Standalone components (standalone: true) are the default since Angular 17. A component does not need to be declared inside an NgModule.


3. String Interpolation

Output any member value as text using double curly braces:

<p>{{ username }}</p>
Enter fullscreen mode Exit fullscreen mode

4. Property Binding

Binds an expression to a DOM element property (not an HTML attribute):

<img [src]="imageUrl" />
<button [disabled]="isLoading">Submit</button>
Enter fullscreen mode Exit fullscreen mode

5. Attribute Binding

A separate mechanism used when no corresponding DOM property exists for an HTML attribute (e.g. ARIA attributes, colspan):

<td [attr.colspan]="colSpan"></td>
<button [attr.aria-label]="label">Click</button>
Enter fullscreen mode Exit fullscreen mode

6. @Input() Decorator

The old-style @Input() decorator relies on Zone.js-based change detection (dirty checking). Angular wraps browser events via Zone.js to know when to re-run change detection.

@Input() user: User;
Enter fullscreen mode Exit fullscreen mode

7. Signals

The new reactive primitive. Declare a signal and call it as a function to read its value — both in the template and inside the class:

// in the class
selectedUser = signal(DUMMY_USERS[0]);

// read inside the class
const name = this.selectedUser().name;
Enter fullscreen mode Exit fullscreen mode
<!-- in the template -->
<p>{{ selectedUser().name }}</p>
Enter fullscreen mode Exit fullscreen mode

8. Computed Signals

Use computed() (not compute) for derived values. A computed signal re-evaluates automatically when its dependencies change:

import { computed, signal } from '@angular/core';

selectedUser = signal(DUMMY_USERS[0]);
username = computed(() => this.selectedUser().name);

// read it like any other signal
const name = this.username();
Enter fullscreen mode Exit fullscreen mode
<p>{{ username() }}</p>
Enter fullscreen mode Exit fullscreen mode

9. Signal Inputs — input()

Replaces @Input(). Signal inputs are read-only — you cannot call .set() on them:

// optional with a default value
user = input('default');

// required (throws if not provided by parent)
user = input.required<User>();

// equivalent old-style decorator
@Input({ required: true }) user!: User;
Enter fullscreen mode Exit fullscreen mode

Because signal inputs are read-only, this.user.set(value) is not allowed.


10. @Output() Decorator

Lets a child component notify its parent by emitting events through an EventEmitter.

Child component:

import { Component, Output, EventEmitter } from '@angular/core';

@Output() userSelected = new EventEmitter<User>();
selectedUser: User = DUMMY_USERS[0];

onSelectUser() {
  this.userSelected.emit(this.selectedUser);
}
Enter fullscreen mode Exit fullscreen mode

Child template:

<button (click)="onSelectUser()">Select User</button>
Enter fullscreen mode Exit fullscreen mode

Parent template (listen to the event with $event to receive the emitted value):

<app-user (userSelected)="selectUser($event)" />
Enter fullscreen mode Exit fullscreen mode

11. output() Function

The decorator-free replacement for @Output() + EventEmitter (Angular 17.3+). It is not a signal — it returns an OutputEmitterRef<T>, which is emit-only and cannot be read like a signal. It belongs to the same decorator-free API family as input() and model().

Child component:

import { Component, output, signal } from '@angular/core';

selectedUser = signal(DUMMY_USERS[0]);
userSelected = output<User>();  // OutputEmitterRef<User>, not a Signal

onSelectUser() {
  this.userSelected.emit(this.selectedUser());
}
Enter fullscreen mode Exit fullscreen mode

Child template:

<button (click)="onSelectUser()">Select User</button>
Enter fullscreen mode Exit fullscreen mode

Parent template (identical to @Output() usage):

<app-user (userSelected)="selectUser($event)" />
Enter fullscreen mode Exit fullscreen mode

Key differences from @Output(): no EventEmitter, no decorator. Unlike input(), you cannot call this.userSelected() to read a value — it only has .emit().


12. @for — List Rendering

Built-in control flow for rendering lists (Angular 17+). track is required and tells Angular how to identify each item across re-renders — similar to key in React. Prefer tracking by a unique id over $index when available.

<ul>
  @for (user of users; track user.id) {
    <li>{{ user.name }}</li>
  }
</ul>
Enter fullscreen mode Exit fullscreen mode

@for also exposes implicit variables: $index, $first, $last, $even, $odd.

@for (user of users; track user.id) {
  <li [class.last]="$last">{{ $index + 1 }}. {{ user.name }}</li>
}
Enter fullscreen mode Exit fullscreen mode

@empty — fallback when the list is empty

Add an @empty block directly after the @for block to render fallback content when the collection is empty or null/undefined. It requires no extra *ngIf check on the list.

@for (user of users; track user.id) {
  <li>{{ user.name }}</li>
} @empty {
  <li>No users found.</li>
}
Enter fullscreen mode Exit fullscreen mode

Before Angular 17*ngFor structural directive (must be imported from CommonModule or NgFor):

<li *ngFor="let user of users; trackBy: trackById">{{ user.name }}</li>
Enter fullscreen mode Exit fullscreen mode
trackById(index: number, user: User) {
  return user.id;
}
Enter fullscreen mode Exit fullscreen mode

13. @if / @else — Conditional Rendering

Built-in control flow for conditional rendering (Angular 17+):

@if (isLoggedIn) {
  <p>Welcome back, {{ username() }}!</p>
} @else {
  <p>Please log in.</p>
}
Enter fullscreen mode Exit fullscreen mode

You can also chain @else if:

@if (status === 'loading') {
  <p>Loading...</p>
} @else if (status === 'error') {
  <p>Something went wrong.</p>
} @else {
  <p>{{ data }}</p>
}
Enter fullscreen mode Exit fullscreen mode

Before Angular 17*ngIf structural directive (must be imported from CommonModule or NgIf). The else branch requires an <ng-template> with a reference variable:

<p *ngIf="isLoggedIn; else loggedOut">Welcome back!</p>
<ng-template #loggedOut><p>Please log in.</p></ng-template>
Enter fullscreen mode Exit fullscreen mode

14. Class & Style Binding

Conditionally apply a CSS class based on a boolean expression:

<!-- single class -->
<div [class.active]="isActive"></div>

<!-- multiple classes via object — keys are class names, values are conditions -->
<div [ngClass]="{ active: isActive, disabled: isDisabled }"></div>
Enter fullscreen mode Exit fullscreen mode

For inline styles, use [style.property] or [ngStyle]:

<div [style.color]="isError ? 'red' : 'black'"></div>
Enter fullscreen mode Exit fullscreen mode

15. Two-Way Binding — ngModel

Import FormsModule in the component's imports array, then use [(ngModel)] on an input. Each input must also have a name attribute for FormsModule to track it.

import { FormsModule } from '@angular/forms';

@Component({
  imports: [FormsModule],
  ...
})
export class MyComponent {
  title = '';
}
Enter fullscreen mode Exit fullscreen mode
<input [(ngModel)]="title" name="title" />
Enter fullscreen mode Exit fullscreen mode

With a writable signal, [(ngModel)] does not work correctly — even though the template compiles without error (because ngModel accepts any). At runtime, [(ngModel)]="title" desugars to [ngModel]="title" + (ngModelChange)="title=$event", which passes the signal function itself to ngModel and then replaces the signal with a plain string on the first change event, destroying it. Use the split form instead:

<input [ngModel]="title()" (ngModelChange)="title.set($event)" name="title" />
Enter fullscreen mode Exit fullscreen mode

Alternatively, use model() (Angular 17.2+) which natively supports two-way binding:

title = model('');
Enter fullscreen mode Exit fullscreen mode
<input [(ngModel)]="title" name="title" />
Enter fullscreen mode Exit fullscreen mode

Setting up custom two-way binding

Two-way binding on a custom component means the parent can both pass a value in and receive updates when the child changes it. There are two ways to set this up.

Option A — @Input() + @Output() (traditional)

The output name must be the input name suffixed with Change. This is the convention Angular uses to desugar [(value)] into [value] + (valueChange).

// child component
@Component({ selector: 'app-rating' })
export class RatingComponent {
  @Input() rating = 0;
  @Output() ratingChange = new EventEmitter<number>();

  setRating(value: number) {
    this.rating = value;
    this.ratingChange.emit(value);
  }
}
Enter fullscreen mode Exit fullscreen mode
<!-- parent template -->
<app-rating [(rating)]="userRating" />
Enter fullscreen mode Exit fullscreen mode

[(rating)] desugars to [rating]="userRating" (ratingChange)="userRating = $event".

Option B — model() (Angular 17.2+)

model() replaces the @Input() + @Output() pair with a single writable signal. The child updates it with .set(), and Angular syncs the new value back to the parent automatically.

// child component
import { model } from '@angular/core';

@Component({ selector: 'app-rating' })
export class RatingComponent {
  rating = model(0); // writable signal, two-way bindable

  setRating(value: number) {
    this.rating.set(value); // notifies the parent automatically
  }
}
Enter fullscreen mode Exit fullscreen mode
<!-- parent template — identical syntax to Option A -->
<app-rating [(rating)]="userRating" />
Enter fullscreen mode Exit fullscreen mode

The parent's userRating updates whenever the child calls this.rating.set(). Unlike input(), model() is writable — .set() and .update() are available inside the child.


16. Form Submission — ngSubmit

FormsModule enhances <form> elements. Bind (ngSubmit) to handle submission without a page reload.

Child component:

import { Component, output } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  imports: [FormsModule],
  ...
})
export class CreateUserComponent {
  title = '';
  name = '';
  email = '';

  createUser = output<{ title: string; name: string; email: string }>();

  onSubmit() {
    this.createUser.emit({
      title: this.title,
      name: this.name,
      email: this.email,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Child template:

<form (ngSubmit)="onSubmit()">
  <input [(ngModel)]="title" name="title" />
  <input [(ngModel)]="name" name="name" />
  <input [(ngModel)]="email" name="email" />
  <button type="submit">Create</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Parent template:

<app-create-user (createUser)="onCreate($event)" />
Enter fullscreen mode Exit fullscreen mode

17. Content Projection — <ng-content>

Similar to children in React. Place <ng-content /> in a child component's template as a slot; whatever is placed between the child's opening and closing tags in the parent gets projected into that slot.

Card component template:

<div class="card">
  <h2>Card Title</h2>
  <ng-content />
</div>
Enter fullscreen mode Exit fullscreen mode

Parent template:

<app-card>
  <p>This paragraph is projected into the card.</p>
</app-card>
Enter fullscreen mode Exit fullscreen mode

Multiple named slots — use the select attribute to target specific elements by CSS selector:

<div class="card">
  <div class="card-header">
    <ng-content select="[card-header]" />
  </div>
  <div class="card-body">
    <ng-content select="[card-body]" />
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
<app-card>
  <h2 card-header>My Title</h2>
  <p card-body>My content goes here.</p>
</app-card>
Enter fullscreen mode Exit fullscreen mode

18. Services & Dependency Injection

A service decorated with @Injectable({ providedIn: 'root' }) is instantiated once and shared across the entire app (singleton). Angular's DI system injects the same instance wherever it is requested.

Service:

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class TasksService {
  private tasks = ['Task 1', 'Task 2'];

  getTasks() {
    return this.tasks;
  }
}
Enter fullscreen mode Exit fullscreen mode

Constructor injection (traditional style):

export class TasksComponent {
  constructor(private taskService: TasksService) {}

  getTasks() {
    return this.taskService.getTasks();
  }
}
Enter fullscreen mode Exit fullscreen mode

inject() function (Angular 14+, preferred in modern Angular):

import { inject } from '@angular/core';

export class TasksComponent {
  private taskService = inject(TasksService);

  getTasks() {
    return this.taskService.getTasks();
  }
}
Enter fullscreen mode Exit fullscreen mode

inject() is preferred in modern code because it works in field initializers (no constructor needed), plays well with signals, and can be used in standalone functions (e.g. route guards, interceptors) where a constructor is not available.

Component-scoped service — provide a service in the component's providers array to get a fresh instance per component (not a singleton):

@Component({
  providers: [TasksService],
  ...
})
export class TasksComponent { ... }
Enter fullscreen mode Exit fullscreen mode

19. Pipes — Transforming Template Output

Pipes transform a value directly in the template using the | operator. They do not mutate the original value.

<p>{{ username | uppercase }}</p>
<p>{{ dueDate | date:'mediumDate' }}</p>
<p>{{ price | currency:'EUR' }}</p>
Enter fullscreen mode Exit fullscreen mode

Passing parameters — add :param after the pipe name. Chain multiple params with more colons:

<p>{{ 3.14159 | number:'1.0-2' }}</p>   <!-- "3.14" -->
<p>{{ dueDate | date:'yyyy-MM-dd' }}</p>
Enter fullscreen mode Exit fullscreen mode

Chaining pipes — apply multiple pipes left to right:

<p>{{ username | slice:0:10 | uppercase }}</p>
Enter fullscreen mode Exit fullscreen mode

Common built-in pipes (all from @angular/common):

Pipe Example Output
uppercase / lowercase `'hello' \ uppercase`
titlecase `'hello world' \ titlecase`
date `date \ date:'shortDate'`
currency `9.9 \ currency:'USD'`
number `3.14159 \ number:'1.0-2'`
percent `0.85 \ percent`
slice `'Angular' \ slice:0:3`
json `obj \ json`
async `obs$ \ async`

Importing pipes in standalone components — import each pipe individually (Angular 15+) rather than the entire CommonModule:

import { DatePipe, UpperCasePipe, CurrencyPipe } from '@angular/common';

@Component({
  imports: [DatePipe, UpperCasePipe, CurrencyPipe],
  ...
})
export class MyComponent { ... }
Enter fullscreen mode Exit fullscreen mode

Custom pipe — implement PipeTransform and decorate with @Pipe:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'truncate', standalone: true })
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 20): string {
    return value.length > limit ? value.slice(0, limit) + '' : value;
  }
}
Enter fullscreen mode Exit fullscreen mode
<p>{{ longText | truncate:50 }}</p>
Enter fullscreen mode Exit fullscreen mode

Top comments (0)