Skip to main content

Developing Custom UI5 Web Components

Note: All examples in this tutorial are taken from the Demo UI5 Web Component (ui5-demo), generated by the package initialization script. For more information on creating a new package with a demo web component inside, click here.

The Class Definition​

The main file representing the web component is Demo.js.

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";

// Template
import DemoTemplate from "./generated/templates/DemoTemplate.lit.js";

// Styles
import DemoCss from "./generated/themes/Demo.css.js";


const metadata = {
tag: "ui5-demo",
properties: {
},
slots: {
},
events: {
},
};

class Demo extends UI5Element {
static get metadata() {
return metadata;
}

static get render() {
return litRender;
}

static get template() {
return DemoTemplate;
}

static get styles() {
return DemoCss;
}


static get dependencies() {
return []; // array of components used internally
}
}

Demo.define();

export default Demo;

The UI5Element Base Class​

Every UI5 Web Component must extend the base class UI5Element, provided by the @ui5/webcomponents-base package:

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";

class Demo extends UI5Element {

}

export default Demo;

The Metadata Object​

Metadata is a JavaScript object, containing information about the public interface of a UI5 Web Component (tag name, properties, slots, events, etc.).

Metadata is passed via the metadata static getter:

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";

const metadata = {
tag: "ui5-demo",
properties: {
},
slots: {
},
events: {
},
};

class Demo extends UI5Element {
static get metadata() {
return metadata;
}
}

export default Demo;

So far, we've defined a ui5-demo web component with no properties, slots or events.

For a complete reference to all metadata entities, click here.

The Render Engine​

UI5 Web Components are agnostic of the DOM render engine used. However, all standard UI5 Web Components (@ui5/webcomponents, @ui5/webcomponents-fiori, etc.) use lit-html as the rendering technology of choice.

The render engine is defined via the render static getter:

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";

const metadata = {
tag: "ui5-demo",
properties: {
},
slots: {
},
events: {
},
};

class Demo extends UI5Element {
static get metadata() {
return metadata;
}

static get render() {
return litRender;
}
}

export default Demo;

Here we import LitRenderer.js from the @ui5/webcomponents-base package which is a very tiny wrapper around lit-html.

The Template​

Now that we've defined the rendering technology of choice, we can pass a template in that technology's syntax.

This is done via the template static getter:

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";

// Template
import DemoTemplate from "./generated/templates/DemoTemplate.lit.js";

const metadata = {
tag: "ui5-demo",
properties: {
},
slots: {
},
events: {
},
};

class Demo extends UI5Element {
static get metadata() {
return metadata;
}

static get render() {
return litRender;
}

static get template() {
return DemoTemplate;
}
}

export default Demo;

The standard UI5 Web Components use handlebars templates that are automatically converted to lit-html syntax by the build script.

If you have a Demo.hbs file along with the Demo.js file, the build script is going to create for you generated/templates/DemoTemplate.lit.js file. Therefore, we pass the content of this file to the template static getter.

For more information, see the next chapter of this tutorial.

The CSS Definitions​

You can pass CSS to be inserted in the shadow root of the UI5 Web Component by using the styles static getter:

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";

// Template
import DemoTemplate from "./generated/templates/DemoTemplate.lit.js";

// Styles
import DemoCss from "./generated/themes/Demo.css.js";

const metadata = {
tag: "ui5-demo",
properties: {
},
slots: {
},
events: {
},
};

class Demo extends UI5Element {
static get metadata() {
return metadata;
}

static get render() {
return litRender;
}

static get template() {
return DemoTemplate;
}

static get styles() {
return DemoCss;
}
}

export default Demo;

If you have a themes/Demo.css file, the build script will automatically generate for you a generated/themes/Demo.css.js file, which in addition to your component's CSS, also contains definitions for all CSS variables for the default theme. You can define your own CSS variables for each theme in the respective theme directory in themes/:

FileDescriptions
themes/sap_horizon/parameters-bundle.cssValues for the component-specific CSS variables for the sap_horizon theme.
themes/sap_horizon_hcb/parameters-bundle.cssValues for the component-specific CSS variables for the sap_horizon_hcb theme.
themes/sap_fiori_3/parameters-bundle.cssValues for the component-specific CSS variables for the sap_fiori_3 theme.
themes/sap_fiori_3_dark/parameters-bundle.cssValues for the component-specific CSS variables for the sap_fiori_3_dark theme.

For more information, see the CSS chapter of this tutorial.

The Dependencies​

You must import all other web components, used inside the .hbs template file. Otherwise, the internally used web components won't be defined.

Furthermore, in order to make use of the Scoping feature, you must list all the internally used web components in the dependencies static getter, as the framework reads the dependencies and scopes the tag names of the listed web components.

For example: If the ui5-icon tag (or any other standard or custom UI5 Web Component) is used inside the template, you must import the Icon web component and add it to the dependencies static getter as shown below.

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";

// Template
import DemoTemplate from "./generated/templates/DemoTemplate.lit.js";
// Styles
import DemoCss from "./generated/themes/Demo.css.js";

import Icon from "@ui5/webcomponents/dist/Icon.js";

const metadata = {
tag: "ui5-demo",
properties: {
},
slots: {
},
events: {
},
};

