Table State

@qualcomm-ui/angular/table provides a state management system for storing and managing table state. It allows you to extract specific state elements to integrate with your own state management system. This guide explains the various methods for interacting with and managing table state.

Accessing Table State

No special setup is required for the table state to function. If you do not provide any parameters for state, initialState, or any of the on[State]Change table options, the table will automatically manage its state internally. You can access any part of this internal state through the table.getState() API.

export class ExampleComponent {
  protected readonly table = createAngularTable(() => ({
    columns,
    data: this.data(),
    // ...
  }))

  // access the entire internal state
  readonly tableState = computed(() => this.table.getState())

  // access just the row selection state
  readonly rowSelection = computed(() => this.table.getState().rowSelection)
}

Initial State

If you only need to customize the initial values for certain states, you don't need to manage the state yourself. Simply set the desired values in the initialState option of the table instance.

export class ExampleComponent {
  protected readonly table = createAngularTable(() => ({
    columns,
    data: this.data(),
    initialState: {
      columnOrder: ["age", "firstName", "lastName"], // customize the initial column order
      columnVisibility: {
        id: false, // hide the id column by default
      },
      expanded: true, // expand all rows by default
      sorting: [
        {
          id: "age",
          desc: true, // sort by age in descending order by default
        },
      ],
    },
    // ...
  }))
}

NOTE

Specify each particular state in either initialState or state, but not both. If you provide a state value to both initialState and state, the value in state will overwrite the corresponding value in initialState.

Controlled State

@qualcomm-ui/angular/table allows you to control and manage any or all of the table state within your own state management system. You can achieve this by passing your own state and state management functions to the state and on[State]Change table options.

Individual Controlled State

You can control only the state you need easy access to; you do not have to manage the entire table state if it's unnecessary. It's recommended to control state selectively, based on your specific needs.

To control a particular state, you need to pass both the corresponding state value and the on[State]Change function to the table instance.

For example, in a "manual" server-side data fetching scenario, you can manage the filtering, sorting, and pagination state within your own state management system. You can ignore other states, like column order or column visibility, if they are not relevant to your API.

import {Component, signal} from "@angular/core"
import {
  SortingState,
  ColumnFiltersState,
  PaginationState,
} from "@qualcomm-ui/core/table"
import {createAngularTable} from "@qualcomm-ui/angular/table"

@Component({
  // ...
})
export class ExampleTableComponent {
  readonly columnFilters = signal<ColumnFiltersState>([])
  readonly pagination = signal<PaginationState>({
    pageIndex: 0,
    pageSize: 15,
  })
  readonly sorting = signal<SortingState[]>([
    {
      id: "age",
      desc: true, // sort by age in descending order by default
    },
  ])

  private readonly dataApi = inject(SomeDataApi)

  private async fetchData() {
    const columnFilters = this.columnFilters()
    const sorting = this.sorting()
    const pagination = this.pagination()

    // some fetch function
    this.data.set(
      await this.dataApi.search({columnFilters, pagination, sorting}),
    )
  }

  readonly table = createAngularTable(() => ({
    columns: this.columns,
    data: this.data(),
    state: {
      // pass controlled state back to the table (overrides internal state)
      columnFilters: this.columnFilters(),
      sorting: this.sorting(),
      pagination: this.pagination(),
    },
    // hoist state updates into our own state management
    onColumnFiltersChange: (updater) => {
      updater instanceof Function
        ? this.columnFilters.update(updater)
        : this.columnFilters.set(updater)
    },
    onSortingChange: (updater) => {
      updater instanceof Function
        ? this.sorting.update(updater)
        : this.sorting.set(updater)
    },
    onPaginationChange: (updater) => {
      updater instanceof Function
        ? this.pagination.update(updater)
        : this.pagination.set(updater)
    },
  }))
}

Fully Controlled State

Alternatively, you can control the entire table state using the onStateChange table option. This will hoist the entire table state into your own state management system. However, be cautious with this approach, as raising frequently changing state values, such as columnSizingInfo, up a component tree might cause performance issues.

A couple of additional steps may be needed to make this work. When using the onStateChange table option, the initial values of the state must include all relevant state values for the features you want to use. You can either manually input all the initial state values or use a constructor in a special way, as demonstrated below.

import {signal} from "@angular/core"
import {TableState} from "@qualcomm-ui/core/table"

@Component({
  // ...
})
export class ExampleComponent {
  // create an empty table state, we'll override it later
  readonly state = signal({} as TableState)

  // create a table instance with default state values
  readonly table = createAngularTable(() => ({
    columns: this.columns,
    data: this.data(),
    // our fully controlled state overrides the internal state
    state: this.state(),
    onStateChange: (updater) => {
      // any state changes will be pushed up to our own state management
      this.state.set(
        updater instanceof Function ? updater(this.state()) : updater,
      )
    },
  }))

  constructor() {
    // set the initial table state
    this.state.set({
      // populate the initial state with all of the default state values
      // from the table instance
      ...this.table.initialState,
      pagination: {
        pageIndex: 0,
        pageSize: 15, // optionally customize the initial pagination state.
      },
    })
  }
}

On State Change Callbacks

So far, we have seen that the on[State]Change and onStateChange table options can "hoist" table state changes into our own state management. However, there are a few important considerations to keep in mind when using these options.

Callback Parameters

State Change Callbacks MUST have their corresponding state value in the state option.

Specifying an on[State]Change callback indicates that the state will be controlled. If you do not provide the corresponding state value, that state will remain "frozen" at its initial value.

import {signal} from "@angular/core"
import {SortingState} from "@qualcomm-ui/core/table"

@Component({
  // ...
})
export class ExampleComponent {
  protected sorting = signal<SortingState>([])

  protected table = createAngularTable(() => ({
    columns,
    data: this.data(),
    state: {
      sorting: this.sorting(), // required because we are using `onSortingChange`
    },
    onSortingChange: (updater) => {
      this.sorting.update((old) =>
        updater instanceof Function ? updater(old) : updater
      )
    },
    // ...
  }))
}

Updaters

Updaters can either be raw values or callback functions (like Angular's signal.update() updater function).

The on[State]Change and onStateChange callbacks function exactly like the signal.update() method. The updater values can either be a new state value or a callback function that takes the previous state value and returns the new state value.

This means that if you want to add some extra logic to any of the on[State]Change callbacks, you can do so, but you need to check whether the new incoming updater value is a function or a value.

import {signal} from "@angular/core"
import {PaginationState, SortingState} from "@qualcomm-ui/core/table"

@Component({
  // ...
})
export class ExampleComponent {
  protected sorting = signal<SortingState>([])
  protected pagination = signal<PaginationState>({
    pageIndex: 0,
    pageSize: 10,
  })

  protected table = createAngularTable(() => ({
    columns,
    data: this.data(),
    state: {
      pagination: this.pagination(),
      sorting: this.sorting(),
    },
    // syntax 1
    onPaginationChange: (updater) => {
      this.pagination.update((old) => {
        const newPaginationValue =
          updater instanceof Function ? updater(old) : updater
        // do something with the new pagination value
        // ...
        return newPaginationValue
      })
    },
    // syntax 2
    onSortingChange: (updater) => {
      const newSortingValue =
        updater instanceof Function
          ? updater(this.sorting())
          : updater
      // do something with the new sorting value
      // ...
      this.sorting.set(newSortingValue) // normal state update
    },
  }))
}

State Types

All complex states have their own TypeScript types that you can import and use. This is useful for ensuring that you are using the correct data structures and properties for the state values you are controlling.

Last updated on by Ryan Bower