-
Notifications
You must be signed in to change notification settings - Fork 7
Open
Description
🎯 Problem Statement
Currently, accessing graph components by type and ID requires navigating through different stores:
// Different paths for different component types
blockListStore.getBlockState(id)?.getViewComponent()
connectionStore.getConnectionState(id)?.getViewComponent()
// Custom components — no standard wayThis creates challenges for systems that need to work with any component type:
- SelectionService needs to find components for selection operations
- DragService needs to collect draggable components
- Future HighlightSystem needs to apply visual states to any component
- Plugin authors have no standard way to register custom components
💡 Proposed Solution
Introduce a ComponentRegistry — a centralized registry for all GraphComponent instances, accessible by entity type and ID.
Core Principles
- Mandatory registration — All components must implement identification methods
- Automatic lifecycle — Registration/unregistration happens in component lifecycle
- Instance-scoped — Registry belongs to Graph instance (supports multiple graphs)
- Extensible — Custom component types work without library changes
📝 Detailed Design
1. ComponentRegistry Class
// src/services/ComponentRegistry.ts
export type TEntityType = string;
export type TEntityId = string | number;
export interface IRegistrableComponent {
getEntityType(): TEntityType;
getEntityId(): TEntityId;
}
export class ComponentRegistry {
private components = new Map<TEntityType, Map<TEntityId, IRegistrableComponent>>();
register(component: IRegistrableComponent): void;
unregister(component: IRegistrableComponent): void;
get<T>(type: TEntityType, id: TEntityId): T | undefined;
getAll<T>(type: TEntityType): T[];
forEach<T>(type: TEntityType, callback: (component: T, id: TEntityId) => void): void;
has(type: TEntityType, id: TEntityId): boolean;
getTypes(): TEntityType[];
count(type?: TEntityType): number;
clear(): void;
}2. Integration in Graph
// src/graph.ts
export class Graph {
public readonly componentRegistry = new ComponentRegistry();
public unmount() {
// ...existing cleanup
this.componentRegistry.clear();
}
}3. Abstract Methods in GraphComponent
// src/components/canvas/GraphComponent/index.tsx
export abstract class GraphComponent<...> implements IRegistrableComponent {
// REQUIRED: Must be implemented by all components
public abstract getEntityType(): string;
public abstract getEntityId(): string | number;
protected willMount() {
super.willMount();
// Auto-register on mount
this.context.graph.componentRegistry.register(this);
}
protected unmount() {
// Auto-unregister on unmount
this.context.graph.componentRegistry.unregister(this);
super.unmount();
}
}4. Implementation in Core Components
// Block
export class Block extends GraphComponent {
public static readonly ENTITY_TYPE = "block" as const;
public getEntityType(): string {
return Block.ENTITY_TYPE;
}
public getEntityId(): TBlockId {
return this.props.id;
}
}
// BaseConnection
export class BaseConnection extends GraphComponent {
public static readonly ENTITY_TYPE = "connection" as const;
public getEntityType(): string {
return BaseConnection.ENTITY_TYPE;
}
public getEntityId(): TConnectionId {
return this.props.id;
}
}
// Anchor
export class Anchor extends GraphComponent {
public static readonly ENTITY_TYPE = "anchor" as const;
public getEntityType(): string {
return Anchor.ENTITY_TYPE;
}
public getEntityId(): string {
return this.props.id;
}
}5. Type-Safe Access (Optional Enhancement)
// Built-in type mapping
export const EntityTypes = {
BLOCK: "block",
CONNECTION: "connection",
ANCHOR: "anchor",
} as const;
// Extensible interface for type safety
export interface EntityTypeMap {
block: Block;
connection: BaseConnection;
anchor: Anchor;
}
// Usage in consumer projects
declare module "@gravity-ui/graph" {
interface EntityTypeMap {
myCustomBlock: MyCustomBlock;
}
}🔄 Usage Examples
Basic Usage
const registry = graph.componentRegistry;
// Get single component
const block = registry.get<Block>("block", "block-123");
// Get all components of type
const allConnections = registry.getAll<BaseConnection>("connection");
// Iterate efficiently (no intermediate array)
registry.forEach<Block>("block", (block, id) => {
console.log(`Block ${id}: ${block.state.name}`);
});
// Check existence
if (registry.has("block", "block-123")) {
// ...
}Custom Components
// In plugin/consumer code
class MyOverlay extends GraphComponent {
public static readonly ENTITY_TYPE = "overlay";
public getEntityType(): string {
return MyOverlay.ENTITY_TYPE;
}
public getEntityId(): string {
return this.props.id;
}
}
// Access custom components
const overlay = graph.componentRegistry.get<MyOverlay>("overlay", "my-id");⚠️ Breaking Changes
This is a breaking change for custom components:
Before
class CustomBlock extends GraphComponent {
// No required methods
}After
class CustomBlock extends GraphComponent {
// REQUIRED: Must implement these methods
public getEntityType(): string {
return "customBlock";
}
public getEntityId(): string | number {
return this.props.id;
}
}Migration Guide
- Add
getEntityType()method returning a unique string identifier - Add
getEntityId()method returning the component's ID - Ensure IDs are unique within each entity type
🔗 Related
- Enables: Highlight System for Graph Components #118 (Highlight System)
- May improve: SelectionService, DragService
Metadata
Metadata
Assignees
Labels
No labels