1

I’m using Angular 21’s new Signal Forms. I have a search form where the user types a query, receives a paginated result list, and can move between pages.

When the user changes the search phrase, I want to automatically reset pagination.page to 0 (DEFAULT_PAGE) so the results start from the first page again.

My model:

interface SearchModel {
  phrase: string;
  pagination: {
    page: number;
    size: number;
  };
}
readonly #searchModel = signal<SearchModel>({
    phrase: '',
    pagination: {
      page: DEFAULT_PAGE,
      size: DEFAULT_PAGE_SIZE,
    },
  });

My form:

protected readonly searchForm = form(this.#searchModel, (path) => {
  debounce(path.phrase, 300);
});

I tried using an effect in the constructor to reset the page when the phrase becomes dirty and its value is empty:

effect(() => {
  if (
    this.searchForm.phrase().dirty() &&
    this.searchForm.phrase().value().length === 0
  ) {
    this.#searchModel.update((current) => ({
      ...current,
      pagination: { ...current.pagination, page: DEFAULT_PAGE },
    }));
  }
});

This results in an infinite loop.

What is the correct way to reset pagination.page whenever the phrase value changes?

1 Answer 1

1

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);

Stackblitz Demo

Sign up to request clarification or add additional context in comments.

1 Comment

Using untracked to access the control seems to work. Thank you.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.