Composing a component

Most behavior a new component needs already exists as a tested controller or helper. Compose them in the component and keep only what is specific to it.

Compose, then extend

  • A component composes controllers. Hold each one in a field of the custom element with createX(this, config). Config is getters (getItems, getTarget) and callbacks (onChange), so the element keeps its own state and the controller never reaches into it.
  • Extend a controller to change how it decides. A controller documents protected hooks at its decision points. Subclass it, override the hook, and register the subclass with bindController.
  • A component never extends a controller. The component is the host; the controller is a part it holds.

Prefer the lightest tool that works: a config callback first, a documented protected hook by subclassing second, a new controller last. Use a helper, not a controller, for anything that only maps input to output.

Extending through a hook

SortableController.resolveTargetIndex decides the index a lifted item takes from where the pointer is. This subclass snaps the drop position to a grid by changing only that decision:

import { SortableController, bindController, type SortableRect } from '@ecopages/radiant-ui/controllers';
 
class SnapSortable extends SortableController {
	protected override resolveTargetIndex(
		rects: readonly SortableRect[],
		from: number | null,
		point: { x: number; y: number },
	) {
		return super.resolveTargetIndex(rects, from, { x: Math.round(point.x / 40) * 40, y: point.y });
	}
}
// in the element: bindController(this, new SnapSortable({ getContainer, getItems, onChange }));

createX always returns the base class, so a subclass is constructed directly. The component holds the instance; it does not become one.

Steps

  1. Name the behaviors: open and close, keyboard movement, dragging, filtering, date math, positioning.
  2. Pick one primitive per behavior from Controllers and Helpers. Controllers own the DOM wiring; helpers own the decisions and formatting.
  3. Keep what is specific to the component (state, markup, rui-change events) in the component.
  4. Look at Recipes for compositions of several primitives.