or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

application-management.mdchange-detection.mdcomponent-system.mddependency-injection.mdindex.mdmodern-authoring.mdresource-api.mdrxjs-interop.mdtesting.mdutilities-helpers.md
tile.json

change-detection.mddocs/

Change Detection & Reactivity

Angular's change detection and reactivity system efficiently manages DOM updates and provides reactive programming primitives including signals, computed values, and effects for state management.

Capabilities

Change Detection Control

Core change detection APIs for manually controlling when Angular checks for changes.

/**
 * Reference to the change detector for a component
 */
abstract class ChangeDetectorRef {
  /**
   * Mark component and ancestors as needing change detection
   */
  abstract markForCheck(): void;

  /**
   * Detach change detector from change detection tree
   */
  abstract detach(): void;

  /**
   * Manually trigger change detection for this component
   */
  abstract detectChanges(): void;

  /**
   * Check that no changes have occurred (dev mode only)
   */
  abstract checkNoChanges(): void;

  /**
   * Reattach previously detached change detector
   */
  abstract reattach(): void;
}

/**
 * Change detection strategies
 */
enum ChangeDetectionStrategy {
  /** Use OnPush change detection - only check when inputs change */
  OnPush = 0,
  /** Use default change detection - check on every cycle */
  Default = 1
}

Zone Management

Zone.js integration for automatic change detection triggering.

/**
 * Service for executing work inside or outside Angular's zone
 */
class NgZone {
  /**
   * Execute function inside Angular zone (triggers change detection)
   * @param fn - Function to execute
   * @returns Function result
   */
  run<T>(fn: (...args: any[]) => T): T;

  /**
   * Execute function outside Angular zone (no change detection)
   * @param fn - Function to execute  
   * @returns Function result
   */
  runOutsideAngular<T>(fn: (...args: any[]) => T): T;

  /**
   * Execute function when Angular zone is stable
   * @param fn - Function to execute
   * @returns Function result
   */
  runTask<T>(fn: (...args: any[]) => T): T;

  /** Observable that emits when zone becomes stable */
  readonly onStable: EventEmitter<any>;

  /** Observable that emits when zone becomes unstable */
  readonly onUnstable: EventEmitter<any>;

  /** Observable that emits when zone encounters an error */
  readonly onError: EventEmitter<any>;

  /** Whether currently in Angular zone */
  readonly isStable: boolean;
}

/**
 * Provider for zone-based change detection
 * @param options - Zone configuration options
 * @returns Environment providers for zone-based change detection
 */
function provideZoneChangeDetection(options?: NgZoneOptions): EnvironmentProviders;

/**
 * Provider for zoneless change detection (experimental)
 * @returns Environment providers for zoneless change detection
 */
function provideZonelessChangeDetection(): EnvironmentProviders;

/**
 * Configuration options for NgZone
 */
interface NgZoneOptions {
  /** Enable long stack trace (for debugging) */
  enableLongStackTrace?: boolean;
  /** Should coalesce event change detection */
  shouldCoalesceEventChangeDetection?: boolean;
  /** Should coalesce run change detection */
  shouldCoalesceRunChangeDetection?: boolean;
}

Reactive Signals

Core reactive programming primitives using signals for state management.

/**
 * Create a writable signal with initial value
 * @param initialValue - Initial value for the signal
 * @param options - Signal creation options
 * @returns Writable signal instance
 */
function signal<T>(initialValue: T, options?: CreateSignalOptions<T>): WritableSignal<T>;

/**
 * Create a computed signal derived from other signals
 * @param computation - Function that computes the value
 * @param options - Computed signal options
 * @returns Readonly computed signal
 */
function computed<T>(computation: () => T, options?: CreateComputedOptions<T>): Signal<T>;

/**
 * Create reactive effect that runs when signals change
 * @param effectFn - Effect function to execute
 * @param options - Effect creation options
 * @returns Effect reference for cleanup
 */
function effect(
  effectFn: (onCleanup: EffectCleanupRegisterFn) => void,
  options?: CreateEffectOptions
): EffectRef;

/**
 * Read signals without tracking dependencies
 * @param fn - Function to execute without tracking
 * @returns Function result
 */
function untracked<T>(fn: () => T): T;

/**
 * Check if value is a signal
 * @param value - Value to check
 * @returns True if value is a signal
 */
