Fetching latest headlines…
Core Concepts
NORTH AMERICA
🇺🇸 United StatesJune 29, 2026

Core Concepts

0 views0 likes0 comments
Originally published byDev.to

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>

4. Property Binding

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

<img [src]="imageUrl" />
<button [disabled]="isLoading">Submit</button>

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>

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;

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;
<!-- in the template -->
<p>{{ selectedUser().name }}</p>

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();
<p>{{ username() }}</p>

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;

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);
}

Child template:

<button (click)="onSelectUser()">Select User</button>

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

<app-user (userSelected)="selectUser($event)" />

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());
}

Child template:

<button (click)="onSelectUser()">Select User</button>

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

<app-user (userSelected)="selectUser($event)" />

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>

@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>
}

@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>
}

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

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

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>
}

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>
}

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>

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>

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

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

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 = '';
}
<input [(ngModel)]="title" name="title" />

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" />

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

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

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);
  }
}
<!-- parent template -->
<app-rating [(rating)]="userRating" />

[(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
  }
}
<!-- parent template — identical syntax to Option A -->
<app-rating [(rating)]="userRating" />

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,
    });
  }
}

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>

Parent template:

<app-create-user (createUser)="onCreate($event)" />

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>

Parent template:

<app-card>
  <p>This paragraph is projected into the card.</p>
</app-card>

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>
<app-card>
  <h2 card-header>My Title</h2>
  <p card-body>My content goes here.</p>
</app-card>

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;
  }
}

Constructor injection (traditional style):

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

  getTasks() {
    return this.taskService.getTasks();
  }
}

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();
  }
}

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 { ... }

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>

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>

Chaining pipes — apply multiple pipes left to right:

<p>{{ username | slice:0:10 | uppercase }}</p>

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 { ... }

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;
  }
}
<p>{{ longText | truncate:50 }}</p>

Comments (0)

Sign in to join the discussion

Be the first to comment!