htmlangularangular-changedetectionangular-signals

Angular signals not updating select value


I have a reusable angular component that handles cursor based pagination. However, the select value never changes on the HTML side, yet the change detection works.

Example, select options are 10, 25, 50. I can select my option as 25 and my page size updates. However the UI still reads as 10. So I cannot go back to the default page size of 10.

See code below:

Pagination component

import {
  ChangeDetectionStrategy,
  Component,
  computed,
  input,
  model,
  Output,
  signal 
} from '@angular/core';
import { EventEmitter } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CursorPaginationInput } from '@generated-types';

@Component({
  selector: 'app-cursor-pagination',
  imports: [FormsModule],
  template: `
    <div class="p-4 flex items-center justify-between">
      <div>
        <label>
          Items per page:
          <select [value]="itemsPerPage()" (change)="onItemsPerPageChange($event)">
            @for (size of [10, 25, 50, 100]; track size) {
              <option [value]="size">{{size}}</option>
            }
          </select>
        </label>
      </div>

      <div class="flex items-center gap-2">
        <button
          class="btn"
          (click)="goToPrevious()"
          [disabled]="!hasPreviousPage()"
        >
          ← Previous
        </button>

        <button
          class="btn"
          (click)="goToNext()"
          [disabled]="!hasNextPage()"
        >
          Next →
        </button>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CursorPaginationComponent {

  hasNextPage = input.required<boolean>();
  hasPreviousPage = input.required<boolean>();
  startCursor = input<string | undefined>(undefined);
  endCursor = input<string | undefined>(undefined);

  itemsPerPage = signal(10);
  str = computed(() => console.log(this.itemsPerPage(), 'lalala'));
  
  
  @Output() paginationChange = new EventEmitter<CursorPaginationInput>();

  goToNext() {
    if (this.hasNextPage() && this.endCursor()) {
      this.paginationChange.emit({
        after: this.endCursor(),
        first: this.itemsPerPage(),
      });
    }
  };

  goToPrevious() {
    if (this.hasPreviousPage() && this.startCursor()) {
      this.paginationChange.emit({
        before: this.startCursor(),
        last: this.itemsPerPage(),
      });
    }
  };

  onItemsPerPageChange(event: Event) {
    const newSize = parseInt((event.target as HTMLSelectElement).value, 10);
    this.itemsPerPage.set(newSize);
    this.paginationChange.emit({
      first: newSize,
    });
    
  };
}

Parent component

@let paginationMeta = (formsRxResorce.value())?.pageInfo;

<app-cursor-pagination
        [hasNextPage]="paginationMeta!.hasNextPage"
        [hasPreviousPage]="paginationMeta!.hasPreviousPage"
        [startCursor]="paginationMeta.startCursor!"
        [endCursor]="paginationMeta.endCursor!"
        (paginationChange)="onPaginate($event)"
      ></app-cursor-pagination>

export class ManageFormsComponent {
  cursorPaginationInputType = signal<CursorPaginationInput>({
    after: undefined,
    before: undefined,
    first: 10,
    last: 10,
  });
  formsRxResorce = rxResource({
    stream: () => this.getForms,
    params: () => this.cursorPaginationInputType(),
  });


  onPaginate(event: CursorPaginationInput) {
    viewTransitionsFn(() => this.cursorPaginationInputType.set(event));
  }

}


Solution

  • It is highly likely that you have few console errors which is causing the functionality to not work as expected.


    Below are the corrections made to get rid of few errors.

    We can use ?. Optional chaining to fallback to default value when API does not return anything.

    Safe navigation operator (?.) or (!.) and null property paths

    The optional chaining operator ?. permits reading the value of a property located deep within a chain of connected objects without having to expressly validate that each reference in the chain is valid. The ?. operator functions similarly to the . chaining operator, except that instead of causing an error if a reference is nullish (null or undefined), the expression short-circuits with a return value of undefined. When used with function calls, it returns undefined if the given function does not exist.

        @let paginationMeta = (formsRxResorce.value())?.pageInfo;
        <app-cursor-pagination
          [hasNextPage]="paginationMeta?.hasNextPage || false"
          [hasPreviousPage]="paginationMeta?.hasPreviousPage || false"
          [startCursor]="paginationMeta?.startCursor"
          [endCursor]="paginationMeta?.endCursor"
          (paginationChange)="onPaginate($event)"
        ></app-cursor-pagination>
    

    Also we can specify defaultValue to add a fallback on initial initialization.

    formsRxResorce = rxResource<any, any>({
      stream: () => this.getForms(),
      params: () => this.cursorPaginationInputType(),
      defaultValue: {
        hasNextPage: false,
        hasPreviousPage: false,
        endCursor: '',
        startCursor: '',
      },
    });
    

    Working example:

    import { Component, inject, ChangeDetectorRef } from '@angular/core';
    import { rxResource } from '@angular/core/rxjs-interop';
    import { bootstrapApplication } from '@angular/platform-browser';
    import {
      ChangeDetectionStrategy,
      computed,
      input,
      Output,
      signal,
    } from '@angular/core';
    import { EventEmitter } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { of } from 'rxjs';
    
    @Component({
      selector: 'app-cursor-pagination',
      imports: [FormsModule],
      template: `
        <div class="p-4 flex items-center justify-between">
          <div>
            <label>
              Items per page:
              <select [value]="itemsPerPage()" (change)="onItemsPerPageChange($event)">
                @for (size of [10, 25, 50, 100]; track size) {
                  <option [value]="size">{{size}}</option>
                }
              </select>
            </label>
          </div>
    
          <div class="flex items-center gap-2">
            <button
              class="btn"
              (click)="goToPrevious()"
              [disabled]="!hasPreviousPage()"
            >
              ← Previous
            </button>
    
            <button
              class="btn"
              (click)="goToNext()"
              [disabled]="!hasNextPage()"
            >
              Next →
            </button>
          </div>
        </div>
      `,
      changeDetection: ChangeDetectionStrategy.OnPush,
    })
    export class CursorPaginationComponent {
      hasNextPage = input.required<boolean>();
      hasPreviousPage = input.required<boolean>();
      startCursor = input<string | undefined>(undefined);
      endCursor = input<string | undefined>(undefined);
    
      itemsPerPage = signal(10);
      str = computed(() => console.log(this.itemsPerPage(), 'lalala'));
    
      @Output() paginationChange = new EventEmitter<any>();
    
      goToNext() {
        if (this.hasNextPage() && this.endCursor()) {
          this.paginationChange.emit({
            after: this.endCursor(),
            first: this.itemsPerPage(),
          });
        }
      }
    
      goToPrevious() {
        if (this.hasPreviousPage() && this.startCursor()) {
          this.paginationChange.emit({
            before: this.startCursor(),
            last: this.itemsPerPage(),
          });
        }
      }
    
      onItemsPerPageChange(event: Event) {
        const newSize = parseInt((event.target as HTMLSelectElement).value, 10);
        this.itemsPerPage.set(newSize);
        this.paginationChange.emit({
          first: newSize,
        });
      }
    }
    @Component({
      selector: 'app-root',
      imports: [CursorPaginationComponent],
      template: `
        @let paginationMeta = (formsRxResorce.value())?.pageInfo;
        <app-cursor-pagination
          [hasNextPage]="paginationMeta?.hasNextPage || false"
          [hasPreviousPage]="paginationMeta?.hasPreviousPage || false"
          [startCursor]="paginationMeta?.startCursor"
          [endCursor]="paginationMeta?.endCursor"
          (paginationChange)="onPaginate($event)"
        ></app-cursor-pagination>
    
      `,
    })
    export class App {
      cursorPaginationInputType = signal<any>({
        after: undefined,
        before: undefined,
        first: 10,
        last: 10,
      });
      formsRxResorce = rxResource<any, any>({
        stream: () => this.getForms(),
        params: () => this.cursorPaginationInputType(),
        defaultValue: {
          hasNextPage: false,
          hasPreviousPage: false,
          endCursor: '',
          startCursor: '',
        },
      });
    
      getForms() {
        return of({
          hasNextPage: false,
          hasPreviousPage: false,
          endCursor: '',
          startCursor: '',
        });
      }
    
      onPaginate(event: any) {
        console.log(event);
        // viewTransitionsFn(() => this.cursorPaginationInputType.set(event));
      }
    }
    
    bootstrapApplication(App);
    
    

    Stackblitz Demo