Deep dive and best practices
This tutorial will cover some finer details and best practices when designing and developing UI5 Web Components.
Metadata deep dive
The metadata defines the public API of your component. Among other things, here you define:
- the tag name
- what properties/attributes (and of what type) your component supports
- what slots your component supports
- what events your component fires
Tag
The tag name must include a -
as required for any custom element. The tag is declared using @customElement
decorator:
@custom("my-component")
//or
@custom({
tag: "my-component"
})
and then the usage is:
<my-component></my-component>
The tag
, as defined is referred to as the "pure tag", meaning it is not suffixed (scoping is not used).
Important: the pure tag name of every UI5 Web Component is always set as an attribute to the component too.
For example, when you create a ui5-button
:
<ui5-button id="b1" class="button1" design="Emphasized"></ui5-button>
the framework will create an empty attribute with the name ui5-button
too, so the actual DOM would look like this:
<ui5-button id="b1" class="button1" design="Emphasized" ui5-button></ui5-button>
Even if a suffix for tag names is configured (when scoping is enabled), the attribute with the pure tag name will be the same.
For example, if the configured suffix is -demo
and all components are used with this suffix:
<ui5-button-demo id="b1" class="button1" design="Emphasized" ui5-button></ui5-button-demo>
the attribute will still be the same (ui5-button
as opposed to the tag name of ui5-button-demo
).
Therefore, the best practice when developing UI5 Web Components is to write CSS selectors for the shadow roots using attribute selectors, instead of tag selectors.
For example, if the Demo.hbs
file looks like this:
<div class="my-component">
<ui5-button id="openBtn">Open</ui5-button>
<div>
<slot></slot>
</div>
<ui5-list></ui5-list>
</div>
you should not write selectors by tag name for other components in the Demo.css
file:
ui5-button {
width: 50px;
}
because, as stated above, the tag name could be suffixed and is not guaranteed to always be the same as the pure tag name.
Instead, use the attribute selector:
[ui5-button] {
width: 50px;
}
or another type of selector (for example by ID):
#openBtn {
width: 50px;
}
Properties
Properties are managed state
The framework will create a getter/setter pair on your component's prototype for each property, defined with @property
decorator.
For example, defining text
property:
@property()
text = ""
you can use the text
getter/setter on this component's instances:
let t = myComponent.text;
myComponent.text = "New text";
Whenever text
is read or set, the framework-defined getter/setter will be called and thus the framework will be in control of the property.
Properties vs attributes
The properties
defined via the @property
decorator results in both properties and attributes for your component. By default, for each property (camelCase
name) an attribute with the
same name but in kebab-case
is supported. Properties of type Object
have no attribute counterparts. If you wish to not have an attribute for a given property regardless of type, you can configure it with noAttribute: true
setting.
For example, defining headerText
property:
@property()
headerText = ""
you can use both the headerText
property and header-text
attribute:
let t = myComponent.text;
myComponent.headerText = "New text";
myComponent.setAttrbite("header-text", "New text");
Public vs private properties
The framework does not distinguish between public and private properties. You can treat some properties as private in a sense that you can document them as such and not advertise them to users.
The usual convention is that private properties start with an _
, but this is not mandatory. In the end, all properties defined in the metadata, public or private,
are component state, therefore cause the component to be invalidated and subsequently re-rendered, when changed.
Property types and default values
The most common types of properties are String
, Boolean
, Object
and Number
.
Most property types can have a default value, but Boolean
properties should always default to false
. When a boolean attribute is absent, it's treated as false, therefore, the default value of an attribute must be always false.
For example, defining different types of properties:
class MyComponent extends UI5Element {
@property()
text = "Hello";
@property({ type: Number, noAttribute: true })
width = 1024;
@property({ type: Number })
scale = 0.5;
@property({ type: Object })
data = {};
/**
* @private
*/
@property({ type: Boolean })
_isPhone = false;
}
Here text
, width
, scale
and data
are public properties, and _isPhone
private, but only by convention. If the user (or the component internally) changes any of these properties, the component will be invalidated.
Best practices for using properties
-
Аvoid directly modifying public properties from within a component, as these properties are typically controlled by the parent application. The only exception to this rule is when the property change results directly from user interaction (e.g., updating a value after a user types in an input field, or toggling a checked property after a user clicks a checkbox). Additionally, whenever you modify a public property due to user interaction, it's important to fire an event to notify the parent application. This ensures that the application can synchronize its state accordingly.
-
As for private properties, the best practice is to only change them internally and never let the application know about their existence.
-
Using attribute selectors instead of setting and using CSS classes on your component. Both public and private properties are great ways to create CSS selectors for your component with the
:host()
selector. The:host()
selector targets the custom element itself, and can be combined with other selectors.
For example, using the size
property (respectively the attribute with the same name) to change component's dimensions for certain values - size="XS"
:
:host {
height: 5rem;
width: 5rem;
}
:host([size="XS"]) {
height: 2rem;
width: 2rem;
}
<my-comopnent size="XS"></my-comopnent> <!-- :host() targets my-component -->
Metadata properties vs standard JS properties
It is important not to confuse properties defined with @property
decorator with regular Javascript properties.
You can create any number of properties on your component's instance, f.e.:
constructor() {
super();
this._isMobile = false;
}
However, only metadata-defined properties are managed by the framework: cause invalidation and are converted to/from attributes. Feel free to create as many regular JS properties for the purpose of your component's functionality as you need, but bear in mind that they will not be managed by the framework.
Events
Most UI5 components emit events to inform the application about user interactions. Defining and firing events involves several key aspects:
Describing the Event
Use the @event
decorator to define the event. If the event name consists of multiple words, use kebab-case:
@event("selection-change", {
detail: {
valid: { type: Boolean },
},
})
class MyDemoComponent extends UI5Element {
}
Firing the Event
The fireEvent
method
Use the UI5Element#fireEvent
method to trigger the event:
@event("selection-change", {
detail: {
valid: { type: Boolean },
},
})
class MyDemoComponent extends UI5Element {
onItemSelected(e: Event) {
this.fireEvent("selection-change", {
valid: true,
});
}
}
By defualt when using fireEvent
it assumes the event is bubbling (bubbles: true) and not preventable (cancelable: false).
- Fire event with default configuration
// Fires the event as NOT preventable and bubbling
this.fireEvent("change");
- Fire event with non-default configuration
The method allows configuring the cancelable
and bubbles
fields via function arguments - the third and fourth parameters respectively.
// Fires the event as preventable and non-bubbling
this.fireEvent("change", {}, true, false);
The fireDecoratorEvent
method
Use the UI5Element#fireDecoratorEvent
method to trigger the event.
The method is available since version v2.4.0
and it is similar to fireEvent
. It fires a custom event, but gets the configuration for the event from the @event
decorator. In case you rely on the decorator settings, you must use the fireDecoratorEvent
method.
Keep in mind that cancelable
and bubbles
are false
by default and you must explicitly enable them in the @event
decorator if required.
- Fire event with default configuration
@event("change")
// Fires the event as NOT preventable and NOT bubbling
this.fireDecoratorEvent("change");
- Fire event with non-default configuration
@event("change", {
bubbles: true // false by default
cancelable: true // false by default
})
// Fires the event as preventable and bubbling
this.fireDecoratorEvent("change");
Note: since v2.4.0
it's recommended to describe the event in the @event
decorator and use the fireDecoratorEvent
method.
Describing the Event Detail
When an event includes a detail it's recommended to create a TypeScript type that describes the event detail and use it in the fireEvent
or fireDecoratorEvent
(as generic methods) to force static checks ensuring that proper event detail is passed.
The naming convention for the type is a combination of the component class name ("MyDemoComponent"), the event name ("SelectionChange"), followed by "EventDetail", written in PascalCase, e.g "MyDemoComponentSelectionChangeEventDetail":
export type MyDemoComponentSelectionChangeEventDetail = {
valid: boolean;
};
@event<MyDemoComponentSelectionChangeEventDetail>("selection-change", {
detail: {
valid: { type: Boolean },
},
})
class MyDemoComponent extends UI5Element {
onItemSelected(e: Event) {
this.fireDecoratorEvent<MyDemoComponentSelectionChangeEventDetail>("selection-change", {
valid: true,
});
}
}
Note: it's a best practice to export the type to make it available for outside usage.
Handling Events in Templates
When attaching event handlers within your component's template for events fired by other web components, use the ui5-
prefix for the event name.
For example, if a ui5-list component emits a selection-change
event, handle it using the ui5-selection-change
event name:
By default, events are fired in pairs: one with the standard name and another prefixed with ui5-
. While the ui5-
prefixed event is always emitted, the non-prefixed event can be suppressed if the noConflict
configuration setting is enabled. In this case, only the prefixed event will be triggered. For more details on the noConflict
setting, refer to the Configuration section.
Preventable Events
It's common to prevent certain events in an application. You must configure the cancelable
setting in the @event
decorator to make the event preventable.
@event("change", {
cancelable: true // false by default
})
You most likely will need to update (or revert) the component's state when an event is prevented by the consuming side. To determine if an event was prevented, check the return value of the fireDecoratorEvent
method. It returns false if the event was cancelled (preventDefault
was called) and true otherwise:
@event("change", {
cancelable: true // false by default
})
class Switch extends UI5Element {
toggle() {
this.checked = !this.checked;
const changePrevented = !this.fireDecoratorEvent("change");
if (changePrevented) {
this.checked = !this.checked;
}
}
}
Slots
Web Components offer a slot
mechanism for component composition, allowing components to render children
or other components in specific locations within their shadow root.
To enable slotting for your component, simply add a <slot>
element within your .hbs
template.
This acts as a placeholder that can be filled with any HTML markup.
{{!-- MyDemoComponent.hbs --}}
<div class="my-component-root">
<slot></slot>
</div>
On the consuming side, you can insert HTML elements into your component:
<!-- index.html -->
<my-demo-component>
<span>Hello World</span>
</my-demo-component>
For documentation purposes and to inform component consumers about the available slot, we should describe it with a brief JSDoc comment at component class level as shown below:
/*
* @slot {Array<Node>} default - Defines the content of the component.
*/
@customElement({
tag: "ui5-demo-component",
})
class MyDemoComponent extends UI5Element {}
Slot as Class Member
We can define our slots as class members via the @slot
decorator as follows:
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot()
items!: Array<HTMLElement>;
}
Defining a slot with the @slot
decorator means that this slot will be managed by the framework:
- If any of the children are custom elements, the framework will wait until they are all defined and upgraded before rendering the component.
- The component will be re-rendered when its children are added, removed, or rearranged.
Also, we define slots as class members when we need to access the slotted children for some reason. For example, to get the slotted elements count:
const itemsCount = this.items.length;
Or, to read some state of the slotted elements:
const hasDisabledItem = this.items.some(el => el.disabled);
Or, sometimes even set some private state on the slotted elements:
this.items.forEach((item, key) => {
const isLastChild = key === this.items.length - 1;
item.showBorder = isLastChild;
});
All slots, declared with the @slot
decorator, are arrays with elements of type Node or HTMLElement.
So, you can safely and must declare slots (by convention) with !:
as the accessor will return an empty array in the worst case.
Also, when you declare slots as class members, you can document them in place - you don't need to describe them at class level as mentioned in the previous section.
/**
* Defines the items of the component.
* @public
*/
@slot()
items: Array<HTMLElement>
Default and Named Slot
Default slot is the one that can be used without setting the slot
attribute of the slotted elements, while
named slot requires setting the slot
attribute:
- Default slot
{{!-- MyDemoComponent.hbs --}}
<div class="my-component-root">
<slot></slot>
</div>
<!-- index.html -->
<my-demo-component>
<span>Hello World</span>
</my-demo-component>
- Named slot
The named slot requires a small change in the component's template. You must pass the name
attrbite to the slot
element:
{{!-- MyDemoComponent.hbs --}}
<div class="my-component-root">
<slot name="content"></slot>
</div>
```html
<!-- index.html -->
<my-demo-component>
<span slot="content">Hello World</span>
</my-demo-component>
- Declare default slot
All slots are named if you simply use the @slot
decorator without any settings, while the default slots must be explicitly marked as such with the "default"
setting:
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement, "default": true })
content!: Array<HTMLElement>;
}
- Declare named slot
Simply use the @slot
decorator without any settings:
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot()
content!: Array<HTMLElement>;
}
It's a good practice is to make use of the default slot as it requires less code to use your component. And, if your component has multiple slots - to pick the most important and used one as the default.
For example, here we assume that the "content" slot is more important and we declared it as default.
{{!-- MyDemoComponent.hbs --}}
<div class="my-component-root">
<div class="my-component-heading">
<slot name="heading"></slot>
</div>
<slot></slot>
</div>
<!-- index.html -->
<my-demo-component>
<h1 slot="heading">Heading</h1>
<span>Hello World</span>
</my-demo-component>
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement, "default": true })
content!: Array<HTMLElement>;
@slot()
heading!: Array<HTMLElement>;
}
Note: If the slot configuration object is not provided (e.g. @slot()
), HTMLElement
will be used as the default type.
However, if you provide this object, the type
field is mandatory.
Individual Slots
The @slot
decorator provides an option called individualSlots
, which is of boolean type. This option determines if each child element will be placed in its own slot, allowing for flexible arrangement or wrapping of the children within the component. When individualSlots
is enabled, the framework assigns a unique _individualSlot
property to each child element. This property can then be used within the component's template, as shown in the following example.
First, enable individualSlots
by setting it to true
:
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement, individualSlots: true })
content!: Array<HTMLElement>;
}
Next, iterate over the child elements in the template, using the _individualSlot
property in the name attribute of the slot element:
{{#each mySlot}}
<slot name="{{this._individualSlot}}"></slot>
{{/each}}
Here is an example using the Carousel
web component, which leverages individualSlots
to wrap each slotted child within the content slot to achieve a specific design:
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement("ui5-carousel")
class Carousel extends UI5Element {
@slot({ type: HTMLElement, individualSlots: true })
content!: Array<HTMLElement>;
}
{{!-- Carousel.hbs --}}
<div>
{{#each content}}
<div
class="ui5-carousel-item"
role="option"
aria-posinset="{{posinset}}"
aria-setsize="{{setsize}}"
aria-selected = "{{selected}}"
>
<slot name="{{this.item._individualSlot}}"></slot>
</div>
{{/each}}
</div>
Note: When individualSlots
is enabled, the _individualSlot
property is assigned to each direct child. The value of _individualSlot
follows the pattern {nameOfTheSlot}-{index}
, and the slot attribute is updated accordingly.
Invalidation on Child Change
The @slot
decorator offers an invalidateOnChildChange
option, which can be set as a boolean or a configuration object. This option determines whether a component should be invalidated when changes occur within its child elements.
By default, if child elements are added or removed from a slot, the component will be invalidated automatically. The invalidateOnChildChange
option goes a step further by triggering invalidation even when properties or slots of the child elements change. This is useful if the state of parent component depends on the state of its children.
The simplest way to use this option is to set invalidateOnChildChange
to "true"
. This configuration ensures that the my-demo-component
web component will be invalidated whenever any of the UI5Element instances slotted into the content slot are updated, whether due to a property or slot change.
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement, invalidateOnChildChange: true })
content!: Array<HTMLElement>;
}
For more specific scenarios, you can use a more detailed configuration. The following example demonstrates how to invalidate the "my-demo-component"
web component only when certain properties or slots of the slotted UI5Element instances change. In this case, the component will be invalidated if the "myProp" property or the "mySlot" slot of the child elements are modified.
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement("my-demo-component")
class MyDemoComponent extends UI5Element {
@slot({ type: HTMLElement, invalidateOnChildChange: { properties: ["myProp"], slots: ["mySlot"] }})
content!: Array<HTMLElement>;
}
The invalidateOnChildChange
option is especially useful when working with "abstract" elements option is particularly useful when dealing with "abstract" elements, such as UI5Element instances that do not have their own templates. In these cases, the parent component is responsible for rendering the content based on the state of its child elements.
For instance, consider a Wizard
web component that accepts WizardStep
elements in its "steps"
slot. Since WizardStep
does not have its own template, the Wizard
must handle rendering based on the properties and state of the steps. Therefore, the Wizard
needs to be invalidated whenever any changes occur within its child elements to ensure proper rendering.
class Wizard extends UI5Element {
@slot({
"default": true,
type: HTMLElement,
invalidateOnChildChange: true,
})
steps!: Array<WizardStep>
}
<ui5-wizard>
<ui5-wizard-step title-text="Product type" icon="sap-icon://product" selected></ui5-wizard-step>
<ui5-wizard-step title-text="Options"></ui5-wizard-step>
<ui5-wizard-step title-text="Pricing" disabled></ui5-wizard-step>
</ui5-wizard>
{{!-- Wizard.hbs --}}
<div class="ui5-wizard-root">
<nav>
{{!-- _steps is a calculated state based on the steps slot --}}
{{#each _steps}}
<div class="ui5-wiz-step-root">
</div>
{{/each}}
</nav>
</div>
Note: The invalidateOnChildChange
option is meant to be used with slots that are UI5Element instances.
Styling of Slotted Children
The :slotted
CSS selector applies to any element that has been placed into a slot.
It works when used inside CSS placed within the shadow DOM of the component that offers the slot.
For example:
<!-- index.html -->
<my-demo-component>
<h1 slot="heading">Heading</h1>
<span>Hello World</span>
</my-demo-component>
/* MyDemoComponent.css */
::slotted([slot="heading"]) {
width: 200px;
height: 100px;
}
Understanding rendering
What is rendering?
In the context of UI5 Web Components the notion of rendering means creating the content of a shadow root (building the shadow DOM).
Physical and logical components
Each component that has a template
described in @customElement
decorator will be rendered (will have its shadow DOM built) initially and every time it gets invalidated.
Example:
import MyComponentTemplate from "./generated/templates/MyComponentTemplate.lit.js";
@customElement({
template: MyComponentTemplate
})
Components that do not have template
defined in @customElement
decorator are considered logical or marker elements only. These components are never rendered (do not have a shadow root at all)
and their only purpose is to serve as items for higher-order components. The classical example of a logical component is a select option.
Example:
<ui5-calendar>
<ui5-date></ui5-date>
</ui5-calendar>
The ui5-date
component does not have template, and is therefore never rendered. However, the ui5-calendar
component, which is a physical component that has a template,
renders HTML corresponding to each of its children (ui5-date
instances) as part of its own shadow DOM.
What is invalidation?
Invalidation means scheduling an already rendered component for asynchronous re-rendering (in the next animation frame). If an already invalidated component gets changed again, before having been re-rendered, this will have no downside - it's in the queue of components to be re-rendered anyway.
Important: when a component is re-rendered, only the parts of its shadow DOM, dependent on the changed properties/slots are changed, which makes most updates very fast.
A component becomes invalidated whenever:
- a metadata-defined property changes (not regular properties that f.e. you define in the constructor)
- children are added/removed/rearranged in any slot declared with
@slot
decorator. - a slotted child in a slot configured with
invalidateOnChildChange: true
is invalidated.
Changes to properties always cause an invalidation. No specific metadata configuration is needed.
@property()
text?: string;
Whenever text
changes, the component will be invalidated.
As we defined earlier there two kind of slots - unnamed and named. Unnamed slots do not cause an invalidation. Most components do not need to render differently based on whether they have any slotted children or not. This component will not invalidate when children are added/removed from any of its unnamed slots.
However, some components render differently based on whether they have children or not (e.g. show counters/other UX elements for the number of children, f.e. carousel; or have special styles when empty or have a child in a specific slot, f.e. button with an icon).
If that is the case for the component you're building, you need to define slot using slot
decorator. Thus, your component will become invalidated whenever children are added, removed or swap places in any of its slots.
@slot({ type: HTMLElement, "default": true })
content!: Array<HTMLElement>;
@slot()
header!: Array<HTMLElement>;
@slot()
footer!: Array<HTMLElement>;
Now that this component has slots defined with @slot
decorator, changes to each of these slots will trigger an invalidation.
And finally, there are components that not only need to render differently based on the number/type of children they have, but they must also get invalidated whenever their children change. This holds true for all components that work with abstract items (such as select with options, combo box with combo box items) because these abstract items do not have a template (do not render themselves) and therefore rely on their parent to render some DOM for them in its own shadow root. So, when they get invalidated, they must also invalidate their parent.
@slot({ type: HTMLElement, "default": true, invalidateOnChildChange: true })
content!: Array<HTMLElement>;
@slot()
header!: Array<HTMLElement>;
@slot()
footer!: Array<HTMLElement>;
Only changes to children in the "content" slot will trigger invalidation for this component. Note that invalidateOnChildChange
is defined per slot.
Finally, invalidateOnChildChange
allows for more fine-granular rules when exactly children can invalidate their parents.
Lifecycle hooks
Using the right lifecycle hook for the task is crucial to a well-designed and performant component.
constructor
Use the constructor for one-time initialization tasks.
What to do:
- bind functions to
this
(very common when using theResizeHandler
helper class) - do one-time work when the first instance of a given component is created (f.e. instantiate a helper class or attach a special event listener to the
window
object)
What not to do:
- anything rendering-related (use
onBeforeRendering
/onAfterRendering
) - anything related to the state (use
onBeforeRendering
) - anything requiring DOM manipulation (the component isn't attached to the DOM yet - use
onAfterRendering
oronEnterDOM
/onExitDOM
)
Example:
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import ItemNavigation from "@ui5/webcomponents-base/dist/delegate/ItemNavigation.js";
@customElement({
tag: "my-component",
})
class MyComponent extends UI5Element {
_itemNavigation: ItemNavigation;
_handleResizeBound: ResizeObserverCallback;
constructor() {
super();
// bind a method once so that you can pass the same function to register/deregister-based helpers
this._handleResizeBound = this._handleResize.bind(this);
// do one-time work when the first instance of a component is created
if (!isGlobalHandlerAttached) {
document.addEventListener("mouseup", this._deactivate);
isGlobalHandlerAttached = true;
}
// initialize a helper class for the instance
this._itemNavigation = new ItemNavigation(this, {
navigationMode: NavigationMode.Horizontal,
getItemsCallback: () => this._getFocusableItems(),
});
}
}
onBeforeRendering
Use onBeforeRendering
to prepare variables to be used in the .hbs
template.
What to do:
- prepare calculated (derived) state for use in the renderer
What not to do:
- do not try to access the DOM (use
onAfterRendering
instead)
Let's take for example a component with the following metadata:
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
@customElement({
tag: "my-component",
})
class MyComponent extends UI5Element {
@property()
filter = "";
@slot({ type: HTMLElement, individualSlots: true, "default": true })
items!: Array<HTMLElement>
}
This component has a filter
property and a default
slot that we want to call items
(thus accessible with this.items
).
Let's imagine we want to only show the items whose name
property matches the value of our filter
property - so we filter the items by name.
class MyComponent extends UI5Element {
@property()
filter = "";
@slot({ type: HTMLElement, individualSlots: true, "default": true })
items!: Array<HTMLElement>
this._filteredItems = [];
onBeforeRendering() {
this._filteredItems = this.items.filter(item => item.name.includes(this.filter));
}
}
In onBeforeRendering
we prepare a _filteredItems
array with some of the component's children (only the ones that have the this.filter
text as part of their name
property)
And finally, in the .hbs
template we have for example:
We loop over the _fiteredItems
array that we prepared in onBeforeRendering
and for each child we render a slot
based on the child's _individualSlot
property,
created automatically by the framework due to the default slot's metadata configuration (individualSlots: true
).
The usage of this component would be for example:
<my-filter-component filter="John">
<my-filter-item name="John Smith"></my-filter-item>
<my-filter-item name="Jane Doe"></my-filter-item>
<my-filter-item name="Jack Johnson"></my-filter-item>
</my-filter-component>
The user would only see the first and third items as these are the only ones we rendered an individual slot for (the ones matching the filter
value of "John").
In summary: onBeforeRendering
is the best place to prepare all the variables you are going to need in the .hbs
template.
onAfterRendering
The onAfterRendering
lifecycle hook allows you to access the DOM every time the component is rendered.
You should avoid using this method whenever possible. It's best to delegate all HTML manipulation to the framework: change the state of the component, the component will be invalidated, the template will be executed with the latest state, and DOM will be updated accordingly. It is an anti-pattern to manually change the DOM.
In some cases, however, you must directly access the DOM since certain operations can only be performed imperatively (and not via the template):
- setting the focus;
- manually scrolling an element to a certain position;
- calling a public method on a DOM Element (for example, to close a popup);
- reading the sizes of DOM Elements;
Example:
<div class="my-component">
<input id="first">
<input id="second">
</div>
onAfterRendering() {
this.shadowRoot.querySelector("#second").focus();
this._totalWidth = this.shadowRoot.querySelector("div.my-component").offsetWidth;
}
onEnterDOM
and onExitDOM
Unlike onBeforeRendering
and onAfterRendering
, which sound like parts of the same flow (but are not, and are actually used for completely independent tasks),
onEnterDOM
and onExitDOM
should almost always be used together, therefore they are presented as a whole in this article.
onEnterDOM
is executed during the web component's standardconnectedCallback
method's executiononExitDOM
is executed during the web component's standarddisconnectedCallback
method's execution
If you have prior experience with web component development, you could think of onEnterDOM
as connectedCallback
and of onExitDOM
as disconnectedCallback
.
Note that these hooks are completely independent of the component's rendering lifecycle, and are solely related to its insertion and removal from DOM.
Normally, when a web component is created, for example:
const b = document.createElement("my-component");
it is already fully operational, although it isn't in DOM yet. Therefore, you should use onEnterDOM
and onExitDOM
only for functionality, related to
the component being in the DOM tree at all (and not to rendering, stying or anything related to the shadow root).
Common use cases are:
- registering/de-registering a ResizeHandler
- working with Intersection observer
- any work you want to carry out only if the component is in the DOM;
Probably the best example of these hooks is the usage of the ResizeHandler
helper class.
The component has a private _width
property, defined and the following code in its class:
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
@customElement({
tag: "my-component",
})
class MyComponent extends UI5Element {
@property({ type: Number })
_width = 0;
constructor() {
super();
this._fnOnResize = this._onResize.bind(this);
}
onEnterDOM() {
ResizeHandler.register(this, this._fnOnResize);
}
onExitDOM() {
ResizeHandler.deregister(this, this._fnOnResize);
}
_onResize() {
this._width = this.offsetWidth;
}
get styles() {
return {
valueStateMsgPopover: {
"max-width": `${this._width}px`,
},
};
}
}
In the constructor
we bind the _onResize
method to the component's instance to get a function with the correct context,
and then in onEnterDOM
and onExitDOM
we register/deregister this function with the ResizeHandler
helper class.
Then, whenever the component resizes, the ResizeHandler
will trigger the callback, the metadata _width
property will be updated to a new value in _onResize
,
the component will be invalidated, and the template will be executed with the new value of _width
, respectively styles
.