function isSignal(value: unknown): value is Signal<unknown>;

/**
 * Readonly signal interface
 */
interface Signal<T> {
  /** Get current signal value */
  (): T;
}

/**
 * Writable signal interface
 */
interface WritableSignal<T> extends Signal<T> {
  /** Set signal to new value */
  set(value: T): void;
  
  /** Update signal using current value */
  update(updateFn: (value: T) => T): void;
  
  /** Get readonly version of signal */
  asReadonly(): Signal<T>;
}

/**
 * Options for creating signals
 */
interface CreateSignalOptions<T> {
  /** Custom equality function for change detection */
  equal?: ValueEqualityFn<T>;
}

/**
 * Options for creating computed signals
 */
interface CreateComputedOptions<T> {
  /** Custom equality function for change detection */
  equal?: ValueEqualityFn<T>;
}

/**
 * Options for creating effects
 */
interface CreateEffectOptions {
  /** Injector to use for effect context */
  injector?: Injector;
  /** Whether effect should run manually */
  manualCleanup?: boolean;
  /** Allow effect to write to signals */
  allowSignalWrites?: boolean;
}

/**
 * Reference to created effect
 */
interface EffectRef {
  /** Destroy effect and cleanup resources */
  destroy(): void;
}

/**
 * Function type for value equality comparison
 */
type ValueEqualityFn<T> = (a: T, b: T) => boolean;

/**
 * Function type for registering effect cleanup
 */
type EffectCleanupRegisterFn = (cleanupFn: EffectCleanupFn) => void;

/**
 * Function type for effect cleanup
 */
type EffectCleanupFn = () => void;

After Render Effects

Effects that run after Angular's render phase.

/**
 * Create effect that runs after every render
 * @param effectFn - Effect function to execute
 * @param options - After render options
 * @returns Effect reference
 */
function afterEveryRender(
  effectFn: (phase: AfterRenderPhase) => void,
  options?: AfterRenderOptions
): EffectRef;

/**
 * Create effect that runs after next render only
 * @param effectFn - Effect function to execute  
 * @param options - After render options
 * @returns Effect reference
 */
function afterNextRender(
  effectFn: (phase: AfterRenderPhase) => void,
  options?: AfterRenderOptions
): EffectRef;

/**
 * Options for after render effects
 */
interface AfterRenderOptions {
  /** Injector to use for effect context */
  injector?: Injector;
  /** Render phase to execute in */
  phase?: AfterRenderPhase;
}

/**
 * Phases of Angular's render cycle
 */
enum AfterRenderPhase {
  /** Early read phase */
  EarlyRead = 0,
  /** Write phase */
  Write = 1,
  /** Mixed read/write phase */
  MixedReadWrite = 2,
  /** Read phase */
  Read = 3
}

/**
 * Reference to after render effect
 */
interface AfterRenderRef {
  /** Destroy effect */
  destroy(): void;
}

Collection Differs

System for detecting changes in collections and objects.

/**
 * Registry for iterable differ factories
 */
class IterableDiffers {
  /**
   * Create differ for iterable collection
   * @param iterable - Collection to create differ for
   * @param trackByFn - Optional track by function
   * @returns Iterable differ instance
   */
  find(iterable: any): IterableDifferFactory;

  /**
   * Create IterableDiffers with custom factories
   * @param factories - Array of differ factories
   * @param parent - Optional parent differs
   * @returns IterableDiffers instance
   */
  static create(factories: IterableDifferFactory[], parent?: IterableDiffers): IterableDiffers;

  /** Extend with additional factories */
  static extend(factories: IterableDifferFactory[]): StaticProvider;
}

/**
 * Differ for iterable collections
 */
interface IterableDiffer<V> {
  /** Calculate differences from previous state */
  diff(object: NgIterable<V> | undefined | null): IterableChanges<V> | null;
}

/**
 * Factory for creating iterable differs
 */
interface IterableDifferFactory {
  /** Check if factory supports the collection type */
  supports(objects: any): boolean;
  
  /** Create differ instance */
  create<V>(trackByFn?: TrackByFunction<V>): IterableDiffer<V>;
}

/**
 * Registry for key-value differ factories
 */
