Skip to content

feat: Add ComponentRegistry for unified component access #204

@draedful

Description

@draedful

🎯 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

  1. Mandatory registration — All components must implement identification methods
  2. Automatic lifecycle — Registration/unregistration happens in component lifecycle
  3. Instance-scoped — Registry belongs to Graph instance (supports multiple graphs)
  4. 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

  1. Add getEntityType() method returning a unique string identifier
  2. Add getEntityId() method returning the component's ID
  3. Ensure IDs are unique within each entity type

🔗 Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions