This is the practical companion to Widgets. Read that first for the concepts;
this is the code.
Save as /js/widgets/heading.js:
import { Widget } from "/js/widgets/widgets.js";
export default class Heading extends Widget {
constructor(options = {}) {
super(options);
let sourceID = options.id != null ? options.id : "";
this.Name = "Heading: #" + sourceID;
this.parentID = "#" + sourceID;
this.Parent = document.getElementById(sourceID);
this.Subscribe("reset", (m) => { this.Render(); });
this.Subscribe("vessel", (m) => { this.Render(); });
}
Render() {
SetHtml(this.parentID, thisvessel.HeadingVerbose);
}
}
Declare it in a console page:
<hz-widget id="heading" source="heading" class="panel-header"></hz-widget>
That is the whole contract. The default export is the constructor, the source
attribute names the file, and options carries every attribute from the
declaration.
Anatomy
super(options) first. Pass the options through — the
base needs them, and it registers the widget as under construction so a later throw
can still be cleaned up.
this.Name is how
workbench.Widgets.Find() will locate this instance. Include the element id;
that is the convention and it makes duplicates visible.
this.Parent is the element to render into. The
controller has already created it by the time the constructor runs.
this.Subscribe, not game.Subscribe. This
is the rule that matters most.
Adding server data
If your widget needs something the console does not already receive, claim the packet
and subscribe to the topic it raises:
this.Subscribe("ready", (m) => {
this.AcceptPacket("DAMAGE-TEAMS");
});
this.Subscribe("vessel-damage", (m) => { this.Render(); });
Claim on ready rather than in the constructor when you need the socket
to exist. And claim through this.AcceptPacket so the claim is released when
the widget is — the global does not track owners for you. See
Packets And Topics.
Using a template
Widgets that render repeated rows load an HTML template rather than building strings
inline:
this.layout = options.layout || "/templates/damage-listing.htm";
GetContent(this.layout).then((data) => {
this.template = data;
this.Render();
});
Templates use [Name]‑style placeholders that the widget replaces.
Letting the layout be overridden through options.layout costs nothing and
lets a module restyle your widget without replacing it.
Drawing in 3D or on a canvas
Ask for a canvas in the declaration:
<hz-widget id="radar" source="radar-3d" element="canvas" class="panel-full"></hz-widget>
three.js is available as a bare import, through the page's import map:
import * as THREE from "three";
A widget with its own render loop must stop it in Dispose(), and must not
create a second one on reset.
Cleaning up
Dispose() {
if (this._raf != null) {
cancelAnimationFrame(this._raf);
this._raf = null;
}
// ... release GPU objects, detach listeners ...
super.Dispose(); // <- required
}
The super.Dispose() call is what releases subscriptions and packets.
Forgetting it is a silent leak, and copying the base's body instead of calling it is the
same mistake with more typing.
Checklist
- Does every subscription go through
this.Subscribe?
- Does every packet claim go through
this.AcceptPacket?
- If
Dispose() is overridden, does it call super.Dispose()?
- Is everything the constructor does safe to do twice? Reset re‑runs
initialisation on a page that is already running. This is where most widget bugs are.
- Is the element id unique on the page? Two declarations with one id mount two
widgets against one element.
- Does it survive
thisvessel being null? It will be, before assignment
and after a reset.
- Does it do work when nothing has changed? A widget re‑rendering on every
contact update in a busy mission will cost more than the entire 3D view.
Shipping it
Put the file at Html/js/widgets/<source>.js inside your module.
Module files layer over the base game's by path, so a module can add new widgets and
also replace existing ones. See Modules.
To make it appear on a console without editing that console, declare it from a
component, a vessel class or a mission. That is the
intended route, and it is what makes a widget content rather than a code change.
Debugging
The widget controller logs each widget as it mounts, and warns about duplicate ids.
From the browser console, workbench.Widgets.Instances lists everything
mounted and workbench.Widgets.Find("...") gets you a live handle to poke
at. Import failures are reported as errors — if a widget simply never appears, that
is the first place to look.