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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions