Wrapping Custom Elements with Fragments

Custom element applications enable developers to build rich, interactive experiences using modern JavaScript frameworks like React. But without a bridge to Liferay’s page editor, these applications remain locked behind code, accessible only to technical users.

This creates a gap. Developers can build amazing tools, but content managers can’t easily use or customize them.

Fragments are the bridge. By wrapping a custom element in a fragment, you transform a standalone JavaScript application into a reusable, configurable building block that can be used just like any other design component in the page editor.

There are many advantages to this approach. You can eliminate developer bottlenecks, promote the idea of reusability across pages, and take advantage of the more robust pattern of configuration over code. By leveraging the visual editing experience, you achieve faster outcomes by shortening the back and forth between teams.

When to Use Fragment Wrappers

Use a fragment wrapper for a custom element when

  • Your app is designed to be embedded on a Liferay page.

  • The app supports inputs or configuration options that may change depending on the context.

  • You want to empower non-developers to place and configure the app without code changes.

  • The same custom element may appear multiple times across the site, each time with different parameters (e.g., user IDs, product SKUs, layout preferences).

You can implement this pattern by

  1. Creating a simple fragment.

  2. Embedding a custom element inside it.

  3. Exposing configuration fields that pass data from the page editor directly into your React app via props.

But first, let’s understand how a custom element works.

Understanding Custom Element Client Extensions

Once you understand where the name of this client extension comes from, the rest of how this all works becomes crystal clear. In a plain HTML site, every DOM element you create is a descendant of the HTML element. This means that the markup is a collection and hierarchy of elements.

When you are creating a custom element client extension, one of the first steps is to change the value of the constant ELEMENT_ID found in the main.jsx file. You normally modify the value to be the same as your client extension project name. For example:

const ELEMENT_ID = 'clarity-custom-element-map';

if (!customElements.get(ELEMENT_ID)) {
	customElements.define(ELEMENT_ID, WebComponent);
}

The JavaScript following this assignment is used to check for the custom element, or register it if it is not found. Custom elements are nothing more than web components, and the element name becomes the ELEMENT_ID value. When you create an HTML page and you use a <div>, you are using a web component, an element with the name, or ELEMENT_ID, of 'div'. If the ELEMENT_IDs translate into the HTML tags, and you treat your custom elements as HTML elements, then it means you should be able to reference the above example, in markup, as:

<clarity-custom-element-map/>

This can be further clarified by looking at the Liferay Sample Workspace client extension liferay-sample-custom-element-1, which is a plain custom element starter with no JavaScript framework.

(function () {

	// Note this needs to be a real ES6 class (not transpiled).
	//
	// See: https://github.com/w3c/webcomponents/issues/587

	class VanillaCounter extends HTMLElement {
		constructor() {
			super();
...

Your custom element is nothing more than the definition of a new HTML element that the browser should process. Now, let’s understand how to wrap a custom element in a fragment.

Wrapping a Custom Element in a Fragment

Fragments in Liferay are typically managed in a fragment collection. You can create and manage these either through the UI manually, by importing an archive with the fragments, or by deploying them via Liferay Workspace using a site initializer client extension. Fragments are most commonly used for parts of the page that aren’t dynamic in nature. Some examples include a banner, an image, or maybe a heading for a section. But there are cases where the fragments also serve as highly dynamic elements, such as the Collections Display Fragment.

Custom elements are already dynamic by nature. So why would you wrap them in a fragment? There are two main scenarios for this behavior:

  • You want to pass configuration into the custom element that will affect its behavior/rendering, and this dynamic nature will change from one usage to the next.

  • You have some additional visual treatment you want to add.

Let’s take a closer look.

Passing Configuration to Fragments

If custom elements are nothing more than HTML elements, then it’s possible to leverage the attributes of an HTML element to pass data into the custom element.

<div> 
      <clarity-ticket-viewer ticket-id="12345" />
</div>

Or, you can pass the data in dynamically:

<div>
      <clarity-ticket-viewer ticket-id="{configuration.ticketId}" />
</div>

where the configuration options for the fragment set the value that will ultimately be passed to the custom element. To access these values, you can reference the attributes and build the props array to be passed to the application entry point.

const props = Object.fromEntries(
	Array.from(this.attributes).map(item => [item.name, item.value])
);

this.root.render(
	
);

From there, you can follow normal React processes to access the properties.

Augmenting Visuals

A second scenario is one in which you want to add some additional presentation around the custom element itself. To change the markup that the custom element generates, you would need to modify the client extension itself (for example, the React code). There are many reasons why this might not be possible. The person who needs the change may not be a React developer or the owner of the code. Fortunately, wrapping the custom element in a fragment provides some basic options.

<div class="ticket-container">
      <h1>Clarity Tickets</h1> 
      <div>
          <clarity-ticket-viewer ticket-id="{configuration.ticketId}" />
      </div>
</div>

In this example, it is possible to leverage the HTML for the fragment wrapper to create an outer container and add a level 1 heading that is not present in the custom element itself. When the content manager adds this fragment to the page, they will see the details (and functionality) of the custom element but also have the heading and any styling that has been applied via the ticket-container CSS class.

Using Custom Elements with Templates

When using custom elements with templates, you can achieve a similar result. Whether you are working with a Web Content Display template, an Application Display template, or a Display Page Template, simply including the custom element tag in the markup will enact the same behavior. This is because the result actually has nothing to do with Liferay itself. The Liferay server is responsible for returning the page with all the markup included but the evaluation of the markup is the responsibility of the browser. Whether the markup comes from a template, a fragment, or even a JSP, the only requirement is to have the custom element registered with the web components.

Conclusion

Wrapping a custom element in a fragment is the key to bridging the gap between developers and content managers in Liferay. This powerful pattern transforms a code-only component into a reusable, configurable block for the page editor, empowering both technical and non-technical users to build rich, dynamic pages.

Next, you’ll help Clarity use fragment configurations for their custom elements.

Loading Knowledge