Build a custom Control
Subclass an existing Control, wire a brand picker — 45 minutes.
In Build a custom Element you wired your Countdown's settings panel from existing controls (TextControl). Sometimes the shipped catalogue isn't enough — your brand needs a colour picker that pre-loads brand swatches, a slider that emits px AND em, a dropdown whose options depend on another field. Then you author a Control. Time budget: 45 minutes.
What a Control IS
A Control is a settings-panel widget that mutates one or more attributes / formats of the currently-selected element. The shipped catalogue (≈ 40 control types — TextControl, SelectControl, ColorPickerControl, DimensionControl, BorderControl, …) lives in src/includes/<Name>Control.js.
Two paths to author a new one:
- Subclass an existing Control — recommended. You get free wiring (label, container DOM, focus, the
_applyValuesyncing). Start here. - Subclass
BaseControldirectly — only when the existing controls have a fundamentally different input model than what you need (rare).
This doc walks the canonical pattern: a Brand Color Picker that subclasses ColorPickerControl and adds 6 quick-access brand swatches.
Pre-flight
Confirm the demo runs and the canonical example renders:
Step 1 — Subclass
Step 2 — Override render()
The shipped ColorPickerControl.render() builds the standard label + colour swatch + popover. We extend it: call super first, then append the brand-swatches strip.
- Always call
super.render()first. The parent builds the DOM your additions live inside. - Always route value changes through
_applyValue— the canonical entry point that syncs the control's internal state (the visible hex input, the swatch preview, the parent'svalueproperty, AND theonChangecallback the buyer passed in). The footgun: there's also_notify(value)which is the change-event path; it fires the callback but DOESN'T sync the visible UI. Use_applyValue. (W6.C.13 lesson.)
Step 3 — Wire it into an Element
You don't register custom Controls anywhere — they're instantiated inline inside an Element's getControls():
Step 4 — There's no Builder.registerControl()
Custom Controls aren't registry-based today (W6.C.13 lesson). You instantiate them inline inside getControls(). That works for any project where the Control's constructor args are statically known.
A future engine API (Builder.registerControl(name, klass)) would unlock JSON-described custom panels — buyers would author getControls() in JSON instead of JS — but that's a CD-2 follow-up, not shipped today. The roadmap for engine-side improvements is tracked in the BuilderJS repo's open issues.
Step 5 — Test
- Open the builder with a sample that uses your
BrandBadgeElement. - Click the badge → settings panel shows your
BrandColorPickerControl. - Click any of the 6 swatches → the canvas updates with the new colour.
- Open DevTools → Console:
builder.getData(). Confirm yourbrand_colorvalue is the hex you clicked. - Reload the page → the badge re-renders with the saved colour (round-trip).
If step 3 paints the canvas but step 5 forgets the colour, your getData() is missing the brand_color field. Re-read Build a custom Element — every domain attribute must be in getData() AND parse().
Did you make it?
You should now have:
- ✅ A
BrandColorPickerControlclass subclassingColorPickerControl. - ✅ Override
render()that callssuper.render()first, then appends the brand strip. - ✅ Each swatch click routes through
this._applyValue(hex)— NOT_notify(hex). - ✅ The custom Control instantiated inline inside an Element's
getControls(). - ✅ The round-trip: setting a swatch persists across save / load.
If a step didn't work:
- 🩹 Click on swatch updates the visible hex input but the actual saved value is stale → you used
_notifyinstead of_applyValue. They look similar;_applyValueis the syncing entry. - 🩹 Custom Control doesn't render at all → confirm you called
super.render()first. The parent builds the container yourpopoverelement lives inside. - 🩹 I want to subclass
BaseControldirectly, not an existing Control → that's the rare path;BaseControlis mostly an interface (label, value, container, callback). Readsrc/includes/BaseControl.jsline by line.
What's next?
- → Build a custom Widget — palette items the buyer drags onto the canvas.
- 🔗
examples/extensions/3-custom-control/— runnable Brand Color Picker end-to-end. - 🔗
src/includes/ColorPickerControl.js— canonical parent + the_applyValuevs_notifycontract. - 🔗
docs/core/CONTROL_DEFINITION.md— the full control catalogue (≈ 40 shipped types).