Accessing the phrase signal inside the effect will cause the effect to fire on each update of the form. To get rid of the infinite loop, use untracked to access the phrase signal (So that its changes are not tracked).
Once we have the phrase untracked value, we can access the signals that actually need to be tracked, so that will be dirty and value.
constructor() {
effect(() => {
// wrapping the control in `untracked` prevents the `phrase` signal changes
// to be detected by the effect
const phraseCtrl = untracked(() => this.searchForm.phrase());
// then we access the signals which we do need to track
// so it is value and dirty signal.
const phraseValue = phraseCtrl.value();
const phraseDirty = phraseCtrl.dirty();
if (phraseDirty && phraseValue.length === 0) {
// when condition is met update the signal
this.#searchModel.update((current) => ({
...current,
pagination: { ...current.pagination, page: DEFAULT_PAGE },
}));
}
});
}
Full Code:
import { Component, signal, effect, untracked } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { form, debounce, Field } from '@angular/forms/signals';
const DEFAULT_PAGE = 1;
const DEFAULT_PAGE_SIZE = 10;
interface SearchModel {
phrase: string;
pagination: {
page: number;
size: number;
};
}
@Component({
selector: 'app-root',
imports: [Field],
template: `
<input type="text" [field]="searchForm.phrase">
<input type="number" [field]="searchForm.pagination.page">
<input type="number" [field]="searchForm.pagination.size">
`,
})
export class Playground {
readonly #searchModel = signal<SearchModel>({
phrase: '',
pagination: {
page: DEFAULT_PAGE,
size: DEFAULT_PAGE_SIZE,
},
});
protected readonly searchForm = form(this.#searchModel, (path) => {
debounce(path.phrase, 300);
});
constructor() {
effect(() => {
// wrapping the control in `untracked` prevents the `phrase` signal changes
// to be detected by the effect
const phraseCtrl = untracked(() => this.searchForm.phrase());
// then we access the signals which we do need to track
// so it is value and dirty signal.
const phraseValue = phraseCtrl.value();
const phraseDirty = phraseCtrl.dirty();
if (phraseDirty && phraseValue.length === 0) {
// when condition is met update the signal
this.#searchModel.update((current) => ({
...current,
pagination: { ...current.pagination, page: DEFAULT_PAGE },
}));
}
});
}
}
bootstrapApplication(Playground);