Kala Tech

ΚΑΛΑ • GOOD TECH

Converting to Angular Signal Syntax: Inputs, Outputs, and Dependency Injection

Angular has undergone a massive renaissance, introducing modern reactive primitives known as Signals. Signals provide a fine-grained, developer-friendly way to manage state changes and reactivity, leading to faster execution and simpler component change detection.

If you are upgrading an older Angular application or starting a new project, migrating to Signal-based syntax is one of the highest-impact improvements you can make. Let's look at simple, direct examples of how to rewrite your decorators and constructors using the new APIs.


1. Changing @Input to input()

In traditional Angular, we used the @Input() decorator to pass data into components. The variable was a standard property that was not reactive unless we intercepted it with a setter or implemented OnChanges.

With Signals, we use the input() function. The input becomes a read-only Signal, meaning we can track its changes dynamically and derive state reactively.

Before: Decorator-based Input

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

@Component({
  selector: 'app-user-profile',
  template: `<div>User: {{ username }}</div>`
})
export class UserProfileComponent {
  @Input() username: string = 'Guest';
  @Input({ required: true }) userId!: string;
}

After: Signal-based Input

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

@Component({
  selector: 'app-user-profile',
  template: `<div>User: {{ username() }}</div>` // Note the parenthesis!
})
export class UserProfileComponent {
  // Declared as a Signal input with a default value
  username = input<string>('Guest');
  
  // Declared as a required Signal input
  userId = input.required<string>();
}

2. Changing @Output to output()

Outputs are used to emit custom events to parent components. Previously, we used @Output() coupled with the EventEmitter class.

Angular now provides a simplified output() function. It operates similarly but is lighter and does not rely on RxJS EventEmitter internally.

Before: Decorator-based Output

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

@Component({
  selector: 'app-user-actions',
  template: `<button (click)="onDelete()">Delete</button>`
})
export class UserActionsComponent {
  @Output() userDeleted = new EventEmitter<string>();

  onDelete() {
    this.userDeleted.emit('user-123');
  }
}

After: Signal-friendly Output

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

@Component({
  selector: 'app-user-actions',
  template: `<button (click)="onDelete()">Delete</button>`
})
export class UserActionsComponent {
  // Use the output() function
  userDeleted = output<string>();

  onDelete() {
    this.userDeleted.emit('user-123');
  }
}

3. Using inject() Over Constructor Injection

For years, constructor-based dependency injection was the only way to request services in Angular.

Now, the inject() function provides a more flexible, readable alternative. It allows you to declare dependencies directly as class fields, which is especially helpful when using functional features like route guards or inheritance.

Before: Constructor Injection

import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-dashboard',
  template: `...`
})
export class UserDashboardComponent implements OnInit {
  users: any[] = [];

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.userService.getUsers().subscribe(data => this.users = data);
  }
}

After: inject() Function Injection

import { Component, inject, OnInit } from '@angular/core';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-dashboard',
  template: `...`
})
export class UserDashboardComponent implements OnInit {
  // Inject directly as a property
  private userService = inject(UserService);
  users: any[] = [];

  ngOnInit() {
    this.userService.getUsers().subscribe(data => this.users = data);
  }
}

4. Derived State: computed() vs Getters

Previously, if you wanted to compute a value based on inputs, you would use a standard TypeScript getter. However, get methods run on every change detection cycle, even when the input values haven't changed.

With Signals, you wrap your calculations in computed(). The value is cached and only recalculates when the dependencies (the inputs or other signals) actually emit a new value.

Before: Getters or Lifecycle hook

import { Component, Input, OnChanges } from '@angular/core';

@Component({
  selector: 'app-user-header',
  template: `<h1>Hello, {{ displayName }}</h1>`
})
export class UserHeaderComponent implements OnChanges {
  @Input() firstName: string = '';
  @Input() lastName: string = '';
  displayName: string = '';

  ngOnChanges() {
    this.displayName = `${this.firstName} ${this.lastName}`.trim();
  }
}

After: Computed Signals

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

@Component({
  selector: 'app-user-header',
  template: `<h1>Hello, {{ displayName() }}</h1>`
})
export class UserHeaderComponent {
  firstName = input<string>('');
  lastName = input<string>('');

  // Dynamically computed and cached
  displayName = computed(() => {
    return `${this.firstName()} ${this.lastName()}`.trim();
  });
}

Conclusion

Migrating your code to the new Angular Signal APIs results in cleaner templates, more predictable reactivity, and better component performance. Modernizing your codebase step-by-step with these patterns prepares your app for Angular's future zoneless change detection!