Integrating External Applications

Connecting a Custom Element to a Data Set

Liferay DXP 2026.Q3+

Release Feature

Important

The Data Set Manager is behind a release feature flag (LPS-164563). Enable it to create a data set and to reach the fields this tutorial uses. The FDSConnection API and the sample client extension aren’t gated by the flag.

A custom element client extension can set the search query of a data set on the same page. For example, the liferay-sample-custom-element-7 sample renders a search field and keeps it in sync with a data set. Because the sample is a separate widget, you can place its search field anywhere on the page rather than using the data set’s management bar.

Prerequisites

To use the liferay-sample-custom-element-7 client extension,

  1. Install a supported version of Java.

  2. 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\=zip
    
    unzip com.liferay.sample.workspace-latest.zip
    
  3. Start a new Liferay DXP instance by running

    docker run -it -m 8g -p 8080:8080 liferay/dxp:2026.q1.9-lts
    
  4. 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.

  5. Enable the LPS-164563 release feature flag.

  6. Create a data set to connect to. See Creating Data Sets.

  7. Add the data set to a page. See Adding a Data Set to a Page.

Now you have the tools to deploy your first data set search custom element. But first, explore its code.

Examine the Custom Element Client Extension

The custom element example is in the sample workspace’s client-extensions/liferay-sample-custom-element-7/ folder. Its client-extension.yaml file defines these details:

assemble:
    -   from: build/static
        into: static
liferay-sample-custom-element-7:
    friendlyURLMapping: liferay-sample-custom-element-7
    htmlElementName: liferay-sample-custom-element-7
    instanceable: true
    name: Liferay Sample Custom Element 7
    portletCategoryName: category.client-extensions
    type: customElement
    urls:
        -   index.*.js
    useESM: true

The client extension declares its ID (liferay-sample-custom-element-7), its type (customElement), and the HTML tag the browser renders (htmlElementName), and urls points at the JavaScript resource the build produces (index.*.js). The assemble block packages the build/static folder into the client extension’s .zip file as static resources, including the built index.*.js file. See the Custom Element YAML Configuration Reference for more information.

The name property is the widget name that appears in the page editor, and portletCategoryName puts that widget in the Client Extensions category. With instanceable: true, you can add the widget to a page more than once. The friendlyURLMapping property gives the widget its own URL, which this scenario doesn’t use. Using useESM: true makes Liferay load the client extension’s JavaScript as an ES module. Only an ES module can resolve a bare module specifier through the page’s import map, which is how this sample reaches the data set connection API at runtime.

Understand the Code

The sample is small, but it handles four key elements that any custom element connecting to a data set must handle. Each section below covers one of them.

  • Target Selection: How the element learns which data set to drive.

  • Connection Setup: How it opens a connection and tracks the data set’s search query.

  • Connection Lifecycle: How it stays subscribed across navigation and cleans up when removed.

  • Module Resolution: How the connection API resolves at build time and at runtime.

Read the Target Data Set from an HTML Attribute

The src/index.tsx file registers the custom element and reads the target data set from an fds-name HTML attribute, falling back to a constant when the attribute is absent:

import React from 'react';
import {Root, createRoot} from 'react-dom/client';

import App from './App';

const DEFAULT_FDS_NAME =
	'com_liferay_frontend_data_set_sample_web_internal_portlet_FDSSamplePortlet-classic';

const ELEMENT_NAME = 'liferay-sample-custom-element-7';

class LiferaySampleCustomElement extends HTMLElement {
	private _root?: Root;

	connectedCallback() {
		const fdsName = this.getAttribute('fds-name') || DEFAULT_FDS_NAME;

		this._root = createRoot(this);
		this._root.render(<App fdsName={fdsName} />);
	}

	disconnectedCallback() {
		this._root?.unmount();
	}
}

if (!customElements.get(ELEMENT_NAME)) {
	customElements.define(ELEMENT_NAME, LiferaySampleCustomElement);
}

For a data set you create in the Data Set Manager, the fds-name value is its external reference code. Copy that code from the Data Set External Reference Code field on the data set’s Details tab. See Managing Data Sets.

Note

The DEFAULT_FDS_NAME fallback uses a different pattern because it targets the data set that Liferay’s frontend data set sample widget renders.

Create the Connection

The src/App.tsx file imports the connection API from the @liferay/frontend-data-set-web/api module:

import {
	FDSConnection,
	FDSConnectionInfo,
	FDSConnectionStatus,
} from '@liferay/frontend-data-set-web/api';

You construct a connection with new FDSConnection(fdsName, fdsStateChangeCallback, onFDSConnectionInfoChange, options?). The fdsStateChangeCallback argument is an object holding a search callback. Liferay invokes that callback once when the connection becomes ready, so your component starts from the data set’s current query, and again on every later change. The onFDSConnectionInfoChange argument receives an object shaped {fdsName, instanceId, status}. Pass options.timeout to override the default connection timeout of 10000 ms.

A connection has three methods:

  • getSearch() returns the data set’s current query.
  • setSearch(query) writes a new query to the data set.
  • disconnect() ends the connection.

A connection’s status is connecting, ready, timeout, or disconnected. Both getSearch() and setSearch(query) short-circuit unless the status is ready, which covers the window before the connection completes and everything after disconnect(). Keep your controls disabled until the status is ready.

