Component lifecycle
A Component goes through a small, predictable sequence of phases from construction to destruction. Understanding it explains when DOM elements appear, when layout runs, and where to hook your own subclass code.
The phases
Component(tag) ─→ in-memory object, no DOM
│
addComponent(child) ─→ parent registers the child
│
getElement() ─→ first call creates the <div>; subsequent
calls return the cached element
│
render() (subclass hook) ─→ builds bespoke DOM (called from getElement
when an element is needed)
│
init() (subclass hook) ─→ wires native listeners, sets initial styles
│
doLayout() ─→ layout manager positions children, writes
x / y / width / height pixel values
│
… updates …
│
removeComponent(child) ─→ detaches from the parent tree
│
destructor() (protected) ─→ subclass cleanup hookConstruction
const button = Button('Save');The component object exists in memory immediately. No <div>, no styles, no listeners — just JavaScript state. You can call any setter (setPosition, setForegroundColor, setBorder) at this point, or supply matching options in a trailing options bag — Button('Save', { foregroundColor: '#fff', preferredSize: { width: 120, height: 32 } }). Values are stashed and replayed when the DOM element is eventually created.
This is why setBackgroundColor works before you've added the component to a parent. The framework defers DOM work until it's actually needed.
new Button('Save') also works — every component class is callable in both forms. Bare-call is the recommended style; new is kept for backwards compatibility.
Adding to a parent
panel.addComponent(button); // single child
panel.addComponents(button, label, textField); // variadic
panel.addComponents([button, label, textField]); // or as an arrayaddComponent registers a child with its parent's component list and applies the parent's layout-manager constraints (optional second argument). addComponents accepts any mix of bare components, { component, constraints } pairs, or arrays of either. Neither call forces a DOM commit — the element is still lazy.
For a fully declarative tree, pass children via the components option at construction time:
const panel = Panel({
layoutManager: VBox(),
components: [Button('Save'), Button('Cancel')]
});Element creation: getElement()
button.getElement(); // first call: creates the <div>The first call to getElement() invokes the subclass's render() followed by init(). From here on, the component has a real DOM node. Anything you set before is replayed onto the element.
You usually don't call getElement() directly — it's called for you the first time the component participates in a layout pass.
Layout: doLayout()
Layout is what positions absolutely-positioned children and writes pixel values for top / left / width / height. It runs:
- At first render — when the root
Bodylays out top-level children. - On viewport resize —
Bodylistens forwindow.resizeand re-runs layout. - Explicitly — when you call
parent.doLayout()after changing a child's preferred size.
button.setPreferredSize(120, 32);
button.getParentComponent()?.doLayout(); // child's parent re-runs layoutrAF-coalesced scheduling
Internally, setters and event handlers call scheduleLayout() rather than doLayout() directly. The queue flushes once per animation frame and prunes any component whose ancestor is also dirty (the ancestor's layout will recurse into it). Multiple changes within the same frame coalesce into a single layout pass.
Pausing layout
For bulk mutations, suspend automatic layout passes:
panel.pauseLayout();
panel.addComponents(largeArray.map(buildItem));
panel.resumeLayout(); // triggers a single doLayout afterwardspauseLayout() blocks scheduleLayout(); resumeLayout() triggers a synchronous layout pass and re-enables scheduling. Without these, every addComponent would queue its own layout, and rAF coalescing would still mean one batched pass — but pauseLayout makes the intent explicit and disables the scheduling work entirely during the bulk operation.
Removal
panel.removeComponent(button);Detaches the component from the parent's child list. Future layout passes ignore it. The DOM element is removed; future calls to getElement() would re-create it from scratch.
Subclass hooks
If you write your own component subclass, override these in addition to whatever public surface you expose:
| Hook | When | Purpose |
|---|---|---|
render() (protected) | Called by getElement() on first access | Build the DOM element. Default returns a <div> (or whatever was passed to super(tag)). |
init() (protected) | Called once the element exists | Wire native listeners, apply initial styles. The framework calls this after render(). |
doLayout() | Called on every layout pass | Override only if you need custom positioning beyond what a LayoutManager provides. |
destructor() (protected) | Called on removal / disposal | Clean up listeners, timers, theme subscriptions. |
The framework uses these in built-in components — Button adds a label in init(), Window wires drag handlers, Text subscribes to ThemeManager.onThemeChange.
Disposal
Text (and anything that subclasses it — Label, Header, Legend) registers a theme-change listener on construction. Custom components that create Text instances dynamically and remove them must call text.dispose() to detach the listener and avoid memory leaks. The framework does this automatically for built-in components attached and removed through normal addComponent / removeComponent flows.
See also
- Mental model — the architectural overview
- Layout system — how the layout manager actually positions children
- Sizing — preferred / min / max size semantics
- Performance —
pauseLayout, virtual scrolling, dispose patterns