| name | thatopen-syntax-components |
| description | Use when accessing ThatOpen components, implementing custom components, or working with the component lifecycle and event system. Prevents direct instantiation of components instead of using get(). Covers components.get() singleton pattern, component registration, lifecycle interface implementation, Event system, DataMap, DataSet reactive collections. Keywords: components, get, singleton, uuid, lifecycle, event, datamap, dataset, disposable, updateable, configurable, component api, how to use components, register component.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires @thatopen/components 3.3.x. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
ThatOpen Component Syntax
Purpose
This skill covers the syntax and usage patterns for ThatOpen's component
system. It provides exact method signatures, lifecycle interface contracts,
event system API, and reactive collection patterns. For architectural overview
and design rationale, see thatopen-core-architecture.
Version: @thatopen/components 3.3.x
The components.get() Pattern
ALWAYS obtain component instances through the singleton registry:
import * as OBC from "@thatopen/components";
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const ifcLoader = components.get(OBC.IfcLoader);
const fragments = components.get(OBC.FragmentsManager);
How it works:
- First call:
get() invokes new ComponentClass(components) internally.
- The constructor calls
components.add(ComponentClass.uuid, this) to
register the instance.
- Subsequent calls:
get() returns the cached instance from components.list.
NEVER call new OBC.Worlds(components) or any component constructor directly.
This bypasses the registry and creates untracked duplicate instances.
Components Container API
class Components implements Disposable {
readonly list: DataMap<string, Component>;
enabled: boolean;
onDisposed: Event<void>;
onInit: Event<undefined>;
static release: string;
get<U extends Component>(Ctor: new (c: Components) => U): U;
add(uuid: string, instance: Component): void;
init(): void;
dispose(): void;
}
init()
ALWAYS call components.init() after setting up your world. This starts
the requestAnimationFrame loop using THREE.Clock for delta time.
Without it, nothing renders and no Updateable components receive updates.
dispose()
ALWAYS call components.dispose() on cleanup. This iterates all registered
components and disposes each one. FragmentsManager is ALWAYS disposed last
to prevent dangling references. After dispose(), NEVER use any component
references — they are invalidated.
Component Base Class
Every ThatOpen component extends this hierarchy:
abstract class Base {
constructor(public components: Components) {}
isDisposeable(): this is Disposable;
isUpdateable(): this is Updateable;
isConfigurable(): this is Configurable<any, any>;
isResizeable(): this is Resizeable;
isHideable(): this is Hideable;
isSerializable(): this is Serializable<any>;
}
abstract class Component extends Base {
static readonly uuid: string;
abstract enabled: boolean;
}
Creating Custom Components
Every custom component MUST follow this exact pattern:
import * as OBC from "@thatopen/components";
class MyTool extends OBC.Component implements OBC.Disposable {
static readonly uuid = "a1b2c3d4-e5f6-7890-abcd-ef1234567890" as const;
enabled = true;
onDisposed = new OBC.Event<void>();
constructor(components: OBC.Components) {
super(components);
components.add(MyTool.uuid, this);
}
dispose(): void {
this.enabled = false;
this.onDisposed.trigger();
this.onDisposed.reset();
}
}
myTool = components.();
Checklist for custom components:
- Static
uuid with as const assertion
enabled property initialized
- Constructor calls
super(components) then components.add()
- Implements
Disposable if it holds any resources
dispose() sets enabled = false, triggers onDisposed, resets events
Lifecycle Interfaces
Components opt into behaviors by implementing interfaces. This is a mixin
pattern — no deep inheritance hierarchies.
Disposable
ALWAYS implement if the component holds resources (event handlers, GPU
buffers, DOM references, subscriptions).
interface Disposable {
dispose(): void;
onDisposed: Event<void>;
}
Implementation pattern:
dispose(): void {
this.enabled = false;
this.someMap.clear();
this.someDomElement?.remove();
this.onDisposed.trigger();
this.onDisposed.reset();
this.onSomeEvent.reset();
}
Updateable
Implement when the component needs per-frame updates. The Components
animation loop calls update(delta) on EVERY enabled Updateable
component each frame.
interface Updateable {
update(delta?: number): void;
onBeforeUpdate: Event<any>;
onAfterUpdate: Event<any>;
}
Implementation pattern:
class AnimationController extends OBC.Component
implements OBC.Updateable, OBC.Disposable {
static readonly uuid = "..." as const;
enabled = true;
onBeforeUpdate = new OBC.Event<void>();
onAfterUpdate = new OBC.Event<void>();
onDisposed = new OBC.Event<void>();
constructor(components: OBC.Components) {
super(components);
components.add(AnimationController.uuid, this);
}
update(delta?: number): void {
this.onBeforeUpdate.trigger();
this.onAfterUpdate.trigger();
}
dispose(): {
. = ;
..();
..();
..();
..();
}
}
Configurable
Implement when the component requires deferred or async initialization.
ALWAYS call setup() before using a Configurable component.
interface Configurable<TConfig, TPartialConfig> {
setup(config?: TPartialConfig): void;
config: TConfig;
isSetup: boolean;
onSetup: Event<any>;
}
Implementation pattern:
class MyConfigurable extends OBC.Component
implements OBC.Configurable<MyConfig, Partial<MyConfig>>, OBC.Disposable {
static readonly uuid = "..." as const;
enabled = true;
isSetup = false;
config: MyConfig = { };
onSetup = new OBC.Event<MyConfigurable>();
onDisposed = new OBC.Event<void>();
constructor(components: OBC.Components) {
super(components);
components.add(MyConfigurable.uuid, this);
}
setup(config?: Partial<MyConfig>): void {
if (config) {
this.config = { ...this.config, ...config };
}
. = ;
..();
}
(): {
. = ;
. = ;
..();
..();
..();
}
}
ALWAYS check isSetup before calling methods that depend on configuration:
const myComp = components.get(MyConfigurable);
if (!myComp.isSetup) {
await myComp.setup({ });
}
Other Interfaces
| Interface | Contract |
|---|
Resizeable | resize(size?), getSize(), onResize: Event |
Hideable | visible: boolean |
Createable | create(), delete(), endCreation(), cancelCreation() |
Serializable | import(data), export(): data |
Event<T> System
ThatOpen uses a custom pub/sub event class throughout the entire API.
class Event<T> {
enabled: boolean;
add(handler: (data: T) => void): void;
remove(handler: (data: T) => void): void;
trigger(data?: T): void;
reset(): void;
}
Usage Rules
-
ALWAYS store handler references when you need to remove them later.
Anonymous arrow functions cannot be removed.
-
ALWAYS call reset() on all owned events in your dispose() method
to prevent memory leaks.
-
Use enabled = false to temporarily suppress an event without
removing handlers. Set back to true to resume.
-
NEVER assume event ordering — handlers fire in registration order,
but do not depend on this for correctness.
Quick Reference
const onLoaded = (model: OBC.FragmentsModel) => { };
fragments.onFragmentsLoaded.add(onLoaded);
fragments.onFragmentsLoaded.remove(onLoaded);
fragments.onFragmentsLoaded.enabled = false;
fragments.onFragmentsLoaded.enabled = true;
fragments.onFragmentsLoaded.reset();
See references/examples.md for detailed patterns.
DataMap<K, V>: Reactive Map
Extends the standard Map with event hooks. Used throughout ThatOpen
for observable collections (e.g., Components.list, Worlds.list,
Classifier.list).
class DataMap<K, V> extends Map<K, V> {
onItemSet: Event<{ key: K; value: V }>;
onItemUpdated: Event<{ key: K; value: V }>;
onItemDeleted: Event<{ key: K }>;
onCleared: Event<void>;
}
All standard Map methods work (get, set, delete, has, forEach,
entries, keys, values, size). The events fire automatically when
the corresponding operations occur.
Reacting to DataMap Changes
const worlds = components.get(OBC.Worlds);
worlds.list.onItemSet.add(({ key, value }) => {
console.log(`World added: ${key}`);
});
worlds.list.onItemUpdated.add(({ key, value }) => {
console.log(`World updated: ${key}`);
});
worlds.list.onItemDeleted.add(({ key }) => {
console.log(`World removed: ${key}`);
});
worlds.list.onCleared.add(() => {
console.log("All worlds cleared");
});
DataSet<T>: Reactive Set
Extends the standard Set with event hooks. Used for collections like
world.meshes, measurement lists, and style sets.
class DataSet<T> extends Set<T> {
onItemAdded: Event<T>;
onItemDeleted: Event<T>;
onCleared: Event<void>;
}
All standard Set methods work (add, delete, has, forEach,
entries, values, size). Events fire automatically.
Reacting to DataSet Changes
world.meshes.onItemAdded.add((mesh) => {
console.log("Mesh added:", mesh.name);
});
world.meshes.onItemDeleted.add((mesh) => {
console.log("Mesh removed:", mesh.name);
});
Components Lifecycle Flow
new Components()
|
v
components.get(X) ──> new X(components) ──> components.add(uuid, instance)
| |
v v
[setup() if Configurable] registered in components.list
|
v
components.init() ──> starts requestAnimationFrame loop
| |
v v
update(delta) called on ALL enabled Updateable components each frame
|
v
components.dispose() ──> dispose() on ALL components
FragmentsManager disposed LAST
Runtime Interface Detection
Use the is*() methods on Base for runtime type checking:
const component = components.list.get(someUuid);
if (component?.isDisposeable()) {
component.dispose();
}
if (component?.isUpdateable()) {
component.update(0.016);
}
if (component?.isConfigurable()) {
if (!component.isSetup) {
component.setup();
}
}
These use duck-typing (checking for method existence), NOT instanceof.
Critical Rules
- ALWAYS use
components.get(ComponentClass) to obtain instances.
NEVER use new ComponentClass(components) directly.
- ALWAYS implement
Disposable if your component holds any resources.
- ALWAYS call
components.add(uuid, this) in your component constructor.
- ALWAYS define a static
uuid with as const on custom components.
- ALWAYS call
reset() on all owned events in dispose().
- ALWAYS store event handler references for later removal.
- ALWAYS call
setup() on Configurable components before using them.
- ALWAYS call
components.init() after world setup.
- NEVER use components after
components.dispose() has been called.
- NEVER use anonymous functions as event handlers if you need to remove
them later.
Reference Files
Source Verification
All API signatures verified against:
- GitHub:
ThatOpen/engine_components main branch (packages/core/src/)
- npm:
@thatopen/components@3.3.3
- Research:
docs/research/vooronderzoek-thatopen.md