class KeyValueDiffers {
  /**
   * Create differ for key-value object
   * @param kv - Object to create differ for
   * @returns Key-value differ factory
   */
  find(kv: any): KeyValueDifferFactory;

  /** Create KeyValueDiffers with custom factories */
  static create<S>(factories: KeyValueDifferFactory[], parent?: KeyValueDiffers): KeyValueDiffers;

  /** Extend with additional factories */
  static extend<S>(factories: KeyValueDifferFactory[]): StaticProvider;
}

/**
 * Differ for key-value objects
 */
interface KeyValueDiffer<K, V> {
  /** Calculate differences from previous state */
  diff(object: Map<K, V> | {[key: string]: V} | undefined | null): KeyValueChanges<K, V> | null;
}

/**
 * Factory for creating key-value differs
 */
interface KeyValueDifferFactory {
  /** Check if factory supports the object type */
  supports(objects: any): boolean;
  
  /** Create differ instance */
  create<K, V>(): KeyValueDiffer<K, V>;
}

/**
 * Function type for tracking items in collections
 */
type TrackByFunction<T> = (index: number, item: T) => any;

/**
 * Type for iterable collections
 */
type NgIterable<T> = Array<T> | Iterable<T>;

Pending Tasks

Service for tracking asynchronous operations and application stability.

/**
 * Service for tracking pending asynchronous tasks
 */
class PendingTasks {
  /**
   * Add a pending task
   * @returns Task ID for later removal
   */
  add(): number;

  /**
   * Remove a pending task
   * @param taskId - ID of task to remove
   */
  remove(taskId: number): void;

  /** Whether there are pending tasks */
  hasPendingTasks: Signal<boolean>;
}

Usage Examples

Manual Change Detection

import { Component, ChangeDetectorRef, ChangeDetectionStrategy, inject } from '@angular/core';

