SolidX
ReferenceExtending SolidXFrontend Customization

List Events

Learn how to create event listeners for list view events in your frontend application.

Overview

List view function extensions let you hook into list lifecycle events in SolidX.

Use them to:

  • Modify the outgoing list query or filter before API fetch
  • Transform list records after they are loaded
  • Adjust list layout dynamically at runtime

You author listener functions, register them in the owning UI module manifest, and reference them in your list view layout JSON.

Supported Events

Based on SolidListView.tsx, list view supports these lifecycle events:

onBeforeListDataLoad - runs just before the list API call. Best for filter and query shaping.

onListLoad - runs after list data is loaded. Best for data or layout transformation.

Event Execution Behavior

Current list lifecycle flow:

List state is prepared.

onBeforeListDataLoad executes if configured.

If the handler returns filterApplied: true with newFilter, that filter object is used for the API request.

The list API request runs.

onListLoad executes with fetched records.

If returned, newListData and newLayout are committed to state.

Project Structure & File Paths

For model-scoped list event functions, use:

  • solid-ui/src/<module-name>/admin-layout/<model-name>/extension-functions/

Register them in:

  • solid-ui/src/<module-name>/<module-name>.ui-module.ts

Example structure:

solid-ui/src/
└── <module-name>/
    ├── admin-layout/
    │   └── <model-name>/
    │       └── extension-functions/
    │           └── <model>ListViewChangeHandler.ts
    └── <module-name>.ui-module.ts

Creating a Handler

Example with both events in one handler:

import type { SolidBeforeListDataLoad, SolidLoadList, SolidListUiEventResponse } from "@solidxai/core-ui";

const handleBookListViewChange = (
  event: SolidBeforeListDataLoad | SolidLoadList
): SolidListUiEventResponse => {
  if (event.type === "onBeforeListDataLoad") {
    const nextFilter = structuredClone(event.filter || {});
    const existing = Array.isArray(nextFilter.$and) ? nextFilter.$and : [];

    nextFilter.$and = [...existing, { isActive: { $eq: true } }];

    return {
      filterApplied: true,
      newFilter: nextFilter,
    };
  }

  if (event.type === "onListLoad") {
    const enriched = (event.listData || []).map((row: any) => ({
      ...row,
      _uiRisk: row.score >= 10 ? "high" : "normal",
    }));

    return {
      dataChanged: true,
      newListData: enriched,
    };
  }

  return {};
};

export default handleBookListViewChange;

Registering the Handler

Register the function in the owning UI module manifest:

import { ExtensionFunctionTypes, type SolidUiModule } from "@solidxai/core-ui";
import handleBookListViewChange from "./admin-layout/book/extension-functions/bookListViewChangeHandler";

const libraryUiModule = {
  name: "library",
  extensionFunctions: [
    {
      name: "bookListViewChangeHandler",
      fn: handleBookListViewChange,
      type: ExtensionFunctionTypes.onBeforeListDataLoad,
    },
    {
      name: "bookListViewChangeHandler",
      fn: handleBookListViewChange,
      type: ExtensionFunctionTypes.onListLoad,
    },
  ],
} satisfies SolidUiModule;

export default libraryUiModule;

Using Handlers in Layout Metadata

Reference the handler name in list layout JSON:

{
  "name": "book-list-view",
  "layout": {
    "type": "list",
    "onBeforeListDataLoad": "bookListViewChangeHandler",
    "onListLoad": "bookListViewChangeHandler"
  }
}

Event Payload (Types)

onBeforeListDataLoad

export type SolidBeforeListDataLoad = {
  type: SolidUiEvents;
  isInitialLoad?: boolean;
  fieldsMetadata: FieldsMetadata;
  viewMetadata: SolidView;
  listViewLayout: ListLayoutType;
  filter?: any;
  queryParams?: any;
  user: any;
  session: Session;
  params?: SolidListViewParams;
};

onListLoad

