Filtering a Data Set with a Custom Element
Liferay DXP 2026.Q4+
To replace a data set’s filtering controls with your own and place them anywhere on the same page, build them in a custom element client extension. The custom element takes over the data set’s filtering, and the data set hides its Filter drop-down and active filters while it does. The liferay-sample-custom-element-8 sample shows a panel of options for each field and a manual mode that takes a typed OData expression.
To add one custom filter to the data set’s own Filter drop-down instead, use a frontend data set filter client extension.
Prerequisites
-
Install a supported version of Java.
NoteCheck the compatibility matrix for supported JDKs, databases, and environments. See JVM Configuration for recommended JVM settings.
-
Download and unzip the sample workspace:
curl -o com.liferay.sample.workspace-latest.zip https://repository.liferay.com/nexus/service/local/artifact/maven/content\?r\=liferay-public-releases\&g\=com.liferay.workspace\&a\=com.liferay.sample.workspace\&\v\=LATEST\&p\=zipunzip com.liferay.sample.workspace-latest.zip
Start a new Liferay DXP instance by running
docker run -it -m 8g -p 8080:8080 liferay/dxp:2026.q1.9-lts
Sign in to Liferay at http://localhost:8080 using the email address test@liferay.com and the password test. When prompted, change the password to learn.
Then set up a data set for the custom element to filter:
-
Create a data set whose REST endpoint accepts a
filterparameter, such as an object’s endpoint. Its items need a text field holding the values you want to filter on. Give it at least one visualization mode and at least one filter of any type, so the data set shows its Filter drop-down. See Creating Data Sets, Data Set Visualization Modes, and Data Set Filters. -
Add the data set to a page. See Adding a Data Set to a Page.
Now you can examine and modify the sample client extension.
Examine the Custom Element Client Extension
The custom element example is in the sample workspace’s client-extensions/liferay-sample-custom-element-8/ folder.
The sample’s client-extension.yaml file defines these details:
assemble:
- from: build/static
into: static
liferay-sample-custom-element-8:
friendlyURLMapping: liferay-sample-custom-element-8
htmlElementName: liferay-sample-custom-element-8
instanceable: true
name: Liferay Sample Custom Element 8
portletCategoryName: category.client-extensions
type: customElement
urls:
- index.*.js
useESM: true
To learn what each property does, see the Custom Element YAML Configuration Reference. To learn how the sample resolves the @liferay/frontend-data-set-web/api module at build time and at runtime, see Resolve the API Module at Build Time and at Runtime.
Understand the Code
As in the liferay-sample-custom-element-7 search sample, the src/index.tsx file reads the data set’s external reference code from the fds-name attribute. Read the Target Data Set from an HTML Attribute walks through that code. Always set fds-name: the sample’s fallback is a data set in an internal sample widget that released versions of Liferay DXP don’t include.
Own the Data Set’s Filtering
The src/App.tsx file creates an FDSConnection to the data set in a useEffect keyed on fdsName and disconnects it in the cleanup function:
useEffect(() => {
fdsConnectionRef.current = new FDSConnection(
fdsName,
{
apply: (customConfig: unknown) =>
applyFilterState(getValidFilterState(customConfig)),
search: () => {},
},
(fdsConnectionInfo: FDSConnectionInfo) => {
setReady(fdsConnectionInfo.status === 'ready');
},
{appId: 'sampleCustomElement8', owns: ['filters']}
);
return () => {
if (fdsConnectionRef?.current) {
fdsConnectionRef?.current.disconnect();
fdsConnectionRef.current = null;
}
};
}, [fdsName]);
For the first three arguments, see Create the Connection. The apply callback restores the filters saved in the page URL. The search callback is required, so the sample passes one that does nothing. A connection’s status is connecting, ready, refused, timeout, or disconnected.
A connection must own the data set’s filtering to change its filters. The fourth argument, options, asks for that ownership:
owns: ['filters']: Asks to own the data set’s filtering, so the custom element’s filters replace the data set’s Filter drop-down and active filters.
appId: A stable ID for your client extension, the same for every widget it adds. A connection that asks for the filtering without one gets the status refused.
Only one connection owns the filtering at a time, and the first to ask gets it. A later one gets the status refused, and if it has an appId, the page shows this warning:
Another widget is already filtering this data set, and only one widget can filter it. This widget is not filtering anything.
When the owning connection disconnects, the data set shows its Filter drop-down and active filters again. Call disconnect() when the custom element is removed. A connection that never disconnects keeps ownership of the data set’s filtering until the next page navigation.
Send Filters to the Data Set
Every filter change in the sample goes through one function, applyFilterState, which updates the sample’s state and sends the matching filter expressions to the data set:
const applyFilterState = (filterState: FilterState) => {
setFilterState(filterState);
fdsConnectionRef.current?.setFilters(
getFilters(filterState),
getCustomConfig(filterState)
);
};
The setFilters() method takes an array of FDSConnectionFilter objects shaped {id, odataFilterString}. Each call replaces the filter expressions the previous call passed, and the data set joins the nonempty expressions with and. To remove every filter expression, call clearFilters(). It applies an empty set and keeps ownership, so the data set’s Filter drop-down and active filters stay hidden.
While a connection owns the filtering, the data set’s request carries that connection’s filter expressions instead of the filters from its Filters tab, even before the first setFilters() call. The request also keeps any filter parameter already set in the data set’s REST endpoint or in the Parameters field of its Advanced Optional Parameters section.
A connection that doesn’t own the filtering ignores setFilters() and logs a console warning. A call made before the connection finds the data set, or after it disconnects, is dropped with no warning.
Restore Filters from the URL
The second argument of setFilters(), customConfig, is a value of your choice that holds the custom element’s filter state. While the owning connection applies at least one nonempty filter expression, the data set stores customConfig in the page URL under the appId, without interpreting it. The sample passes its filter state, which holds either the selected options or the typed expression:
export function getCustomConfig(filterState: FilterState): unknown {
return filterState.manual
? {expression: filterState.expression}
: {selections: filterState.selections};
}
When the page reloads, the browser goes back or forward, or someone opens a shared link, the data set passes customConfig back to apply, and the custom element’s UI and the data set’s results come back. A saved user view also stores customConfig and passes it back to apply when selected. Because the URL keys customConfig by appId, changing the appId breaks links saved earlier.
Because anyone can edit a URL, validate customConfig in apply. The sample’s getValidFilterState function drops any filter definition or option that FILTERS (in src/filters.ts) doesn’t declare. It passes a typed expression through unchanged and returns an empty state for any other value. If your custom element accepts typed expressions, validate them yourself.
To restore the filters before the data set’s first request, call setFilters() inside apply. When the URL carries a customConfig value, the data set holds its first request for up to five seconds, until the owning connection applies it. Filters set later, such as from a React effect, arrive after the unfiltered request. If no connection owns the filtering by then, the data set falls back to its Filter drop-down and active filters.
Define the Filters
The FILTERS array in src/filters.ts holds the sample’s filter definitions: Color, Size, and Status. This is the Color definition:
export const FILTERS: Array<FilterDefinition> = [
{
entityFieldType: 'string',
id: 'color',
label: 'Color',
multiple: true,
options: [
{label: 'Blue', value: 'Blue'},
{label: 'Green', value: 'Green'},
{label: 'Red', value: 'Red'},
{label: 'Yellow', value: 'Yellow'},
],
},
Each filter definition’s label is the panel heading the sample shows. With multiple: true, users can select more than one option in the panel. With multiple: false, selecting an option replaces the one already selected.
The getOdataFilterString function turns a selection into an expression:
export function getOdataFilterString(
{entityFieldType, id, multiple}: FilterDefinition,
values: ReadonlyArray<string>
): string {
if (!values.length) {
return '';
}
const literals = values.map((value) =>
entityFieldType === 'collection-integer'
? value
: `'${value.replace(/'/g, "''")}'`
);
if (entityFieldType === 'collection-integer') {
return `${id}/any(x:${literals
.map((literal) => `(x eq ${literal})`)
.join(' or ')})`;
}
if (values.length === 1 && !multiple) {
return `${id} eq ${literals[0]}`;
}
return `${id} in (${literals.join(', ')})`;
}
In manual mode, the sample sends only the typed expression, without any selected options.
Point the Filters at Your Data Set
The sample’s filter definitions name fields of an internal sample data set. Edit them to name your data set’s fields and values:
-
Open
src/filters.tsin theclient-extensions/liferay-sample-custom-element-8/folder. -
Replace the filter definitions in the
FILTERSarray with one per field you want to filter on. Set each one’s properties this way:-
entityFieldTypeaccepts only'string'and'collection-integer'(a field that holds a list of integers). To filter a field of another type, changegetOdataFilterStringor use the manual mode. -
idis the name the data set’s REST API uses for the field in afilterexpression. To see these names, choose a filter type from New Filter in the data set’s Filters tab and click Select beside Filter By. The Select Field pop-up lists the fields that filter type can use. -
Each option’s
valuemust be a value the field holds, because the sample sends it in the expression. The option’slabelis only the text the panel shows.
For example, this filter definition filters a
categorytext field:export const FILTERS: Array<FilterDefinition> = [ { entityFieldType: 'string', id: 'category', label: 'Category', multiple: true, options: [ {label: 'Books', value: 'Books'}, {label: 'Music', value: 'Music'}, ], }, ]; -
-
Save the file.
Deploy the Custom Element Client Extension to Liferay
With your Liferay instance running, run this command from the client-extensions/liferay-sample-custom-element-8/ folder in the sample workspace:
../../gradlew clean deploy -Ddeploy.docker.container.id=$(docker ps -lq)
This builds the client extension and deploys the zip to Liferay’s deploy/ folder in the Docker container you started. To deploy to Liferay SaaS instead, use the Liferay Cloud Command-Line Tool to run lcp deploy.
Confirm deployment is successful in your Liferay instance’s console:
STARTED liferaysamplecustomelement8_...
Connect the Custom Element to a Data Set
Copy the data set’s external reference code and then add the custom element to the page:
-
Open the Global Menu (
) → Control Panel → Data Sets. -
Click your data set and select the Details tab.
-
Copy the value of the read-only Data Set External Reference Code field.
-
Go to the site page displaying your data set and click Edit (
) at the top of the page. -
In the Fragments and Widgets sidebar (
), click Widgets. -
Find the Client Extensions → Liferay Sample Custom Element 8 widget and drag it onto the page.
-
Click Options (
) for the widget and select Configuration. -
In the Properties field, enter
fds-name=followed by the external reference code you copied, such asfds-name=d579b9dc-224c-dde1-7ea0-df291c2565c0. -
Click Save and close the dialog.
-
Click Publish.

The custom element’s options and its Filter manually button stay disabled until the connection’s status is ready.
If the options stay disabled, a browser console warning gives the reason:
-
Connection timed out for [fds-name]appears after 10 seconds when the connection finds no data set. Check thefds-namevalue and that both widgets are on the same page. -
Refused the filtering of [fds-name] to this connectionmeans the connection was refused, and the warning names why. Set anappIdif the custom element has none. Otherwise, another widget already filters the data set: remove that widget or point it at another data set.
Verify the Custom Element
On the published page, the data set no longer shows its Filter drop-down. These steps use the category example from Point the Filters at Your Data Set; if you defined other filters, use their options instead.
-
In the Liferay Sample Custom Element 8 widget, select Books and Music in the Category panel. The data set lists only their items. The browser’s Back button returns to the Books selection, and Forward restores Music. Clear all shows every item again.
-
Click Filter manually, type
category eq 'Books', click Apply, and reload the page. The data set still lists only the Books items. Click Choose from the options to return to the option panels.
-
Edit the page, add a second Liferay Sample Custom Element 8 widget with the same
fds-namevalue, and publish the page. A warning toast says that another widget is already filtering this data set, and one of the two widgets stays disabled.
-
Remove the second widget and publish the page.
You have successfully connected a custom element client extension that owns a data set’s filtering. Next, see Connecting a Custom Element to a Data Set to set the data set’s search query from another widget.