Skip to content

Make Graph.defaultLoopStyle configurable with a registered EdgeStyle string #758

Description

@tbouffard

Is your feature request related to a problem? Please describe

Currently, the Graph.defaultLoopStyle property directly references the EdgeStyle.Loop implementation. This hardcoded link limits flexibility and makes tree shaking impossible for users who do not rely on loop edges in their application.

Warning

This request assumes that missing a loop edge style won't break rendering. This needs to be verified when stories covering loop behavior are implemented (see the related issue's task for that check).

Describe the solution you'd like

We’d like to make Graph.defaultLoopStyle accept a string name referring to a registered edge style, not the direct function reference. For example: "loopEdgeStyle" instead of EdgeStyle.Loop.

  • It should support both a string or a function (like other edge-related props).
  • The default value can stay "loopEdgeStyle", to ensure backward compatibility.
  • This change would reduce unnecessary bundle size for apps that don’t use loops.

Notes

  • this will introduce a breaking change for people relying on the default loop implementation
  • now, they will have to be sure that the loop edge style is registered when using BaseGraph
  • when using Graph, there will be no change as the loop edge style is registered in that case
  • this is not a new constraint in practice: since 0.20.0, the loop edge style already has to be registered for the current function-reference configuration to behave correctly (see below)

Passing a function reference is already broken when the edge style is not registered

EdgeStyleRegistry keys its metadata (handlerKind, isOrthogonal, allowIntermediateHandles) by the EdgeStyleFunction reference itself. Loop styles are resolved through the very same path as regular edge styles (GraphView.getEdgeStyle), so an unregistered loop function silently falls back to the registry defaults: getHandlerKind returns 'default', isOrthogonal returns false, allowsIntermediateHandles returns true.

This is a regression introduced by #809, released in 0.20.0 (May 2025). Before that version, the edge handler selection compared the resolved edge style against hard-coded EdgeStyle references, so EdgeStyle.Loop always got the ElbowEdgeHandler whether or not it was registered:

if (edgeStyle == EdgeStyle.ElbowConnector || edgeStyle == EdgeStyle.Loop || ...) {
  result = this.createElbowEdgeHandler(state);
}

#809 replaced those hard-coded references with EdgeStyleRegistry.getHandlerKind(edgeStyle) to enable tree-shaking, which makes registration mandatory for the metadata to be found. registerLoopEdgeStyle declares handlerKind: 'elbow' for EdgeStyle.Loop, so today, setting defaultLoopStyle = EdgeStyle.Loop without registering it makes a selected loop edge use the default EdgeHandler instead of the ElbowEdgeHandler, and the edge handles are wrong. The same applies to any custom loop style passed as a function.

This is the same constraint that the CellStateStyle.edgeStyle JSDoc already documents ("IMPORTANT: when using an EdgeStyleFunction, be sure that is correctly registered in the EdgeStyleRegistry. Otherwise, the function may not be correctly configured."), except that loopStyle and defaultLoopStyle do not carry that warning today.

Switching to a string is therefore not a real restriction: the edge style has to be registered anyway for the behavior to be correct. The string form just makes that requirement explicit and impossible to get wrong, instead of failing silently.

Describe alternatives you've considered

Keeping the function reference in place hardcodes the dependency and prevents tree shaking.

This is not the same as the default edge, vertex, and label shapes defined in CellRenderer. Those are critical default implementations: without defaultVertexShape, defaultEdgeShape, or defaultTextShape, cells wouldn’t render at all.

These are the only concrete shape implementations that should be part of the codebase. Everything else - including loop styles - should ideally be configurable through registration.

  • defaultVertexShape = RectangleShape
  • defaultEdgeShape = ConnectorShape
  • defaultTextShape = TextShape

But unlike these core shapes, the loop edge style is not essential and shouldn’t be hardcoded.

Additional context

  • Allow string values for Graph.defaultLoopStyle. This would make the loop style consistent with other configurable elements via registration.
Graph.defaultLoopStyle = "loopEdgeStyle";
// OR
Graph.defaultLoopStyle = EdgeStyle.Loop;
  • We should also:
    • Add a Storybook story showing different loop configurations:
      • default loop from Graph
      • custom style with loopStyle
      • custom style with noLoop
      • custom style with orthogonalLoop
    • ⚠️ Fix mismatch in the CellStateStyle.orthogonalLoop JSDoc: currently mentions OrthConnector, but it's not used. Either the code or the JSDoc should be updated to reflect the actual implementation.
    • Document the registration requirement in the JSDoc, for the function form too:
      • AbstractGraph.defaultLoopStyle (packages/core/src/view/AbstractGraph.ts, moving to GraphView with Refactor Graph class and mixins to improve modularity and tree-shaking #762): add the warning that even when passing an EdgeStyleFunction instead of a registered name, the edge style must be registered in the EdgeStyleRegistry, otherwise its metadata (handlerKind, isOrthogonal, allowIntermediateHandles) is not found and the behavior is incorrect.
      • CellStateStyle.loopStyle (packages/core/src/types.ts): same warning.
      • Reuse the wording already present on CellStateStyle.edgeStyle, which documents exactly this constraint.
    • Align CellStateStyle.loopStyle with CellStateStyle.edgeStyle: type it as StyleEdgeStyleValue so a registered name can be used per cell too, instead of only an EdgeStyleFunction.

Important

Be aware that it is planned to move Graph.defaultLoopStyle to GraphView.defaultLoopStyle as part of #762

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions