Creating custom Widgets
Bryntum components extend the Widget base class and build on its rendering capabilities to produce DOM elements and
listen for events. This guide demonstrates how to create custom widgets using the compose() API.
The Widget class
At its core, a Widget is essentially a wrapper around a <div> element. When you create a widget, Bryntum
automatically creates a <div> element and provides you with a rich API to manage it:
- Lifecycle management - Construction, rendering, destruction
- Configuration system - Declarative property definitions with getters/setters
- Event handling - Built-in event system with
trigger(),on(),un() - DOM management - Element creation and manipulation via
compose() - Responsive updates - Automatic re-rendering when properties change
The compose() Method
Creating a custom widget starts with a class that extends Widget and implements the compose() method. The
compose() method returns a DomConfig object describing the widget's DOM
structure.
For example, a Link component would look like the following:
class Link extends Widget {
static configurable = {
href : null,
text : null
};
compose() {
const { href, text } = this;
return {
tag : 'a',
href,
text
};
}
}
const link = new Link({
appendTo : document.body,
text : 'The link',
href : '#foo'
});
Widgets leverage configuration properties declared in the static configurable object, and these properties automatically
get getters and setters generated. Getters accessed in the first call to compose will trigger re-composition later
when the property is changed.
What is DomConfig?
DomConfig is a plain JavaScript object that describes a DOM element and its properties. It's used throughout Bryntum for declarative DOM generation. Think of it as a "recipe" for creating HTML elements.
Standard DomConfig properties (work with DomHelper):
tag- HTML tag name (default: 'div')classorclassName- CSS classes (string or object)style- Inline styles (string or object)text- Text content (XSS-safe)html- HTML content (raw HTML string)children- Array or object of child DomConfigsdataset- Data attributes- Any standard HTML attributes (
id,src,href, etc.)
Widget-specific extensions (only work in compose()):
reference- Createsthis.referenceNameproperty pointing to the elementlisteners- Event handlers as method names
Children: Array vs Object
The children property can be either an array or an object:
// Array form - anonymous children (no references created)
children : [
{ tag : 'span', text : 'Child 1' },
{ tag : 'span', text : 'Child 2' }
]
// Object form - named children (creates references)
children : {
firstChild : { // Creates this.firstChild reference
tag : 'span',
text : 'Child 1'
},
secondChild : { // Creates this.secondChild reference
tag : 'span',
text : 'Child 2'
}
}
Use object form when you need to access child elements later (for reading values, adding listeners, etc.). Use array form when children are purely presentational.
Automatic Recomposition
The key power of compose() is reactivity: any property you read in the first call to compose() becomes a dependency.
When that property changes, compose() automatically re-runs and efficiently updates only the changed parts of the DOM.
Think of compose() as a "dependency tracker." When it runs for the first time, Bryntum records every config property you
access via this. If you read a property inside a helper method that compose() calls, it is still tracked.
link.text = 'New link text'; // compose() re-runs, DOM updates automatically
link.href = '#bar'; // compose() re-runs again
Changes are batched and applied on the next animation frame, making updates very efficient even when multiple properties change in quick succession.
When compose() Re-runs
The compose() method automatically re-runs when:
- Any property read in
compose()changes (detected via getter tracking) - The widget is hidden then shown again
recompose()is called manually (uncommon)
It does NOT re-run when:
- Properties not read in
compose()change - Methods are called that don't change tracked properties
class Counter extends Widget {
static configurable = {
count : 0,
internal : 0
};
compose() {
const { count } = this; // count is tracked
// `internal` is NOT read, so not tracked
return { text : `Count: ${count}` };
}
}
const counter = new Counter();
counter.count = 5; // Triggers `compose()` re-run
counter._internal = 10; // Does NOT trigger `compose()` re-run
Child Elements
Most widgets will need to render multiple elements. To illustrate, consider an enhancement to the Link widget to
add an external link icon. Instead of the simple <a> element, the new Link renders a <div> with one or two
child <a> elements.
class Link extends Widget {
static configurable = {
external : null,
href : null,
target : '_blank',
text : null
};
compose() {
const { external, href, target, text } = this;
return {
children : {
linkElement : {
tag : 'a',
href,
text
},
externalLinkElement : external && {
tag : 'a',
class : {
'fa' : 1,
'fa-external-link-alt' : 1
},
href,
target
}
}
};
}
}
const link = new Link({
appendTo : document.body,
text : 'The link',
href : '#foo',
external : true
});
The children property contains child elements. Element references for each named element are stored on the widget
instance using the key in the children object, i.e., link.linkElement and link.externalLinkElement.
Note how the externalLinkElement property will be null if the external config property is not set to true.
When external is true, the second child is rendered and link.externalLinkElement is set accordingly.
Change Detection Pattern
The pattern of reading properties at the top of compose() is essential for reactivity. When you destructure properties
at the start, Bryntum's tracking system records which properties your compose() method depends on.
// GOOD - Dependencies clear and tracked
compose() {
const { value, label, color } = this; // Read at top
return {
class : `widget-${color}`,
text : `${label}: ${value}`
};
}
// RISKY - Dependencies hidden in helper
compose() {
return {
text : this.formatDisplay() // Dependency tracking may not work
};
}
formatDisplay() {
return `${this.label}: ${this.value}`; // Hidden dependencies
}
Reading properties at the top makes dependencies explicit and ensures reliable reactivity.
Widget Lifecycle
Understanding the widget lifecycle is crucial for proper resource management and initialization. Here are the key lifecycle methods:
construct(config)
Called during widget instantiation, before any rendering occurs. At this point, the widget's main element (the <div>)
has already been created and is accessible via this.element, but it is not yet part of the DOM. This is where you
should initialize internal state, set up non-config properties, and register event listeners on non-DOM objects.
Always call super.construct(config) first.
class TimerWidget extends Widget {
static configurable = {
interval : 1000
};
construct(config) {
super.construct(config);
// Initialize internal state
this._tickCount = 0;
// Set up timer using `Delayable` mixin to have automatic cleanup on destroy
this.setInterval(() => {
this._tickCount++;
this.trigger('tick', { count : this._tickCount });
}, this.interval);
// Create a websocket connection (example of an external resource)
this._socket = new WebSocket('wss://example.com/socket');
this._socket.addEventListener('message', (event) => {
this.onSocketMessage(event);
});
}
onSocketMessage(event) {
// Handle incoming messages
}
doDestroy() {
if (this._socket) {
this._socket.close();
this._socket = null;
}
super.doDestroy();
}
}
onPaint()
Override onPaint() to perform setup after the widget is in the DOM. The firstPaint property indicates if this is
the initial render.
compose() {
return {
children : [
{
tag : 'button',
reference : 'myButton', // Creates this.myButton automatically
text : 'Click Me'
}
]
};
}
// Override onPaint - called automatically when widget is painted
onPaint({ firstPaint }) {
if (firstPaint) {
// Reference automatically available as this.myButton
this.myButton.addEventListener('click', (e) => {
this.onButtonClick(e);
});
}
}
NOTE: While onPaint() gives you direct access to the DOM, avoid manually updating attributes, classes, or styles that
are already defined in your compose() method. Any manual changes made here will be overwritten the next time the widget
"recomposes" due to a property change. Use onPaint() strictly for non-reactive initialization, such as initializing
third-party libraries (e.g., a D3 chart).
doDestroy()
Called when the widget is being destroyed. This is where you must clean up timers, event listeners on external
objects, and any other resources. Always call super.doDestroy() last.
doDestroy() {
// Clean up timers
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
// Clean up external event listeners
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
super.doDestroy(); // ALWAYS LAST
}
Inheritance, compose(), and Event Handling
When extending a base class such as Link, the derived class can customize the elements by implementing a compose()
method of its own. Unlike typical inherited methods, the compose() methods of all classes are automatically called
and their returned objects are merged key-by-key. Values returned by the base class are overwritten by the derived
class where key names match.
Let's build a CopyableLink widget that extends Link and adds a copy icon. We'll add it incrementally to show how
inheritance and event handling work together.
First, let's add the copy icon element:
class CopyableLink extends Link {
static configurable = {
copyIcon : 'fa-copy'
};
compose() {
const { copyIcon } = this;
return {
children : {
copyIconElement : {
tag : 'span',
class : {
'fa' : 1,
[copyIcon] : 1
}
}
}
};
}
}
The object returned by this compose() is merged with the base Link class's compose() output. The result is a
children object with three keys: linkElement and externalLinkElement (from Link) plus copyIconElement (from
CopyableLink).
Now let's make it interactive by adding an event handler. We add a listeners property to the element:
class CopyableLink extends Link {
static configurable = {
copyIcon : 'fa-copy'
};
compose() {
const { copyIcon } = this;
return {
children : {
copyIconElement : {
tag : 'span',
class : {
'fa' : 1,
[copyIcon] : 1
},
listeners : {
click : 'onCopyLink' // Method name as string
}
}
}
};
}
onCopyLink(event) {
navigator.clipboard?.writeText(this.linkElement.href);
}
}
const link = new CopyableLink({
appendTo : document.body,
text : 'Copyable link',
href : '#foo'
});
The listeners object maps event names to method names (as strings). When clicked, the onCopyLink method is called
with this bound to the widget instance, giving access to this.linkElement and other properties.
Real-World Example: ProgressBar
Let's look at Bryntum's actual ProgressBar widget as a perfect example of how simple and elegant compose-based
widgets can be:
import Widget from './Widget.js';
import DomHelper from '../helper/DomHelper.js';
DomHelper.loadStylesheet('Core/widget/ProgressBar.css');
export default class ProgressBar extends Widget {
static $name = 'ProgressBar';
static type = 'progressbar';
static configurable = {
label : '',
valueText : null,
value : 0,
valueRenderer : null,
max : null,
color : 'b-blue'
};
compose() {
const { label, max, color, value } = this;
let percentage;
if (max != null) {
percentage = Math.min(100, Math.max(0, (value / max) * 100));
}
else {
percentage = Math.min(100, Math.max(0, value * 100));
}
return {
class : 'b-progress-bar',
children : [
{
class : 'b-progress-bar-header',
children : [
{
tag : 'label',
class : 'b-label b-progress-bar-label',
text : label
},
{
tag : 'label',
class : 'b-label b-progress-bar-value',
text : this.getDisplayValue()
}
]
},
{
class : 'b-progress-bar-track',
children : [
{
class : 'b-progress-bar-fill',
style : {
width : `${percentage}%`
}
}
]
}
]
};
}
getDisplayValue() {
const me = this;
if (me.valueRenderer) {
return me.valueRenderer(me.value, me.max);
}
if (me.valueText != null) {
return me.valueText;
}
if (me.max != null) {
return `${Math.round(me.value / me.max * 100)}%`;
}
return `${Math.round(me.value * 100)}%`;
}
}
ProgressBar.initClass();
Notice how simple this is:
- No manual DOM updates - When
value,color,label, ormaxchanges,compose()automatically re-runs - Clean separation - Logic (
getDisplayValue()) is separate from rendering (compose()) - Minimal code - The entire widget is ~140 lines including JSDoc comments
- Reactive - Changes to any property used in
compose()trigger automatic re-rendering
Here's the basic CSS used by the ProgressBar widget:
.b-progress-bar {
display : flex;
flex-direction : column;
width : 100%;
margin-bottom : 1em;
.b-label {
font-size : 0.95em;
}
.b-progress-bar-label {
margin-inline-end: 3em;
}
}
.b-progress-bar-header {
display : flex;
justify-content : space-between;
align-items : center;
margin-bottom : 0.5em;
}
.b-progress-bar-track {
width : 100%;
height : 6px;
background-color : var(--b-neutral-90);
border-radius : 3px;
overflow : hidden;
position : relative;
}
.b-progress-bar-fill {
height : 100%;
border-radius : var(--b-widget-border-radius);
transition : width 0.2s ease;
background-color : var(--b-primary);
}
//<code-header>
fiddle.title = 'Simple progress bar';
//</code-header>
// Simple ProgressBar examples showing basic usage
// Add a button to toggle progress values
new Button({
insertFirst : targetElement,
text : 'Update Progress',
icon : 'fa-sync',
rendition : 'tonal',
color : 'b-blue',
style : 'margin-top: 1em;',
onClick() {
const progressBars = bryntum.queryAll('progressbar');
progressBars[0].value = Math.random();
progressBars[1].value = Math.floor(Math.random() * 12) + 1;
progressBars[2].value = Math.random();
}
});
new ProgressBar({
appendTo : targetElement,
label : 'Upload Progress',
value : 0.65,
color : 'blue'
});
new ProgressBar({
appendTo : targetElement,
label : 'Tasks Completed',
value : 8,
max : 12,
valueRenderer : (value, max) => `${value} of ${max} tasks`,
color : 'green'
});
new ProgressBar({
appendTo : targetElement,
label : 'Database Migration',
valueText : 'Nearly there',
value : 3 / 8,
color : 'orange'
});Custom Rating Widget Example
Here's another example showing a more interactive widget:
class StarRating extends Widget {
static $name = 'StarRating';
static type = 'starrating';
static configurable = {
value : 0,
maxStars : 5,
editable : true
};
compose() {
const { value, maxStars, editable } = this,
stars = [];
for (let i = 1; i <= maxStars; i++) {
const filled = i <= value;
stars.push({
tag : 'i',
class : {
fa : 1,
'fa-star' : filled,
'fa-star-o' : !filled,
'star-active' : editable
},
dataset : {
starIndex : i
}
});
}
return {
class : 'star-rating',
children : stars
};
}
onPaint({ firstPaint }) {
if (firstPaint && this.editable) {
this.element.addEventListener('click', (e) => {
const starEl = e.target.closest('[data-star-index]');
if (starEl) {
this.value = parseInt(starEl.dataset.starIndex);
this.trigger('change', { value : this.value });
}
});
}
}
}
StarRating.initClass();
//<code-header>
fiddle.title = 'Star rating';
//</code-header>
Object.assign(targetElement.style, {
'flex-direction' : 'column',
'align-items' : 'flex-start',
gap : '1em'
});
// Custom StarRating widget example
class StarRating extends Widget {
static $name = 'StarRating';
static type = 'starrating';
static configurable = {
value : 0,
maxStars : 5,
editable : true
};
compose() {
const
{ value, maxStars, editable, size } = this,
stars = [];
// Generate star elements
for (let i = 1; i <= maxStars; i++) {
const filled = i <= value;
stars.push({
tag : 'i',
class : {
fa : 1,
'fa-star' : 1
},
style : {
cursor : 'pointer',
opacity : filled ? 1 : 0.15,
color : filled ? 'var(--b-color-yellow)' : undefined
},
dataset : {
starIndex : i
}
});
}
return {
class : {
'star-rating' : 1,
'star-rating-editable' : editable
},
children : stars
};
}
onPaint({ firstPaint }) {
if (firstPaint) {
this.element.addEventListener('click', this.onStarClick.bind(this));
}
}
onStarClick(e) {
if (this.editable) {
const starEl = e.target.closest('[data-star-index]');
if (starEl) {
const newValue = parseInt(starEl.dataset.starIndex);
this.value = newValue;
Toast.show(`Rated ${newValue} stars!`);
}
}
}
}
StarRating.initClass();
// Create instances
new StarRating({
appendTo : targetElement,
value : 1,
maxStars : 5
});
new StarRating({
appendTo : targetElement,
value : 4,
maxStars : 5
});
new Label({ text : 'Read-only rating:', appendTo : targetElement });
new StarRating({
appendTo : targetElement,
value : 5,
maxStars : 5,
editable : false
});Composite Widgets
Composite widgets are built by combining other widgets. Instead of extending Widget, you extend Container or
Panel and configure child widgets.
class SearchBox extends Container {
static $name = 'SearchBox';
static type = 'searchbox';
static configurable = {
placeholder : 'Search...',
buttonText : 'Search',
layout : 'hbox',
items : {
searchField : {
type : 'textfield',
ref : 'searchField',
flex : 1,
placeholder : 'up.placeholder',
clearable : true
},
searchButton : {
type : 'button',
ref : 'searchButton',
text : 'up.buttonText',
icon : 'fa-search',
rendition : 'filled',
color : 'b-blue',
onClick : 'up.onSearchClick'
}
}
};
get searchValue() {
return this.widgetMap.searchField.value;
}
onSearchClick() {
const value = this.searchValue;
this.trigger('search', { value });
}
}
SearchBox.initClass();
Accessing Child Widgets: ref and widgetMap
In composite widgets, you often need to access child widgets. The ref config creates entries in the parent's
widgetMap object:
items : {
searchField : {
type : 'textfield',
ref : 'searchField' // Creates this.widgetMap.searchField
}
}
get searchValue() {
return this.widgetMap.searchField.value; // Access via widgetMap
}
The ref property creates a reference in widgetMap using the specified name. This allows you to access child widget
instances and call methods or read properties on them.
Note the difference between compose() references and Container ref:
- In
compose():reference: 'myButton'createsthis.myButton(DOM element) - In Container
items:ref: 'searchField'createsthis.widgetMap.searchField(Widget instance) refs of a widget are hoisted to any parent Container'swidgetMapas well for easy access.
Property Resolution with 'up.'
Notice the 'up.placeholder' and 'up.buttonText' values in the example above. The 'up.' prefix is a Bryntum
convention for referencing properties on an ancestor widget. This creates a property binding from child to parent.
When Bryntum encounters a string value starting with 'up.', it resolves the property from the owner widget:
placeholder : 'up.placeholder' // Resolves to this.owner.placeholder
text : 'up.buttonText' // Resolves to this.owner.buttonText
onClick : 'up.onSearchClick' // Calls this.owner.onSearchClick()
This pattern is useful because:
- DRY Principle - Define properties once on the parent, reference them in children
- Encapsulation - Parent widget controls the configuration of its children
items : {
panel : {
type : 'panel',
items : {
button : {
type : 'button',
text : 'up.rootProperty' // Resolves to grandparent.rootProperty
}
}
}
}
For methods (like onClick: 'up.onSearchClick'), the method is called with the owner widget as this, maintaining
proper context.
//<code-header>
fiddle.title = 'Search box';
//</code-header>
// Composite SearchBox widget example
class SearchBox extends Container {
static $name = 'SearchBox';
static type = 'searchbox';
static configurable = {
placeholder : 'Search...',
buttonText : 'Search',
layout : 'hbox',
items : {
searchField : {
type : 'textfield',
ref : 'searchField',
flex : 1,
placeholder : 'up.placeholder',
clearable : true,
triggers : {
search : {
cls : 'fa fa-search'
}
}
},
searchButton : {
type : 'button',
ref : 'searchButton',
text : 'up.buttonText',
icon : 'fa-search',
rendition : 'filled',
color : 'b-blue',
onClick : 'up.onSearchClick'
}
}
};
get searchValue() {
return this.widgetMap.searchField.value;
}
set searchValue(value) {
this.widgetMap.searchField.value = value;
}
onSearchClick() {
const value = this.searchValue;
Toast.show(`Searching for: "${value}"`);
}
construct(config) {
super.construct(config);
this.widgetMap.searchField.on({
change : () => this.onSearchChange(),
keydown : (e) => {
if (e.key === 'Enter') {
this.onSearchClick();
}
}
});
}
onSearchChange() {
console.log('Search input changed:', this.searchValue);
}
}
SearchBox.initClass();
// Create an instance
new SearchBox({
appendTo : targetElement,
placeholder : 'Search docs...',
buttonText : 'Go'
});Child Element Order
The elements contained in the children object are created in the order in which they are
declared in that object. Elements added by derived
classes (as above) are appended to the inherited elements.
When this order is not desired, a derived class can use the > character in the keys of its children properties to
insert its elements before an inherited element. The > syntax functions as "Insert [Key] before [Reference].
This allows you to inject custom elements into specific slots of a complex layout inherited from a Bryntum base class
without having to rewrite the entire children object.
class CopyableLink extends Link {
static configurable = {
copyIcon : 'fa-copy'
};
compose() {
const { copyIcon } = this;
return {
children : {
// Insert copyIconElement before inherited externalLinkElement:
'copyIconElement > externalLinkElement' : {
tag : 'span',
class : {
'fa' : 1,
[copyIcon] : 1
},
listeners : {
click : 'onCopyLink'
}
}
}
};
}
onCopyLink(event) {
navigator.clipboard?.writeText(this.linkElement.href);
}
}
const link = new CopyableLink({
appendTo : document.body,
text : 'Copyable link',
href : '#foo'
});
//<code-header>
fiddle.title = 'Custom widgets';
//</code-header>
class Link extends Widget {
static configurable = {
external : null,
href : null,
target : '_blank',
text : null
};
compose() {
const { external, href, target, text } = this;
return {
children : {
linkElement : {
tag : 'a',
href,
text
},
externalLinkElement : external && {
tag : 'a',
class : {
fa : 1,
'fa-external-link-alt' : 1
},
href,
target
}
}
};
}
}
class CopyableLink extends Link {
static configurable = {
copyIcon : 'fa-copy'
};
compose() {
const { copyIcon } = this;
return {
style : {
display : 'flex',
gap : '.5em',
'align-items' : 'center'
},
children : {
// Insert copyIconElement before inherited externalLinkElement:
'copyIconElement > externalLinkElement' : {
tag : 'span',
style : { cursor : 'pointer' },
class : {
fa : 1,
[copyIcon] : 1
},
listeners : {
click : 'onCopyLink'
}
}
}
};
}
onCopyLink(event) {
navigator.clipboard?.writeText(this.linkElement.href);
Toast.show('Link copied to clipboard!');
}
}
const link = new CopyableLink({
appendTo : targetElement,
text : 'Copyable link',
href : 'https://bryntum.com'
});Styling Your Widget
All Bryntum widgets get a CSS class automatically added to their root element based on their class name. For example,
the ButtonGroup widget class will get the CSS class b-button-group (kebab-cased) added to its root element. You can
use this class to style all instances of that widget.
Similarly, your own created custom widget also gets a CSS class added to it based on its class name. For example, a
custom widget class named: MyWidget will get the CSS class b-my-widget added to its root element.
Use this class to scope your widget's styles.
CSS Best Practices
Use BEM-like naming and CSS variables for theming:
.b-my-widget {
display : flex;
padding : var(--b-widget-padding);
background-color : var(--b-panel-background-color);
border : 1px solid var(--b-border-color);
border-radius : var(--b-widget-border-radius);
}
.b-my-widget-header {
font-weight : 600;
color : var(--b-text-color);
}
/* Use logical properties for RTL support */
.b-my-widget-content {
margin-inline-start : 1em;
padding-block : 0.5em;
}
Configuration Properties
Use configurable for all public properties and implement change handlers when needed. Under the hood, Bryntum generates
updater and changer methods for each property. For a property named propertyName, the following methods are generated:
changeProperty(newValue, oldValue): Called before the property is changed. Use this to validate or transform the value.updateProperty(newValue, oldValue): Called after the property is changed. Use this to react to the change (e.g., trigger events).
static configurable = {
value : 0,
label : 'Default'
};
// Called automatically before value is set to allow mutation/validation
changeValue(newValue, oldValue) {
// Validate value is non-negative
return Math.max(0, newValue);
}
// Called automatically when value changes
updateValue(newValue, oldValue) {
console.log(`Value changed from ${oldValue} to ${newValue}`);
this.trigger('valuechange', { value : newValue, oldValue });
}
The Update Flow
When a configurable property is changed, Bryntum follows a specific sequence:
changeProperty(): Used to transform or validate the incoming value.updateProperty(): Used to react to the change (e.g., triggering events).recompose(): Automatically scheduled for the next animation frame.
Because compose() is batched, you can update multiple properties simultaneously without triggering multiple heavy
DOM repaints.
Boilerplate
All Widget classes need some additional boilerplate:
export default class Link extends Widget {
static $name = 'Link'; // Needed for minification
static type = 'link'; // Short name for config objects
// ...
}
Link.initClass(); // Registers with widget factory
What Each Property Does
static $name: Required for minification. When code is minified, class names get shortened (e.g., Link becomes
a). The $name property preserves the original class name for debugging and reflection.
static type: Registers a short alias for the widget factory. This enables creating widgets from config objects:
// With type registered
{ type : 'link', text : 'Click me' } // Creates Link instance
// Without type, you must use the class directly
new Link({ text : 'Click me' })
initClass(): Must be called after any Widget class definition. This method registers the widget type with the
factory (enables { type : 'link' } syntax).
Forgetting initClass() means your widget won't work with the factory system and config objects won't be processed
correctly.
Best Practices
1. Group Property Extraction at the Top
Reading properties at the top of the method makes the code cleaner and ensures all dependencies are registered
immediately. Only properties read on the first call to compose will trigger recomposition when changed.
compose() {
const { label, value, color } = this;
return {
class : `widget ${color}`,
text : `${label}: ${value}`
};
}
2. Use configurable for Public Properties
static configurable = {
// Public API properties
label : 'Default',
value : 0
};
3. Don't Manually Update DOM
Let compose() handle all DOM updates automatically:
// BAD - Manual DOM updates
updateValue(value) {
this.element.querySelector('.value').textContent = value;
}
// GOOD - Let compose() handle it
compose() {
const { value } = this;
return {
children : [
{
class : 'value',
text : value // Automatically updates when value changes
}
]
};
}
4. Clean Up Resources
construct(config) {
super.construct(config);
this._timer = setInterval(() => this.updateTime(), 1000);
}
doDestroy() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
}
super.doDestroy();
}
5. Use References for DOM Access
When you use reference: 'someName' in compose(), Bryntum automatically creates this.someName
pointing to that DOM element.
compose() {
return {
children : [
{
reference : 'myButton', // Automatically creates this.myButton
tag : 'button',
text : 'Click me'
}
]
};
}
onPaint({ firstPaint }) {
if (firstPaint) {
this.myButton.addEventListener('click', () => this.onButtonClick());
}
}
Caveats
The Widget class relies on the standard getter and setter for config properties. Overriding the getter will prevent
detection of configs used by compose() while overriding the setter will prevent detection of changes to configs and
the automatic call to recompose(). Best practice is to leave the getter and setter alone and implement only the
changer or updater methods when needed.