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.tsCreating 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, returnfilterApplied: truewithnewFilterto override the request filter. - For
onListLoad, returndataChanged: truewithnewListDatato replace records. - For layout mutations, return
layoutChanged: truewithnewLayout.
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: trueandnewFilter. - Data changes ignored -> ensure
dataChanged: trueandnewListDataare both returned. - Layout not updating -> ensure
layoutChanged: truewith a validnewLayout.

