Plugins
The plugin system is still under development and the API is subject to change.
The examples in this page use TypeScript; adapt them if you write JavaScript.
Introduction
The maxGraph plugins system aims to:
- reduce coupling in the code, in particular in the
Graphclass - improve the tree-shaking
- provide extension points
Historically, the Graph class coming from mxGraph was a monolithic class that included all the features.
In particular, it contained many handler instances that were gradually phased out and turned into plugins.
For more details about discussions and decisions, refer to the following discussions:
Available Plugins
The following table lists all built-in plugins provided by maxGraph.
Plugins marked as default are automatically loaded when using Graph. When using BaseGraph, no plugin is loaded, you must pass the ones you need explicitly.
| Plugin | Id, for getPlugin | Description | Kind | Default |
|---|---|---|---|---|
CellEditorHandler | 'CellEditorHandler' | In-place cell label editing. Requires loading CSS | Behavior | ✔️ |
ConnectionHandler | 'ConnectionHandler' | Drawing new edges between cells | Behavior, opt-in | ✔️ |
FitPlugin | 'fit' | Fit-to-container utilities (fit(), fitCenter()) | API | ✔️ |
ImageBundlePlugin | 'image-bundle' | Image bundle resolution for style.image keys (see Image Bundles) | API | ✔️ |
PanningHandler | 'PanningHandler' | Mouse and touch panning | Behavior, opt-in | ✔️ |
PopupMenuHandler | 'PopupMenuHandler' | Right-click context menus. Requires loading CSS | API | ✔️ |
RubberBandHandler | 'RubberBandHandler' | Rubber band (lasso) selection. Requires loading CSS | Behavior | ❌ |
SelectionCellsHandler | 'SelectionCellsHandler' | Manages per-cell selection handlers: the selection border, and the handles that resize a vertex, bend an edge or reconnect it. See Cell Handlers | Behavior | ✔️ |
SelectionHandler | 'SelectionHandler' | Moving and cloning the selection with the mouse, and selecting on mouse down. A plain click selects without it, that behavior belonging to the graph | Behavior | ✔️ |
TooltipHandler | 'TooltipHandler' | Hover tooltips. Requires loading CSS | Behavior, opt-in | ✔️ |
What the Kind column means
The Kind column answers one question: what does the plugin do if the application never calls it?
| Kind | With no call at all | What calling it buys you |
|---|---|---|
| Behavior | The feature works as soon as the plugin is in the list | Nothing beyond setting the configuration flags |
| Behavior, opt-in | Nothing, the feature stays inert until the application switches it on with a call made on the graph, see The plugins you must switch on. PanningHandler keeps a residual reaction to pinch and multi touch gestures | The feature itself |
| API | The feature does not exist, the call is what provides it. The plugin may still react to input, see PopupMenuHandler below | Everything: the call is the feature |
With FitPlugin, the call is the only thing that ever happens, so without it the feature does not exist. With
SelectionCellsHandler, every selected cell gets its border drawn whether the application calls anything or not. Its
handles are a separate matter: the ones that resize a vertex need SelectionHandler as well, and the rotation handle
needs VertexHandlerConfig.rotationEnabled, which is false by default.
Most of these plugins also expose extension points, a factory to replace or a method to override: the
ConstraintHandler factory of ConnectionHandler, the cell handler factories of SelectionCellsHandler, the
getTooltipForCell of TooltipHandler. The column does not track them, because they change a behavior that runs
anyway rather than decide whether it runs at all, and the page of each feature documents them.
The plugins you must switch on
Three plugins of the table are marked opt-in, because loading them is not enough: they register their listeners like any other behavior plugin, but the feature they exist for stays inert until the application switches it on.
| Plugin | What turns it on |
|---|---|
ConnectionHandler | graph.setConnectable(true) |
PanningHandler | graph.setPanning(true) |
TooltipHandler | graph.setTooltips(true) |
PopupMenuHandler needs a call too, and that is why it is an API plugin rather than an opt-in one: the
factoryMethod the application sets on it supplies the menu instead of adjusting one that would have appeared anyway.
Without it no menu is shown at all, not an empty one, since the popup is skipped entirely.
Loading one of these four and forgetting the call is the usual reason for concluding that a feature does not work. The
reverse is just as true, and it matters when trimming the plugin list of a BaseGraph: such a call is useless, and
silently does nothing, if the matching plugin is not in the list.
Two of the four are not entirely idle, so do not read them as "nothing happens at all". PanningHandler reacts to two
inputs that never test panningEnabled. A pinch gesture zooms, through the pinchEnabled field left at true and the
gesture events that only some touch browsers emit. And a drag pans whenever isForcePanningEvent returns true, which
it does for a multi touch event and whenever ignoreCell has been set, so that flag alone makes the graph pannable.
PopupMenuHandler, although it shows no menu, is enabled from the start: on a right-click it changes the selection,
clears it when the click lands on the background, hides the tooltip and consumes the event.
A Behavior plugin can still be retrieved with getPlugin, to set its configuration flags, as the PanningHandler
example of the next section does. That is not what makes a plugin an API one: the
distinction is whether anything happens when the application never calls the plugin at all.
Retrieving and Using a Plugin
Use the getPlugin method to retrieve a plugin instance from a Graph instance and then call the methods or properties it provides:
const graph = new Graph(container);
graph.setPanning(true); // panningEnabled is false by default
const panningHandler = graph.getPlugin<PanningHandler>('PanningHandler')!;
panningHandler.useLeftButtonForPanning = true; // otherwise panning needs the right button or ctrl+shift
panningHandler.ignoreCell = true; // pan even when the drag starts on a cell
setPanning(true) is what an ordinary drag tests, and the two paths that bypass it are described under
The plugins you must switch on.
getPlugin returns undefined when no plugin is registered with the given id, so its return type is nullable.
Here, the non-null assertion (!) is intentional: PanningHandler is one of the default plugins of Graph, so it is always available.
When the plugin may be absent, for example with BaseGraph or with a plugin list built at runtime, use optional chaining (?.) or an explicit check instead.
Pass the id as a string
Pass the id of the plugin as a string literal, as in the example above, rather than reading the pluginId property of
its class:
import type { PanningHandler } from '@maxgraph/core';
// Good: the class is only used as a type
const panningHandler = graph.getPlugin<PanningHandler>('PanningHandler');
// Avoid: reading pluginId needs a value import, `import { PanningHandler }`, so a runtime reference to the class
const otherPanningHandler = graph.getPlugin<PanningHandler>(PanningHandler.pluginId);
- The string is not a blind guess.
getPlugintakes aPluginId, which lists the ids of the built-in plugins, so the editor suggests them as you type. It accepts any other string as well, for custom plugins, so a misspelled id is not reported by the compiler: it makesgetPluginreturnundefined. - The string keeps the dependency on the plugin loose. The type parameter is erased at compile time, so
import typeis all the call needs: the calling module depends on the plugin only through its type, and not at all at runtime. ReadingpluginIdturns this into a hard dependency, since the calling module then imports the plugin module itself. Keeping the runtime dependencies between modules to what is needed makes the code more robust, and it is what lets a bundler leave out the code an application does not use. - Reading
pluginIdkeeps the plugin in the bundle. It needs a value import of the class, which the bundler cannot remove. This costs nothing when the application passes that plugin to the graph anyway, but code reaching an optional plugin, such asgraph.getPlugin<FitPlugin>('fit')?.fit()in a module shared by graphs that do not all load it, then ships the plugin even to the graphs that never use it. For example, with maxGraph 0.24.0 and 0.25.0, readingFitPlugin.pluginIdin an application that does not registerFitPluginadded about 1.6 to 1.9 kB to the bundle with Farm, Rollup, Rsbuild, Vite and webpack, as measured in PR #1270. See Tree-Shaking.
maxGraph currently provides no constant holding the ids apart from the plugin classes. For a custom plugin, you can
declare its id in a module of its own, imported by both the plugin class and the code calling getPlugin.
Choosing the Plugins to Use
The plugins to use can be specified when creating a graph.
The default plugins depend on which Graph class you use, see the Graph documentation page for details on choosing between Graph and BaseGraph.
Every plugin passed to the constructor is bundled with your application, so pass only the ones you need. See Register Only What You Use for the impact on the bundle size, in particular for read-only applications.
You can pass exactly the plugins you need via the constructor. Here is an example with Graph, where the RubberBandHandler plugin is added on top of the default plugins:
const graph = new Graph(container, undefined, [
...getDefaultPlugins(),
RubberBandHandler, // Enables rubber band selection
]);
Here is an example with BaseGraph:
const graph = new BaseGraph({
container,
plugins: [
CellEditorHandler,
SelectionCellsHandler,
SelectionHandler,
PanningHandler,
],
});
It is also possible to use a dedicated set of plugins, in particular when subclassing a built-in plugin.
In the following example, a MyCustomConnectionHandler plugin is used instead of the default ConnectionHandler plugin:
const graph = new BaseGraph({
container,
plugins: [
CellEditorHandler,
SelectionCellsHandler,
MyCustomConnectionHandler,
SelectionHandler,
],
});
When replacing a built-in plugin with a custom implementation, do not pass both the original and your subclass: they share a pluginId, so one of the two silently becomes unreachable through getPlugin and never receives its onDestroy(). A plugin explains why.
Creating a Custom Plugin
When a plugin is the right tool
A plugin is the unit of extension of a graph instance. Prefer one when the behavior you add has at least one of these traits:
- It owns resources that must be released.
destroy()callsonDestroyon every registered plugin first, before destroying the view and before clearing the listeners of the graph, so a plugin can still reach the graph and its container while cleaning up. This is what makes a plugin safe to write:PanningHandlerregisters a listener ondocumentitself, which nothing outside the plugin would remove, and drops it in itsonDestroyalong with its graph listeners. Without that hook, every such resource has to be tracked by the application. - It must be reachable from anywhere that holds the graph.
graph.getPlugin<MyPlugin>('my-plugin')returns the instance, so no module has to store it and no function has to receive it as an argument. A feature written as a plain helper class forces the application to answer "where do I keep this object, and how does that screen get it". - It is configured per graph. Each graph constructs its own plugins, so two graphs on the same page can behave differently. Values shared by every graph belong in the global configuration objects instead.
- It is optional. A plugin that is never passed to a graph is never referenced, so it is never bundled, which is the mechanism behind tree-shaking.
When it is not
- The extension point is global, not per graph. Shapes, edge styles, perimeters, edge markers and stencils live in registries shared by every graph of the page, listed in Global Configuration, and the codecs in a registry of their own, see Codecs and Serialization. Registering any of them from a plugin constructor would tie a global side effect to the lifetime of one graph.
- The feature must be added or removed while the application runs. The plugin list is fixed when the graph is
constructed: there is no
addPluginand noremovePlugin. Register the plugin once and let it do nothing while it is disabled, or create another graph. - You need a hook maxGraph does not offer. A plugin gets exactly two: its constructor, and
onDestroy. Everything else is wired by the plugin itself, withgraph.addListener,graph.addMouseListeneror plain DOM listeners, and must be unwired symmetrically inonDestroy.
Writing the plugin
A custom plugin is defined as a class:
- It must implement the
GraphPlugininterface, whose only member,onDestroy, is mandatory. It is called when the graph is destroyed. - It must satisfy the
GraphPluginConstructortype, which asks for the staticpluginIdbelow and for a constructor whose first parameter is the graph, typedAbstractGraphrather thanGraphso that aBaseGraphis accepted too. Further parameters are allowed as long as they have a default value, since the graph is the only argument ever passed. - Its
pluginIdis the key passed togetPlugin, not the name of the class. New plugins use a kebab-case id, lowercase for a single word, without aPluginorHandlersuffix:'image-bundle'and'fit'follow that rule, while the class-named ids of the historical handlers are kept for backwards compatibility only. - It can provide new methods to extend the existing API or introduce new behavior (using listeners, for example).
class MyCustomPlugin implements GraphPlugin {
static readonly pluginId = 'my-custom-plugin';
constructor(graph: AbstractGraph) {
// Initialization and configuration code, and the listeners the plugin needs
}
/** Releases the resources acquired by the plugin. */
onDestroy(): void {
// Unregister the event listeners, clear the timers, drop the references to the graph
// and to its cells, and empty the caches, to help garbage collection.
// Plugins holding none of these can leave the method empty, as `FitPlugin` does.
}
}