@Component({
  selector: 'app-manual-detection',
  template: `
    <div>
      <h3>Manual Change Detection</h3>
      <p>Count: {{count}}</p>
      <p>Last updated: {{lastUpdated}}</p>
      <button (click)="increment()">Increment</button>
      <button (click)="updateOutsideZone()">Update Outside Zone</button>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualDetectionComponent {
  private cdr = inject(ChangeDetectorRef);
  
  count = 0;
  lastUpdated = new Date();

  increment(): void {
    this.count++;
    this.lastUpdated = new Date();
    // OnPush strategy requires manual marking
    this.cdr.markForCheck();
  }

  updateOutsideZone(): void {
    // Simulate async operation outside Angular zone
    setTimeout(() => {
      this.count += 10;
      this.lastUpdated = new Date();
      // Must manually trigger change detection
      this.cdr.detectChanges();
    }, 1000);
  }
}

Zone Management

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

@Component({
  selector: 'app-zone-example',
  template: `
    <div>
      <h3>Zone Management</h3>
      <p>Counter: {{counter}}</p>
      <button (click)="startInsideZone()">Start Inside Zone</button>
      <button (click)="startOutsideZone()">Start Outside Zone</button>
    </div>
  `
})
export class ZoneExampleComponent {
  private ngZone = inject(NgZone);
  
  counter = 0;

  startInsideZone(): void {
    // This will trigger change detection automatically
    setInterval(() => {
      this.counter++;
    }, 1000);
  }

  startOutsideZone(): void {
    // Run outside Angular zone to avoid triggering change detection
    this.ngZone.runOutsideAngular(() => {
      setInterval(() => {
        // Update happens outside zone
        this.counter++;
        
        // Manually trigger change detection when needed
        this.ngZone.run(() => {
          console.log('Manual change detection triggered');
        });
      }, 1000);
    });
  }

  ngOnInit(): void {
    // Listen for zone stability
    this.ngZone.onStable.subscribe(() => {
      console.log('Zone is stable');
    });

    this.ngZone.onUnstable.subscribe(() => {
      console.log('Zone became unstable');
    });
  }
}

Reactive Signals

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

@Component({
  selector: 'app-signals-example',
  template: `
    <div>
      <h3>Reactive Signals</h3>
      <p>First Name: <input [(ngModel)]="firstName" (input)="updateFirstName($event)"></p>
      <p>Last Name: <input [(ngModel)]="lastName" (input)="updateLastName($event)"></p>
      <p>Full Name: {{fullName()}}</p>
      <p>Initials: {{initials()}}</p>
      <p>Character Count: {{characterCount()}}</p>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class SignalsExampleComponent {
  // Writable signals
  firstName = signal('John');
  lastName = signal('Doe');

  // Computed signals (automatically update when dependencies change)
  fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
  
  initials = computed(() => {
    const first = this.firstName().charAt(0).toUpperCase();
    const last = this.lastName().charAt(0).toUpperCase();
    return `${first}.${last}.`;
  });

  characterCount = computed(() => this.fullName().length);

  constructor() {
    // Effects run when signals change
    effect(() => {
      console.log(`Full name changed to: ${this.fullName()}`);
    });

    // Effect with cleanup
    effect((onCleanup) => {
      const timer = setInterval(() => {
        console.log(`Current name: ${this.fullName()}`);
      }, 5000);

      onCleanup(() => {
        clearInterval(timer);
      });
    });
  }

  updateFirstName(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.firstName.set(target.value);
  }

  updateLastName(event: Event): void {
    const target = event.target as HTMLInputElement;
    this.lastName.set(target.value);
  }

  reset(): void {
    this.firstName.set('John');
    this.lastName.set('Doe');
  }
}

After Render Effects

import { Component, ElementRef, afterNextRender, afterEveryRender, viewChild } from '@angular/core';

@Component({
  selector: 'app-after-render',
  template: `
    <div>
      <canvas #canvas width="400" height="200"></canvas>
      <button (click)="redraw()">Redraw</button>
    </div>
  `
})
export class AfterRenderComponent {
  canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
  
  private frameCount = 0;

  constructor() {
    // Run once after next render
    afterNextRender(() => {
      console.log('Component rendered for the first time');
      this.initializeCanvas();
    });

    // Run after every render
    afterEveryRender(() => {
      this.frameCount++;
      console.log(`Render frame: ${this.frameCount}`);
    });

    // After render with specific phase
    afterEveryRender(() => {
      this.updateCanvasIfNeeded();
    }, {
      phase: AfterRenderPhase.Read
    });
  }

  private initializeCanvas(): void {
    const ctx = this.canvas().nativeElement.getContext('2d');
    if (ctx) {
      ctx.fillStyle = 'lightblue';
      ctx.fillRect(0, 0, 400, 200);
    }
  }

  private updateCanvasIfNeeded(): void {
    // Safe to read DOM properties here
    const canvas = this.canvas().nativeElement;
    if (canvas.width !== canvas.offsetWidth) {
      console.log('Canvas size changed, updating...');
    }
  }

  redraw(): void {
    const ctx = this.canvas().nativeElement.getContext('2d');
    if (ctx) {
      ctx.clearRect(0, 0, 400, 200);
      ctx.fillStyle = `hsl(${Math.random() * 360}, 50%, 50%)`;
      ctx.fillRect(0, 0, 400, 200);
    }
  }
}

Collection Tracking

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

interface User {
  id: number;
  name: string;
  email: string;
}

@Component({
  selector: 'app-tracking-example',
  template: `
    <div>
      <h3>Collection Tracking</h3>
      <button (click)="addUser()">Add User</button>
      <button (click)="shuffleUsers()">Shuffle</button>
      <button (click)="updateUser()">Update First User</button>
      
      <ul>
        <li *ngFor="let user of users; trackBy: trackByUserId; index as i">
          {{i}}: {{user.name}} ({{user.email}})
        </li>
      </ul>
    </div>
  `
})
export class TrackingExampleComponent {
  users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
    { id: 2, name: 'Bob', email: 'bob@example.com' },
    { id: 3, name: 'Charlie', email: 'charlie@example.com' }
  ];

  // TrackBy function for efficient list updates
  trackByUserId: TrackByFunction<User> = (index: number, user: User) => user.id;

  addUser(): void {
    const newId = Math.max(...this.users.map(u => u.id)) + 1;
    this.users.push({
      id: newId,
      name: `User ${newId}`,
      email: `user${newId}@example.com`
    });
  }

  shuffleUsers(): void {
    this.users = this.users.sort(() => Math.random() - 0.5);
  }

  updateUser(): void {
    if (this.users.length > 0) {
      this.users[0] = {
        ...this.users[0],
        name: `Updated ${Date.now()}`
      };
    }
  }
}