A timeout status means the connection never found a data set matching fdsName on the page. A mistyped external reference code is the usual cause.

The sample maps each status to an input placeholder:

const PLACEHOLDERS: Record<FDSConnectionStatus, string> = {
	connecting: 'waiting',
	disconnected: 'Search is not available',
	ready: 'Type search query...',
	timeout: 'Search is not available',
};

The sample hardcodes these strings and the Search button label in English instead of using language keys.

The sample creates the connection in a useEffect keyed on fdsName, updates the placeholder and the disabled state from each status change, and disconnects in the cleanup function:

useEffect(() => {
	fdsConnectionRef.current = new FDSConnection(
		fdsName,
		{
			search: (query: string) => {
				setQuery(query);
			},
		},
		(fdsConnectionInfo: FDSConnectionInfo) => {
			setPlaceholder(PLACEHOLDERS[fdsConnectionInfo.status]);
			setDisabled(fdsConnectionInfo.status !== 'ready');
		}
	);

	return () => {
		if (fdsConnectionRef?.current) {
			fdsConnectionRef?.current.disconnect();
			fdsConnectionRef.current = null;
		}
	};
}, [fdsName]);

The rendered interface is a text input and a Search button, both disabled until the status is ready. The handleSearch function calls fdsConnectionRef.current?.setSearch(query), and the input triggers it on the Enter key as well.

Follow the Connection Lifecycle

The disconnect() method disposes the search subscription, detaches the navigation handle, and reports the disconnected status. FDSConnection also attaches to Liferay’s beforeNavigate event and calls disconnect() itself. This frees you from managing the subscription across single page application (SPA) navigation. When you navigate away from the page and return, the custom element reconnects to the data set with a single subscription. The sample also calls disconnect() on unmount.

The data set stores its search query in the page URL, so you can bookmark or share a filtered view, and the browser’s back and forward buttons move between your searches.

Resolve the API Module at Build Time and at Runtime

The @liferay/frontend-data-set-web/api specifier resolves differently at build time and at runtime.

Liferay writes an import map into every page. An import map is a browser feature that maps a bare module specifier to the URL serving it, so at runtime the browser loads the implementation Liferay serves.

At build time, the sample’s tsconfig.json maps the same specifier to the type definitions published in the @liferay/js-api package:

"compilerOptions": {
    "paths": {
        "@liferay/frontend-data-set-web/api": [
            "./node_modules/@liferay/js-api/data-set"
        ]
    }
}

The build script in the sample’s package.json marks that specifier external, so the browser resolves the module from the import map and the bundle doesn’t include a copy:

"build": "esbuild src/index.tsx --bundle --entry-names=[name].[hash] --external:@liferay/frontend-data-set-web/api --external:react --external:react-dom --format=esm --outdir=build/static"

React and React DOM are external for the same reason, since Liferay provides both at runtime.

Your code writes one import either way. The sample’s package.json pins "@liferay/js-api": "^0.7.1" for the type definitions.

Deploy the Custom Element Client Extension to Liferay

With your Liferay instance running, run this command from the client-extensions/liferay-sample-custom-element-7/ 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. For the complete deployment workflow, including deploying to Liferay SaaS, see Deploy the Client Extension to Liferay.

Confirm deployment is successful in your Liferay instance’s console:

STARTED liferaysamplecustomelement7_...

Connect the Custom Element to a Data Set

First, copy the identifier the custom element needs:

  1. Open the Global Menu (Global Menu), select Control Panel, and click Data Sets.

  2. Click your data set, and select the Details tab.

  3. Copy the value of the read-only Data Set External Reference Code field.

  4. Go to the site page displaying your data set and begin editing it.

  5. Add the Liferay Sample Custom Element 7 widget to the page.

  6. Click Options (Widget Options) for the widget and select Configuration.

  7. Enter fds-name=<data set external reference code> into the Properties field, using the code you copied.

    The Properties field takes one attribute=value pair per line. The fds-name value determines which data set the custom element drives.

  8. Click Save.

  9. Click Publish.

The custom element’s input and Search button are enabled once the data set finishes loading. While the connection is forming, the input displays the waiting placeholder. Once the connection is ready, the placeholder becomes Type search query....

Verify the Custom Element

Confirm the custom element and data set search stays in sync:

  1. Type a query in the custom element’s search field and click Search. The same value appears in the data set’s search field and filters the data set’s results.

  2. Type a query in the data set’s search bar. The same value appears in the custom element’s input field.

    Tip

    If the input stays disabled and reads Search is not available, the connection didn’t find the data set. Check that the external reference code is correct, that the data set is on the page, and that the LPS-164563 feature flag is enabled. FDSConnection also logs a connection timeout warning to the browser console, naming the value it tried.

    Finally, you can remove the built-in search bar for the data set.

  3. Go to the Data Sets application, begin editing your data set, and go to the Settings tab.

  4. Turn off Show Search and click Save.

  5. Return to the site page with the data set. The search bar above the data set should be gone.

  6. Use the custom element to filter the data set.

You have successfully connected a custom element client extension to a data set’s search query. Next, try using data set actions to customize your data set further.