Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use EventEmitters with signals in Angular

I cannot find any documentation on this, but it seems like you cannot emit an event from an effect. What is the recommended way to emit an event based on a signal value changing?

I have a simple component:

class SimpleComponent {
  @Output countChanged: EventEmitter<number> = new EventEmitter<number>();

  constructor() {
    this.form = this.formBuilder.group({
      totalCount: new FormControl<number>(0),
    });

    // we want to output an event when the form value changes
    const formSelection = toSignal(this.form.get('totalCount').valueChanges, { initialValue: 0 });
    effect(() => {
      const selection = formSelection()
      if (selection != 0) {
         // call fails if parent updates a signal
         this.countChanged.emit(selection) 
      }
    });
  }
}

The above code is fine (I think). However, in the parent it updates a signal in the handler like so:

// template:
<simple-component 
   (countChanged)="handleCountChange($event)"
 ></simple-component>

// in component
mySignal = signal(0);

handleCountChanged(count: number) {
   this.mySignal.set(count); // Angular doesn't like this
}

The above code logs 'writing to signals is not allowed in a computed or an effect', and an event is not emitted.

What is the recommended way to use EventEmitters with signals? Or am I doing something wrong?

I can work around this by either setting { allowSignalWrites: true } as a param to the effect call. Another option is to use untracked(this.countChange.emit(selection).

However, I'm not sure if either is recommended. I'm assuming this would be a common use-case with angular signals.


EDIT: Add the detail is that the failure occurs because the parent output handler is updating a signal.

I'm running Angular 16.12.10.

like image 916
Brendan Goldacker Avatar asked Feb 02 '26 07:02

Brendan Goldacker


1 Answers

Since your version is angular 16, There is no need to convert the observable to a signal, you can use the observable sequence to trigger the event emitter.

constructor(private formBuilder: FormBuilder) {
  this.form
    .get('totalCount')!
    .valueChanges.pipe(takeUntilDestroyed(), startWith(0))
    .subscribe((selection: any) => {
      if (selection !== 0) {
        console.log('asdf', selection);
        // call fails if parent updates a signal
        this.countChanged.emit(selection);
      }
    });
}

Full Code:

import { Component, EventEmitter, Output, effect, signal } from '@angular/core';
import {
  outputFromObservable,
  takeUntilDestroyed,
  toSignal,
} from '@angular/core/rxjs-interop';
import {
  ReactiveFormsModule,
  FormBuilder,
  FormControl,
  FormGroup,
} from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
import { startWith } from 'rxjs';
import 'zone.js';

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form"> 
      <input type="number" formControlName="totalCount"/>
    </form>
  `,
})
export class Child {
  @Output() countChanged: EventEmitter<number> = new EventEmitter<number>();
  form: FormGroup = this.formBuilder.group({
    totalCount: new FormControl<number>(0),
  });

    constructor(private formBuilder: FormBuilder) {
      this.form
        .get('totalCount')!
        .valueChanges.pipe(takeUntilDestroyed(), startWith(0))
        .subscribe((selection: any) => {
          if (selection !== 0) {
            console.log('asdf', selection);
            // call fails if parent updates a signal
            this.countChanged.emit(selection);
          }
        });
    }
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Child],
  template: `
  {{mySignal()}}
    <app-child (countChanged)="handleCountChanged($event)"/> 
  `,
})
export class App {
  mySignal = signal(0);

  handleCountChanged(count: any) {
    console.log(count, 'qwe');
    this.mySignal.set(count); // Angular doesn't like this
  }
}

bootstrapApplication(App);

Stackblitz Demo


You can use outputFromObservable to convert the observable from valueChanges to directly emit the values as an EventEmitter without using an effect at all.

  totalCountChange = outputFromObservable(
    this.form.get('totalCount')!.valueChanges
  );

Full Code:

import { Component, EventEmitter, Output, effect } from '@angular/core';
import { outputFromObservable, toSignal } from '@angular/core/rxjs-interop';
import {
  ReactiveFormsModule,
  FormBuilder,
  FormControl,
  FormGroup,
} from '@angular/forms';
import { bootstrapApplication } from '@angular/platform-browser';
import 'zone.js';

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="form"> 
      <input type="number" formControlName="totalCount"/>
    </form>
  `,
})
export class Child {
  @Output() countChanged: EventEmitter<number> = new EventEmitter<number>();
  form: FormGroup = this.formBuilder.group({
    totalCount: new FormControl<number>(0),
  });
  totalCountChange = outputFromObservable(
    this.form.get('totalCount')!.valueChanges
  );
  constructor(private formBuilder: FormBuilder) {}
}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [Child],
  template: `
    <app-child (totalCountChange)="emit($event)"/> 
  `,
})
export class App {
  emit(selection: any) {
    if (selection != 0) {
      alert(selection); // call fails
    }
  }
}

bootstrapApplication(App);

Stackblitz Demo

like image 65
Naren Murali Avatar answered Feb 03 '26 21:02

Naren Murali



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!