Batteries included React Component for rendering, creating and editing diagrams.
- The data is the source of truth
- Can add custom Entities as React Components
- Adding custom icons for these entities appear in Panel and Context Menu
- Infinite canvas to work with; panning
- Zooming for detail work or bird's eye view
- Configurable grid, snap to grid (or no grid at all)
- Continuous feedback while editing
- History, undo, redo; keyboard shortcuts
- Panel for adding new Entities, with drag or click
- Automatic arrow placement
- Labels for arrows
- Editable arrow labels (From the UI; the model and render already contemplates this feature)
- Editable arrow paths (From the UI; the model and render already contemplates this feature)
- Select several entities
- Copy and paste entities
- Alignment tools
It's easier to see the an example already working to explain what we need to add. Pull react-flow-diagram-example and follow along; consider it a finished state of following these instructions. You can also use this repo as a starting point for your own implementation.
Let's assume a fresh create-react-app (which the example is created from) structure, then:
yarn add react-flow-diagram
And we'll also add styled-components for our custom entities, although this is not a requirement. You can just add classNames
to elements and use regular css or any css transpilation language.
yarn add styled-components
in /src/
we create a /CustomDiagram/
following a standard of Grouping by features
And in it we create:
The star of the show, the model that holds the data that will be represented as a diagram.
You don't need to consciously know the structure of the model, since you can always start a diagram via the UI, save it and resume from the UI without ever touching the model. But you may need this information for your specific use case.
The structure held in the model
const can be defined in flow types as (you don't really need to know flowtype to grasp this):
type EntityState = Array<EntityModel>;
model is an array with EntityModel
s representing each entity
type EntityType = string;
type EntityModel = {
id: EntityId, // unique identifier of the Entity
type: EntityType, // type of entity, according to your custom entity components
width: number, // width
height: number, // height
x: number, // x position
y: number, // y position
name: string, // label of the entity
linksTo?: Links, // reference to other entities
custom?: Object, // custom data for you to extend functionalities
};
The id
of an EntityModel
is an EntityId
; which is just a string
type EntityId = string;
The linksTo
attribute is optional (an entity may not link to anyone) and holds a Links
type, which is an array of Link
type Links = Array<Link>;
type Link = {
target: EntityId, // Id of another entity which is being linked to
edited: boolean, // whether or not the link was autogenerated or was edited by the user
points?: Array<Point>, // Array of points that define the position of the link
label?: string, // link label
color?: string, // link color (currently not implemented)
};
The points
attribute is also optional (as well as any attribute ending with ?
) and holds an array of Point
type Point = {
x: number, // x position
y: number, // y position
};
Check model-example.js to see who this all pans out.
In this file we have two configuration objects, config
which deals with diagram configurations and customEntities
which holds references to our custom components that represent each type of entity.
config
can be defined as:
type ConfigState = {
entityTypes: ConfigEntityTypes, // Size of entities
gridSize?: number, // optional grid size
};
ConfigEntityTypes
is an object being used as a Map whose keys reference the types of entities.
type ConfigEntityTypes = {
[EntityType]: {
width: number,
height: number,
},
};
It's recommended to find an entity size that is a multiple of the grid size; however you're free to choose any positive number
.
customEntities
can be defined as:
type CustomEntities = {
[EntityType]: {
component: ComponentType<DiagComponentProps>,
icon: {
path: Element<*>,
size: number,
},
},
};
The component
attribute holds a reference to a React Component that will be provided DiagComponentProps
props. The creation of your custom components is covered in Creating our own Entities
type DiagComponentProps = {
model: EntityModel,
meta: MetaEntityModel,
setName: SetNamePayload => EntityAction,
};
You can use this props to get information about the component, and setName
method to change the name of the component.
We referenced EntityModel
before, and MetaEntityModel
can be defined as:
type MetaEntityModel = {
id: EntityId,
isAnchored: boolean,
isSelected: boolean,
};
Which is information that only matters while interacting with the components on the UI. You wouldn't need to save this information. A HOC Entity component will deal about positioning, selection, context menus and more for you, so you just need to focus on the specific features of your custom Entity.
On index we define our custom Component that initializes seting throguh componentWillMount
and passes customEntities
to the Diagram
Component
import React from 'react';
import {
Diagram,
store as diagramStore,
setEntities,
setConfig,
diagramOn,
} from 'react-flow-diagram';
import model from './model-example';
import { config, customEntities } from './config-example';
class CustomDiagram extends React.PureComponent {
componentWillMount() {
diagramStore.dispatch(setConfig(config));
diagramStore.dispatch(setEntities(model));
diagramOn('anyChange', entityState =>
// You can get the model back
// after modifying the UI representation
console.info(entityState)
);
}
render() {
return <Diagram customEntities={customEntities} />;
}
}
export default CustomDiagram;
We dispatch a setConfig
and setEntities
action to the diagramStore
, and on anyChange
we can get a hold of our modified entityState
We'll cover this in the next section:
This component does not come with any custom entities, and you must create your own. However on react-flow-diagram-example we have two examples of custom entities, namely task and event.
Let's take a look at the task entitiy as an example.
In the task folder we see two files:
Let's start with...
Our custom component will be visually rendered on the UI in such a way that it uniquely identifies a specific concept. We can take for example BPMN's elements or UX flow diagrams. Perhaps in the future we'll include Packs with custom entities for these types of use cases, but for now you'll have to create your own.
Our entity can also deal with user interaction such as changing the name and whatever interaction you may come up with, with the possibility of using already present information in the EntityModel
or adding your custom data to custom
object field.
There are no specific requirements for the component. What we do need to know is that the component will be provided DiagComponentProps
props, which encompasses:
type DiagComponentProps = {
model: EntityModel,
meta: MetaEntityModel,
setName: SetNamePayload => EntityAction,
};
More details about EntityModel
in the model-example.js section and MetaEntityModel
in the config-example.js section.
setName
is a connected action that takes the propery SetNamePayload
and returns EntityAction
. The return of the function can be ignored since the important aspect is the side effect that sets the name of the entity.
type SetNamePayload = { id: EntityId, name: string };
A usage example in component.js is:
handleKeyPress = (ev) => {
switch (ev.key) {
case 'Enter':
this.toggleEdit(false);
this.props.setName({ id: this.props.model.id, name: this.state.name });
break;
case 'Escape':
this.toggleEdit(false);
this.setState({ name: this.props.model.name });
break;
// no default
}
};
We also need to provide an icon which will be used in the Panel for adding new elements and in the contextual menu for each entity to quickly add new entities.
import React from 'react';
const icon = {
path: (
<path d="M14 0H2C1 0 0 1 0 2v12c0 1 1 2 2 2h12c1 0 2-1 2-2V2c0-1-1-2-2-2z" />
),
size: 16,
};
export default icon;
The icon consists of a path
SVG React Element and a size that is the same as the size of viewBox
SVG attribute. Check the icon component to make this more clear.
The size attribute is provided so you don't need to transform the SVG element to fix exactly the needs of the panel or context menu. If we didn't have the size attribute, the SVG element may overflow or underflow its container.
You can use react-flow-diagram-example as a template of a working implementation and modify it for your own needs.
With time I'll add more examples, namely:
- Integration with Redux
- Using the provided flow types
In a Trailblazer related project I had the task of creating an editing environment surrounding bpmn-js which was the first diagramming library we used. bpmn-js proved to be a feature rich library, but extending to add the features we wanted proved to be practically impossible, coupled with dwindling documentation. It's still a great option if you're not thinking of adding your own types of entities or swerving from the BPMN model at all.
After assessing many diagramming libraries available, my second go after bpmn-js was react-diagrams; which worked pretty well, had modern JS standards and was more extensible. However the source of truth (model) was scattered through many object's internal state and getting to the underlying data proved difficult in many scenarios.
I was quite happy with the idea of using a time-tested open source library, but the options available left me making the decision to build an alternative. I hope it's useful for you!
TODO