class Demo extends UI5Element {

static get metadata() {
return metadata;
}

static get render() {
return litRender;
}

static get template() {
return DemoTemplate;
}

static get styles() {
return DemoCss;
}

static get dependencies() {
return [Icon]; // array of components used internally
}
}

export default Demo;

Defining the Web Component​

Defining a web component is necessary in order to register it in the browser.

This is done by calling the UI5Element.define static method:

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";

// Template
import DemoTemplate from "./generated/templates/DemoTemplate.lit.js";

// Styles
import DemoCss from "./generated/themes/Demo.css.js";

const metadata = {
tag: "ui5-demo",
properties: {
},
slots: {
},
events: {
},
};

class Demo extends UI5Element {
static get metadata() {
return metadata;
}

static get render() {
return litRender;
}

static get template() {
return DemoTemplate;
}

static get styles() {
return DemoCss;
}

static get dependencies() {
return []; // array of components used internally
}
}

Demo.define();

export default Demo;

Adding i18n Support​

To do that, follow these steps:

  1. Get and assign an i18n bundle during component definition
await Demo.i18nBundle = getI18nBundle("my-ui5-web-components");

The getI18nBundle method is provided by the i18nBundle.js module from the @ui5/webcomponents-base package.

  1. Get texts from the bundle (in this case for the "Count" word), according to the currently configured language return Demo.i18nBundle.getText(COUNT);

  2. Create a simple getter get countText() to use it in the template later-on.

So the final source code is:

import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";

// Template
import DemoTemplate from "./generated/templates/DemoTemplate.lit.js";

// Styles
import DemoCss from "./generated/themes/Demo.css.js";

import { COUNT } from "./generated/i18n/i18n-defaults.js";


const metadata = {
tag: "ui5-demo",
properties: {
},
slots: {
},
events: {
},
};

class Demo extends UI5Element {
static get metadata() {
return metadata;
}

static get render() {
return litRender;
}

static get template() {
return DemoTemplate;
}

static get styles() {
return DemoCss;
}

static get dependencies() {
return []; // array of components used internally
}

static async onDefine() {
Demo.i18nBundle = await getI18nBundle("my-ui5-web-components");
}

get counterText() {
return Demo.i18nBundle.getText(COUNT);
}
}

Demo.define();

export default Demo;

Please, note that here we use the onDefine method of UI5Element in order to ensure that i18n resources have been fetched before the web component has been defined. The used namespace for resource registration (in this example my-ui5-web-components) is the name property of your package.json file.

Adding a Property​

To add a property, you need to change the metadata object. In this example, new count property has been added with default value 0. Also, we use a custom type Integer as validator for the property.

import Integer from "@ui5/webcomponents-base/dist/types/Integer.js";

const metadata = {
tag: "ui5-demo",
properties: {
counter: {
validator: Integer,
defaultValue: 0,
}
},
};

class Demo extends UI5Element {
static get metadata() {
return metadata;
}

static get render() {
return litRender;
}

static get template() {
return DemoTemplate;
}

static get styles() {
return DemoCss;
}

static get dependencies() {
return []; // array of components used internally
}

static async onDefine() {
Demo.i18nBundle = await getI18nBundle("my-ui5-web-components");
}

get countText() {
return Demo.i18nBundle.getText(COUNT);
}
}

Demo.define();

export default Demo;

The Template File​

The template of the web component is in the Demo.hbs file. In this particular example, it looks like this:

<div>{{countText}} :: {{count}}</div>

The context in the template is the Web Component instance, therefore you can directly use any properties/getters on the object. Here, we see the countText getter that will return the "Count" word, translated into the currently configured language and the count property, defined in the previous step (for example, in English we will get "Count :: 0").

As explained above, the .hbs file is transformed by the build script to a .js file in the lit-html syntax. More specifically, this file is provided to the Web Component class.

For a full description of the .hbs template features and syntax, see Understanding the Handlebars (.hbs) templates.

The CSS​

Let's inspect the following files (one with CSS declarations, the others with the values of the CSS variables for the themes).

FilePurpose
themes/Demo.cssAll CSS rules for the Web Component, same for all themes; will be inserted in the shadow root.
themes/sap_horizon/parameters-bundle.cssValues for the component-specific CSS variables for the sap_horizon theme.
themes/sap_horizon_hcb/parameters-bundle.cssValues for the component-specific CSS variables for the sap_horizon_hcb theme.
themes/sap_fiori_3/parameters-bundle.cssValues for the component-specific CSS variables for the sap_fiori_3 theme.
themes/sap_fiori_3_dark/parameters-bundle.cssValues for the component-specific CSS variables for the sap_fiori_3_dark theme.

In the Demo.css file, in addition to other selectors, we have:

:host {
border: 2px solid var(--ui5-demo-border-color);
background-color: var(--sapBackgroundColor);
color: var(--sapTextColor);
}

The CSS variables starting with --sap are standard and provided by the framework. All the rest are custom for the specific web component.

Respectively, the definitions file for, let's say sap_fiori_3, contains:

:root {
--ui5-demo-border-color: green;
}

What's important to understand here is that you author all the .css files listed in the table above, but the build script generates from them a single .js file for you, and this is namely the file you pass to the Web Component class: generated/themes/Demo.css.js.