Widget Class
The Widget class is the base class for all Serenity widgets. It lives in @serenity-is/corelib.
A widget wraps a DOM node, manages its lifecycle (create/destroy), associates itself with its element for later lookup, and provides helpers for id prefixes, validation and rendering.
A sample Widget
Let's build a widget that increases a DIV's font size every time it is clicked:
import { Widget } from "@serenity-is/corelib";
export class MyCoolWidget extends Widget {
private fontSize = 10;
constructor(props: { element: HTMLElement }) {
super(props);
this.element.on("click", () => {
this.fontSize++;
this.element.css("font-size", this.fontSize + "pt");
});
}
}
<div id="SomeDiv">Sample Text</div>
We can create this widget on an HTML element by passing the element (or a selector) through the element prop:
new MyCoolWidget({ element: "#SomeDiv" });
Widget Class Members
export class Widget<P = {}> {
protected readonly options: WidgetProps<P>;
public readonly uniqueName: string;
public readonly idPrefix: string;
public readonly domNode: HTMLElement;
constructor(props: WidgetProps<P>);
public destroy(): void;
public get element(): Fluent;
public get props(): WidgetProps<P>;
public init(): this;
public render(): any;
public addValidationRule(rule, uniqueName?): void;
public change(handler): void;
public changeSelect2(handler): void;
public getGridField(): Fluent;
protected addCssClass(): void;
protected deferRender(): boolean;
protected getCssClass(): string;
protected byId<TElement>(id): Fluent<TElement>;
protected findById<TElement>(id): TElement;
protected renderContents(): any;
protected legacyTemplateRender(): boolean;
protected useIdPrefix(): IdPrefixType;
protected afterRender(callback): void;
protected syncOrAsyncThen(syncMethod, asyncMethod, then): void;
static createDefaultElement(): HTMLElement;
static getWidgetName(type): string;
static create<TWidget, P>(params): TWidget;
}
Widget Lifecycle
Creating a widget runs the following steps in the constructor:
- Resolve the DOM node — from the
elementprop (an element, selector, array-like, or callback), or a new element fromcreateDefaultElement()(adivby default). - Set element props —
id,class,name,placeholder, etc. are applied to the DOM node. - Assign a unique name —
uniqueNameis derived from the type name plus a counter. - Associate the widget —
associateWidgetregisters the widget in aWeakMapkeyed by the DOM node, so it can be retrieved later withgetWidgetFrom/tryGetWidget. - Register disposal —
addDisposingListenerregistersdestroyso it runs automatically when the DOM node receives adisposingevent. - Compute the id prefix —
idPrefixdefaults touniqueName + '_'. - Add the CSS class —
addCssClassapplies the widget'ss-classes. - Render contents —
renderContents()is called (unlessdeferRender()returnstrue).
Destroying a widget runs the reverse:
- The DOM node is removed (for example via
Fluent.remove()orempty()), which fires thedisposingevent. - The disposing listener calls
destroy(). destroy()deassociates the widget, removes its CSS classes, and detaches event handlers namespaced withuniqueName.
Widget.domNode and Widget.element
The domNode property holds the raw HTMLElement the widget is bound to. The element getter returns a Fluent wrapper around it, which is the idiomatic way to interact with the element:
this.element.on("click", () => { /* ... */ });
this.element.css("font-size", "12pt");
HTML Element and Widget CSS Class
When a widget is created on an HTML element, it adds a CSS class based on the widget's type. In our sample, .s-MyCoolWidget is added to the DIV with ID #SomeDiv:
<div id="SomeDiv" class="s-MyCoolWidget">Sample Text</div>
The CSS class is generated by getCssClass(), which combines the full type name (dots replaced with dashes), the short type name, and — when the full name starts with a configured root namespace — the name with that namespace stripped, each prefixed with s-. For example, MySamples.MyCoolWidget produces s-MySamples-MyCoolWidget s-MyCoolWidget.
Styling the HTML Element With the Widget CSS Class
The widget CSS class can be used to style the HTML element the widget is created on:
.s-MyCoolWidget {
background-color: red;
}
Getting a Widget Reference From an HTML Element
Widgets are associated with their DOM node in an internal WeakMap keyed by the widget's type name (full type name with dots replaced by underscores). This association is created by associateWidget in the constructor and removed by deassociateWidget in destroy.
To retrieve a widget from an element, use getWidgetFrom (throws if not found) or tryGetWidget (returns null if not found):
import { getWidgetFrom, tryGetWidget } from "@serenity-is/corelib";
// Throws if the element has no such widget
const myWidget = getWidgetFrom<MyCoolWidget>("#SomeDiv", MyCoolWidget);
// Returns null if the element has no such widget
const maybe = tryGetWidget<MyCoolWidget>("#SomeDiv", MyCoolWidget);
Both accept an element, an array-like of elements, or a selector string. When the type argument is omitted, the first associated widget is returned.
Fluent.getWidget and Fluent.tryGetWidget
The same helpers are exposed as Fluent extension methods, so they can be chained off a Fluent wrapper:
const myWidget = this.element.getWidget<MyCoolWidget>(MyCoolWidget);
const maybe = this.element.tryGetWidget<MyCoolWidget>(MyCoolWidget);
getWidget throws an error if no matching widget is found:
Element (...) has no widget of type 'MySamples_MyCoolWidget'!
Creating Multiple Widgets on an HTML Element
Only one widget of the same class can be attached to an HTML element. An attempt to create a second widget of the same class on an element throws:
The element already has widget 'MySamples_MyCoolWidget'!
Any number of widgets from different classes can be attached to a single element as long as their behaviour doesn't affect each other.
Widget.uniqueName Property
Every widget instance gets a unique name like MySamples_MyCoolWidget3 automatically, accessible via the uniqueName property. It is derived from the widget's type name plus an incrementing counter.
This unique name is useful as an id prefix for the HTML element and its descendants generated by the widget, and as an event namespace so handlers can be attached/detached without affecting other handlers on the element:
this.element.on("click." + this.uniqueName, () => { /* ... */ });
// ...
this.element.off("click." + this.uniqueName);
Widget.idPrefix Property
The idPrefix property defaults to uniqueName + '_' (or the idPrefix option if provided). It is used by byId, findById and useIdPrefix to resolve child element ids:
// resolves '#MySamples_MyCoolWidget3_SomeChild'
this.byId("SomeChild").on("click", () => { /* ... */ });
Widget.Destroy Method
Sometimes releasing an attached widget is required without removing the HTML element itself. The destroy method handles this: it removes the disposing listener, deassociates the widget from its DOM node, removes its CSS class, and detaches event handlers namespaced with uniqueName.
destroy is called automatically when the DOM node is removed from the document (via a disposing listener), and can also be called manually. If destroy is not performed correctly, memory leaks may occur.
The disposing event is fired by Fluent.remove() and Fluent.empty() (and by jQuery-based removal when jQuery is loaded). This is the same disposal mechanism used by DomWise signal subscriptions, so widgets and reactive UI clean up together when their DOM node is removed.
Rendering and Initialization
renderContents()— override to provide the widget's contents. The default returns thechildrenprop, or renders a legacygetTemplate()string if one is defined.deferRender()— returntrueto defer rendering untilinit()is called.init()— renders the widget's contents if rendering was deferred, then returnsthis.afterRender(callback)— queues a callback to run after the widget's contents are rendered.render()— returns the widget's main element (or the document fragment when the widget is rendered into a fragment).
renderContents vs render
Override renderContents() to provide a widget's contents — not render(). The render() method returns the widget's main element (or the document fragment when the widget is rendered into a fragment) and should not be overridden, because widgets may get their elements from props rather than being regular JSX widgets.
Widget.create
Widget.create is a static helper that creates a widget instance, appends its element to a container, and invokes optional element and init callbacks:
Widget.create({
type: MyCoolWidget,
options: { /* ... */ },
container: document.querySelector("#SomeContainer"),
init: (w) => { /* ... */ }
});
Validation and Events
addValidationRule(rule, uniqueName?)— adds a validation rule to the widget's DOM node.change(handler)— registers achangehandler namespaced withuniqueName.changeSelect2(handler)— likechange, but ignores changes originating from combobox value setting.getGridField()— returns aFluentwrapper for the closest.fieldelement containing the widget's DOM node.
Widget With Options
If a widget requires additional initialization options, derive from the generic Widget<P> class, where P is the options/props type. The options are available through the protected options field and the public props getter.
export interface MyCoolWidgetOptions {
step?: number;
unit?: string;
}
export class MyCoolWidget extends Widget<MyCoolWidgetOptions> {
constructor(props: MyCoolWidgetOptions & WidgetProps<{}>) {
super(props);
const step = this.options.step ?? 1;
const unit = this.options.unit ?? "pt";
// ...
}
}
The WidgetProps<P> type adds common element props (id, class, element) to the widget's own options, so a widget can be created with either an existing element or a selector:
new MyCoolWidget({ element: "#SomeDiv", step: 2, unit: "px" });
The element prop accepts an HTMLElement, an array-like of elements, a selector string, or a callback that receives the created element. When no element is provided, the widget creates one via createDefaultElement() (a div by default).
See Also
- Widget (API reference) — the full
WidgetAPI. - PrefixedContext Class
- Widgets
- Frontend Framework Overview