Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(joint-react): React package for JointJS #2391

Closed
wants to merge 21 commits into from

Conversation

kumilingus
Copy link
Contributor

@kumilingus kumilingus commented Nov 12, 2023

Description

An early stage React package for working with JointJS.

Demo: https://codesandbox.io/s/jointjs-react-vldmzw?file=/src/App.js

JointJS React

React core components for working with JointJS.

Note

The goal of this project is to define a minimal basis for the various React components for JointJS. Please help us shape the package by reporting issues or proposing API changes.

Important

This is an early stage product. The package may contain bugs and security issues. The API is subject to change.

Installation

yarn install jointjs @joint/react

API

Components

<Paper />

The main component that allows you to draw nodes and edges on the canvas. In JointJS terminology, you draw cells (elements and links) on paper. The component requires the <GraphProvider /> to be its ancestor.

Props
Property Type Description
options? dia.Paper.Options The options of the paper. It's async by default.
renderElement? (dia.Element) => React.JSX.Element | null A callback to render React components inside the paper element views. The components are rendered using the React.createPortal().
onReady? (dia.Paper) => void A callback that is triggered after the paper is mounted and ready (cells may not be rendered).
onEvent? (dia.Paper, eventName, ...eventArgs) => void A callback that allows you to listen to the dia.Paper events.
dataAttributes? string[] A list of model attributes that, if changed, will cause the renderElement function to be triggered. The default is ["data"].
portal? Element | string | (dia.ElementView) => string | Element An HTMLElement or SVGElement (or a string selector) that serves as portal for rendering element's content. By default, the portal is selector "portal".
Rendering React component inside an Element view

By default, the content of the element view is rendered using JointJS. However, the content (SVG or HTML) can also be rendered using React.

import { dia, shapes } from 'jointjs';
import { GraphProvider, Paper } from '@joint/react';

// Assumes that the `portal` node is a foreign object descendant.
const renderHTMLElement = (element) => {
   const { label, value } = element.get('data');
   return (
       <div className="my-element">
           <h3>{label}</h3>
           <input
               type='text'
               value={value}
               onChange={(e) => element.prop('data/value', e.target.value)}
           />
       </div>
   );
};

export default function Diagram() {
   const graph = new dia.Graph({}, { cellNamespace: shapes });
   const paperProps = { /* ... */ };
   return (
       <GraphProvider graph={graph}>
           <Paper renderHTMLElement={renderHTMLElement} ...paperProps />
       </GraphProvider>
   )
}

Currently, the following drawbacks are known with this approach:

  • content rendered with React cannot be the source/target of any link (it's always a good idea to also render the SVG <rect /> under the portal node).
  • <Paper /> currently replaces the default dia.Paper.prototype.options.elementView with a custom ElementView that triggers the portal:ready event ((elementView: dia.ElementView, portalEl: SVGElement | HTMLElement) => void) when the portal node is rendered (onRender() method). If you want to use custom views and you want to use renderElement with them, make sure you trigger the portal:ready event manually.
Paper provides context implicitly.

The <Paper /> context provides the paper context to its descendants implicitly. If you need to use the paper context outside of the <Paper />, use the <PaperProvider />.

<GraphProvider graph={graph}>
   <Paper>
       <MySelection/>{/* component is using JointJS `paper` */}
   </Paper>
</GraphProvider>

<PaperProvider />

The <PaperProvider /> component is a context provider that makes it possible to access the JointJS paper outside of the <Paper /> component. Unlike the GraphProvider, the PaperProvider is not mandatory.

import { GraphProvider, PaperProvider, Paper } from '@joint/react';

export default function Diagram() {
    return (
        <GraphProvider graph={graph}>
            <PaperProvider>
                <Paper/>
                <MyZoomInButton/>
                <MyZoomOutButton/>
            </PaperProvider>
        </GraphProvider>
    )
}

<GraphProvider />

The <GraphProvider /> component is a context provider that provides JointJS graph to <Paper /> components. You need use the <GraphProvider /> in order to render the <Paper /> component.

Here's an example of a GraphProvider providing a graph to two papers.

import { dia, shapes } from 'jointjs';
import { GraphProvider, Paper } from '@joint/react';

export default function Diagram() {
    const graph = new dia.Graph({}, { cellNamespace: shapes });
    const paperProps = { /* ... */ };
    const minimapProps = {  /* ... */ };
    return (
        <GraphProvider graph={graph}>
            <Paper ...paperProps className="canvas" />
            <Paper ...minimapProps className="minimap" />
        </GraphProvider>
    )
}
Props
Property Type Description
graph dia.Graph A graph instance to be provided to the descendants

Hooks

The package exposed several custom hooks.

usePaper

The usePaper is a hook that let you use the JointJS paper from your component.

import { usePaper } from '@joint/react';

export default function MyZoomInButton() {
    const paper = usePaper();
    const zoomIn = () => {
        if (!paper) return;
        const { sx, sy } = paper.scale();
        paper.scale(sx * 2, sy * 2);
    }
    return <button onClick={zoomIn}>Zoom In</button>
}

useGraph

The useGraph is a hook that let you use the JointJS graph from your component.

import { useGraph } from '@joint/react';

export default function MyDeleteAllButton() {
    const graph = useGraph();
    const deleteAll = () => {
        if (graph && confirm("Are you sure you want to delete all content?")) {
            graph.clear();
        }
    }
    return <button onClick={deleteAll}>Delete All</button>
}

What's next

The possible tasks ahead of us.

JointJS+

Define React Components for JointJS+.

// NOTE: This is fictional code
// It is just an example of possible API.
import { dia, mvc } from '@joint/core';
import { CommandManager } from '@joint/command-manager';
import { Paper } from '@joint/react';
import DiagramProvider from '@joint/diagram-provider';
import Toolbar from '@joint/toolbar-react';
import Stencil from '@joint/stencil-react';
import Scroller from '@joint/scroller-react';
import Inspector from '@joint/inspector-react';
import Selection from '@joint/selection-react';
import Snaplines from '@joint/snaplines-react';
import Grid from 'some-ui-lib';

export default function Diagram() {
    const graph = new dia.Graph();
    const cmd = new CommandManager({ graph });
    const selection = new mvc.Collection();
    return (
        <DiagramProvider graph={graph} commandManager={cmd} selection={selection}/>
            <Toolbar />
            <Grid>
                <Stencil />
                <Scroller />
                    <Paper >
                        <Selection />
                        <Snaplines />
                    </Paper>
                <Scroller>
                <Navigator />
            </Grid>
            <Navigator />
        <DiagramProvider />
    )
}

Higher-level Components

Define user-friendly higher-level components.

// Paper, Scroller, Toolbar, CommandManager as a single component
<Diagram preset="kitchen-sink" width={400} height={400} fitView={true} virtualRendering={true}></Diagram>
// Domain-specific diagram components
<OrgChartDiagram data={adjacencyList}></OrgChartDiagram>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants