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.
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);
}
});
}
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);
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
);
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With