oo

Creating a Basic Custom Element

Liferay 7.4+

Custom element client extensions use Liferay’s frontend infrastructure to register external, remote applications with the Liferay platform and render them as widgets.

Note

Custom Element client extensions can use any technology, regardless of how it’s built, packaged, or hosted.

Prerequisites

To start developing client extensions,

  1. Install Java (JDK 8 or JDK 11).

    Note

    Check the compatibility matrix for supported JDKs, databases, and environments. See JVM Configuration for recommended JVM settings.

  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
    

Now you have the tools to deploy your first Custom Element client extension.

Examine and Modify the Custom Element Client Extension

The Custom Element client extension is in the sample workspace’s client-extensions/liferay-sample-custom-element-1/ folder. It’s defined in the client-extension.yaml file:

liferay-sample-custom-element-1:
   cssURLs:
      - style.css
   friendlyURLMapping: vanilla-counter
   htmlElementName: vanilla-counter
   instanceable: false
   name: Liferay Sample Custom Element 1
   portletCategoryName: category.client-extensions
   type: customElement
   urls:
      - index.js
   useESM: false

The client extension has the ID liferay-sample-custom-element-1 and contains the key configurations for a Custom Element client extension, including the type and the url property that defines the JavaScript resource file’s location. See the Custom Element YAML configuration reference for more information on the available properties.

It also contains the assemble block:

   assemble:
      - from: assets
      into: static

This specifies that everything in the assets/ folder should be included as a static resource in the built client extension .zip file. The JavaScript and CSS files in a client extension are used as static resources in Liferay.

The assets/index.js file defines a custom HTML element named vanilla-counter that represents a simple counter component. It creates buttons to increment and decrement the counter, displays the current counter value, and shows the internal route based on the URL. Additionally, it attaches event listeners for button clicks and provides methods to handle counter updates and route information.

(function () {
   // Enable strict mode for error handling.
   'use strict';

   // Define a custom HTML element as a subclass of HTMLElement.
   class VanillaCounter extends HTMLElement {
      // Constructor function to initialize the element.
      constructor() {
         super(); // Call the constructor of the superclass (HTMLElement).

         // Initialize instance variables.
         this.friendlyURLMapping = this.getAttribute('friendly-url-mapping');
         this.value = 0;

         // Create DOM elements for counter, buttons, and route.
         this.counter = document.createElement('span');
         this.counter.setAttribute('class', 'counter');
         this.counter.innerText = this.value;

         this.decrementButton = document.createElement('button');
         this.decrementButton.setAttribute('class', 'decrement');
         this.decrementButton.innerText = '-';

         this.incrementButton = document.createElement('button');
         this.incrementButton.setAttribute('class', 'increment');
         this.incrementButton.innerText = '+';

         // Create a <style> element to apply CSS styles.
         const style = document.createElement('style');
         style.innerHTML = `
            button {
               height: 24px;
               width: 24px;
            }

            span {
               display: inline-block;
               font-style: italic;
               margin: 0 1em;
            }
         `;

         // Create a <div> element to display portlet route information
         this.route = document.createElement('div');
         this.updateRoute();

         // Create a root <div> element to hold all elements.
         const root = document.createElement('div');
         root.setAttribute('class', 'portlet-container');
         root.appendChild(style);
         root.appendChild(this.decrementButton);
         root.appendChild(this.incrementButton);
         root.appendChild(this.counter);
         root.appendChild(this.route);

         // Attach the shadow DOM to the custom element.
         this.attachShadow({mode: 'open'}).appendChild(root);

         // Bind event handlers to the current instance.
         this.decrement = this.decrement.bind(this);
         this.increment = this.increment.bind(this);
      }

      // Called when the custom element is added to the DOM.
      connectedCallback() {
         this.decrementButton.addEventListener('click', this.decrement);
         this.incrementButton.addEventListener('click', this.increment);
      }

      // Handles the decrement button click event.
      decrement() {
         this.counter.innerText = --this.value;
      }

      // Called when the custom element is removed from the DOM.
      disconnectedCallback() {
         this.decrementButton.removeEventListener('click', this.decrement);
         this.incrementButton.removeEventListener('click', this.increment);
      }

      // Handles the increment button click event.
      increment() {
         this.counter.innerText = ++this.value;
      }

      // Method to update the portlet route information based on the current URL
      updateRoute() {
         const url = window.location.href;
         const prefix = `/-/${this.friendlyURLMapping}/`;
         const prefixIndex = url.indexOf(prefix);
         let route;

         if (prefixIndex === -1) {
            route = '/';
         } else {
            route = url.substring(prefixIndex + prefix.length - 1);
         }

         this.route.innerHTML = `<hr><b>Portlet internal route</b>: ${route}`;
      }
   }

   // Check if the custom element has already been defined
   if (!customElements.get('vanilla-counter')) {
      // Define the custom element with the tag name 'vanilla-counter'
      customElements.define('vanilla-counter', VanillaCounter);
   }
})();

Other Custom Element client extension samples that use different frameworks/programming languages/libraries are available through the sample workspace. Try deploying and using them too.

Now, deploy the client extension.

Deploy the Custom Element Client Extension to Liferay

Start a new Liferay instance by running

docker run -it -m 8g -p 8080:8080 liferay/portal:7.4.3.112-ga112

Sign in to Liferay at http://localhost:8080. Use the email address test@liferay.com and the password test. When prompted, change the password to learn.

Once Liferay starts, run this command from the client extension’s folder in the sample workspace:

../../gradlew clean deploy -Ddeploy.docker.container.id=$(docker ps -lq)

This builds your client extension and deploys the zip to Liferay’s deploy/ folder.

Note

To deploy your client extension to Liferay SaaS, use the Liferay Cloud Command-Line Tool to run lcp deploy.

Tip

To deploy all client extensions in the workspace simultaneously, run the command from the client-extensions/ folder.

Confirm the deployment in your Liferay instance’s console:

STARTED liferaysamplecustomelement1_7.4.13

Now that your client extension is deployed, check if the widget is working properly.

Add the Custom Element Client Extension as a Widget

  1. Click Edit (Edit) at the top of any page.

  2. Add the widget to the page. In the Fragments and Widgets sidebar (Fragments and Widgets), click Widgets.

  3. Find the Client Extensions → Liferay Sample Custom Element 1 widget and drag it onto the page. Click Publish.

    Drag the Liferay Sample Custom Element 1 onto a page.

Confirm the widget app is working by using the buttons to increase/decrease the counter.

You have successfully used a Custom Element client extension in Liferay. Next, try working with the routes in a React custom element.