export type SolidLoadList = {
  type: SolidUiEvents;
  isInitialLoad?: boolean;
  listData: any[];
  fieldsMetadata: FieldsMetadata;
  totalRecords: number;
  viewMetadata: SolidView;
  listViewLayout: ListLayoutType;
  queryParams?: any;
  user: any;
  session: Session;
  params?: SolidListViewParams;
};

isInitialLoad is true only for the very first time the event fires for a given list mount - SolidListView flips an internal ref to false synchronously, before the handler even runs, so every subsequent call (including one triggered by the handler's own side effects) sees false. This is what makes the pattern below safe.

Returning Changes

List event handlers return SolidListUiEventResponse:

export type SolidListUiEventResponse = {
  filterApplied?: boolean;
  newFilter?: any;
  dataChanged?: boolean;
  newListData?: any[];
  layoutChanged?: boolean;
  newLayout?: LayoutNode;
};

Usage rules:

  • For onBeforeListDataLoad, return filterApplied: true with newFilter to override the request filter.
  • For onListLoad, return dataChanged: true with newListData to replace records.
  • For layout mutations, return layoutChanged: true with newLayout.

Applying a Saved Filter as the Default on First Load

onBeforeListDataLoad doesn't have to return SolidListUiEventResponse at all - a handler can return void and instead reach into the list's own imperative handle to apply a saved filter (including one with unbound variables) as the default view. This is the only way to auto-apply an unbound-variable saved filter without a button click, since resolving its variables may need an async lookup the plain saved-filter dropdown has no way to trigger on its own.

The key to doing this safely is the isInitialLoad guard: applySavedFilter triggers a refetch, which re-runs onBeforeListDataLoad - without a guard, that second call would apply the filter again, triggering another refetch, forever. Gating on event.isInitialLoad !== true means the second (and every later) invocation returns immediately, since the ref backing it has already flipped to false.

import {
  getListView,
  getRegisteredListViewIds,
  type SolidBeforeListDataLoad,
} from "@solidxai/core-ui";
import { getActiveEmployee } from "../../../common/helpers"; // your own session -> domain-record lookup

const SAVED_FILTER_NAME = "Tickets Assigned To Me";

const ticketListDefaultAssignedToMe = async (event: SolidBeforeListDataLoad): Promise<void> => {
  // Only ever act on the very first load for this mount - every later call is a no-op.
  if (event.isInitialLoad !== true) return;

  const prefix = `page:${event.params?.moduleName}:${event.params?.modelName}:`;
  const listId = getRegisteredListViewIds().find((id) => id.startsWith(prefix));
  const listView = listId ? getListView(listId) : undefined;
  if (!listView) return;

  // Don't clobber a filter the user (or a restored session) already applied.
  const predicates = listView.getState().filterPredicates;
  const hasExistingFilter = [
    "custom_filter_predicate",
    "search_predicate",
    "saved_filter_predicate",
    "predefined_search_predicate",
  ].some((key) => predicates?.[key] != null);
  if (hasExistingFilter) return;

  const employee = await getActiveEmployee(event.session);
  if (!employee?.employeeId) return;

  listView.applySavedFilter(SAVED_FILTER_NAME, { employeeId: employee.employeeId });
};

export default ticketListDefaultAssignedToMe;

Register and reference this handler exactly like any other onBeforeListDataLoad function (see above). Because it applies the filter via applySavedFilter rather than a raw newFilter bake-in, it shows up as a normal, removable "Saved: Tickets Assigned To Me" pill in the search bar the moment it applies - no extra payload or rendering work needed on top of what's described here.

Common Patterns

  • Pre-filtering by tenant, role, or context in onBeforeListDataLoad
  • Enforcing default sort or locale-aware query options
  • Adding computed display-only properties in onListLoad
  • Runtime column visibility or label changes with SolidViewLayoutManager

Troubleshooting

  • Handler does not run -> verify metadata key and registered manifest name match exactly.
  • Filter changes not reflected -> ensure you return both filterApplied: true and newFilter.
  • Data changes ignored -> ensure dataChanged: true and newListData are both returned.
  • Layout not updating -> ensure layoutChanged: true with a valid newLayout.

See Also