Skip to content

Commit

Permalink
feat(joint-react): before add graph-store
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelgja committed Feb 4, 2025
1 parent ca2c0ea commit de3b197
Show file tree
Hide file tree
Showing 17 changed files with 321 additions and 190 deletions.
7 changes: 4 additions & 3 deletions packages/joint-react/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Storybook
.storybook-out/

# Editor settings
.vscode/
.idea/
Expand All @@ -43,3 +40,7 @@ coverage/
# Environment variables
.env
.env.*

# Storybook
storybook-static/
.storybook-out/
2 changes: 1 addition & 1 deletion packages/joint-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
"react-dom": "^19.0.0"
},
"devDependencies": {
"storybook": "^8.5.3",
"@eslint-react/eslint-plugin": "1.15.1",
"@eslint/compat": "^1.1.1",
"@eslint/js": "^9.8.0",
"@happy-dom/global-registrator": "16.7.3",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-onboarding": "^8.4.7",
Expand Down
51 changes: 6 additions & 45 deletions packages/joint-react/src/components/graph-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { useEffect, useState } from 'react'
import { isDiaCellsJSON, type Cell } from '../types/cell.types'
import { useId, useMemo, useState } from 'react'
import { GraphContext } from '../context/graph-context'
import { dia, shapes } from '@joint/core'
import { updateGraph } from '../utils/update-graph'

export interface GraphProps<T extends dia.Cell.ID> {
export interface GraphProps {
/**
* Graph instance to use. If not provided, a new graph instance will be created.
* @see https://docs.jointjs.com/api/dia/Graph
Expand All @@ -26,62 +24,25 @@ export interface GraphProps<T extends dia.Cell.ID> {
* @see https://docs.jointjs.com/api/dia/Cell
*/
readonly cellModel?: typeof dia.Cell
/**
* Cells to render - elements (nodes) or links (edges).
* @see https://docs.jointjs.com/api/dia/Cell
*/
readonly cells?: Array<Cell<T>>
/**
* Callback to handle cells change.
* This handle events directly from the graph instance.
* This event is not triggered when cells are changed via React state.
*/
readonly onCellsChange?: (cells: Array<Cell<T>>) => void
readonly onCellsChange?: (cells: Array<dia.Cell>) => void
}

/**
* GraphProvider component creates a graph instance and provides it to its children via context.
* It also handles updates to the graph when cells change via React state or JointJS events.
*/
export function GraphProvider<T extends dia.Cell.ID>(props: GraphProps<T>) {
const { children, cellNamespace = shapes, cellModel, cells, onCellsChange } = props
export function GraphProvider(props: GraphProps) {
const { children, cellNamespace = shapes, cellModel, onCellsChange } = props

const [graphValue] = useState(() => {
const graph = props.graph ?? new dia.Graph({}, { cellNamespace, cellModel })
if (cells?.length && isDiaCellsJSON(cells)) {
graph.addCells(cells)
}
return graph
})

// Update the graph when cells change via react state.
useEffect(() => {
if (cells?.length && isDiaCellsJSON(cells)) {
updateGraph(graphValue, cells)
} else {
graphValue.clear()
}

if (!onCellsChange) {
return
}
/**
* Events handled by jointjs should be debounced to avoid performance issues.
* TODO: We should talk about it, maybe use some trotting instead of debouncing.
*/
const handleCellsChange = () => {
onCellsChange(graphValue.getCells().map((cell) => cell.toJSON() as Cell<T>))
}

graphValue.on('all', handleCellsChange)
return () => {
graphValue.off('all')
}
}, [cells, graphValue, onCellsChange])

// Handle cells change events via graph jointjs events.
// useEffect(() => {

// }, [eventDebounceMs, graphValue, onCellsChange])

return <GraphContext.Provider value={graphValue}>{children}</GraphContext.Provider>
}
13 changes: 12 additions & 1 deletion packages/joint-react/src/components/paper-portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ export interface PaperPortalProps extends PaperElement {
/**
* A function that renders the element. It is called every time the element is rendered.
*/
renderElement: (element: dia.Cell.JSON) => ReactNode
renderElement: (element: dia.Cell) => ReactNode
/**
* Internal version of the paper element.
*/
version: number
}

/**
Expand All @@ -18,6 +22,13 @@ export interface PaperPortalProps extends PaperElement {
*/
function Component(props: Readonly<PaperPortalProps>) {
const { renderElement, cell, containerElement } = props
// useEffect(() => {
// cell.on('change', () => {})
// return () => {
// cell.off('change')
// }
// }, [])

return createPortal(renderElement(cell), containerElement)
}

Expand Down
79 changes: 30 additions & 49 deletions packages/joint-react/src/components/paper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,13 @@ export interface PaperProps extends dia.Paper.Options {
/**
* A function that renders the element. It is called every time the element is rendered.
*/
renderElement?: (element: dia.Cell.JSON) => ReactNode
renderElement?: (element: dia.Cell) => ReactNode
/**
* A function that is called when the paper is ready.
* @param paper The JointJS paper instance.
*/
onReady?: (paper: dia.Paper) => void
/**
* A function that is called when an event is triggered on the paper.
* @param paper The JointJS paper instance.
* @param args The arguments passed to the event.
*/
onEvent?: (paper: dia.Paper, ...args: unknown[]) => void

/**
* The style of the paper element.
*/
Expand All @@ -43,34 +38,24 @@ export interface PaperProps extends dia.Paper.Options {
* Class name of the paper element.
*/
className?: string
/**
* The data attributes to listen to changes.
*/
dataAttributes?: string[]

/**
* The selector of the portal element.
*/
portalSelector?: string | ((view: dia.ElementView) => HTMLElement | null)
}

export interface PaperElement {
cell: dia.Cell.JSON
cell: dia.Cell
containerElement: HTMLElement
version: number
}

/**
* Paper component that renders the JointJS paper element.
*/
function Component(props: Readonly<PaperProps>) {
const {
renderElement,
onReady,
onEvent,
style,
className,
dataAttributes = ['data'],
...paperOptions
} = props
const { renderElement, onReady, style, className, ...paperOptions } = props

const paperWrapperElementRef = useRef<HTMLDivElement | null>(null)
const paper = usePaper(paperOptions)
Expand All @@ -91,26 +76,22 @@ function Component(props: Readonly<PaperProps>) {
const controller = new mvc.Listener()

// Update the elements state when the graph data changes
// const attributeChangeEvents = dataAttributes.map((attribute) => `change:${attribute}`).join(' ')

controller.listenTo(paper, 'resize', resizePaperWrapper)

// We need to setup the react state for the element only when renderElement is provided
if (renderElement) {
const onChange = (model: dia.Cell) => {
controller.listenTo(graph, 'change', (cell: dia.Cell) => {
setElements((previousState) => {
const { id } = model
const element = previousState.find((ele) => ele.cell.id === id)
const { id } = cell
const element = previousState.find((e) => e.cell.id === id)
if (element) {
return previousState.map((elementItem) =>
elementItem.cell.id === id ? { ...elementItem, cell: model.toJSON() } : elementItem
)
element.version += 1
return [...previousState]
}
return previousState
})
}

controller.listenTo(graph, 'change', onChange)
})
controller.listenTo(graph, 'remove', (model: dia.Cell) => {
setElements((previousState) => {
const { id } = model
Expand All @@ -122,23 +103,20 @@ function Component(props: Readonly<PaperProps>) {
controller.listenTo(
paper,
PAPER_PORTAL_RENDER_EVENT,
({ model }: dia.ElementView, portalElement: HTMLElement) => {
({ model: cell }: dia.ElementView, portalElement: HTMLElement) => {
setElements((previousElements) => {
const newElements = previousElements.filter(({ cell: { id } }) => id !== model.id)
return [...newElements, { cell: model.toJSON(), containerElement: portalElement }]
const newElements = previousElements.filter(({ cell: { id } }) => id !== cell.id)
return [...newElements, { cell, containerElement: portalElement, version: 0 }]
})
}
)
}

if (onEvent) {
controller.listenTo(paper, 'all', (...args) => onEvent(paper, ...args))
}

return () => controller.stopListening()
}, [graph, onEvent, paper, renderElement, resizePaperWrapper])
}, [graph, paper, renderElement, resizePaperWrapper])

useEffect(() => {
console.log('why mounbt?')
paperWrapperElementRef.current?.append(paper.el)
resizePaperWrapper()
paper.unfreeze()
Expand All @@ -153,21 +131,24 @@ function Component(props: Readonly<PaperProps>) {
paper.freeze()
unbindEvents()
}
}, [bindEvents, dataAttributes, graph, onEvent, onReady, paper, resizePaperWrapper]) // options, onReady, onEvent, style
}, [bindEvents, graph, onReady, paper, resizePaperWrapper]) // options, onReady, onEvent, style

const hasRenderElement = !!renderElement

console.log('Paper render')
return (
<div className={className} ref={paperWrapperElementRef} style={style}>
{hasRenderElement &&
elements.map((element) => (
<PaperPortal
key={element.cell.id}
cell={element.cell}
containerElement={element.containerElement}
renderElement={renderElement}
/>
))}
elements.map((element) => {
return (
<PaperPortal
key={element.cell.id}
cell={element.cell}
containerElement={element.containerElement}
renderElement={renderElement}
version={element.version}
/>
)
})}
</div>
)
}
Expand Down
97 changes: 97 additions & 0 deletions packages/joint-react/src/components/stories/paper-stress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { dia, shapes } from '@joint/core'
import { ReactElement } from '../../models/react-element'
import { GraphProvider } from '../graph-provider'
import { Paper } from '../paper'
import { PaperProvider } from '../paper-provider'
import { PaperStory, paperStoryOptions } from './paper.stories'
import { useGraphCells } from '../../hooks/use-graph-cells'

const graph = new dia.Graph({}, { cellNamespace: { ...shapes, ReactElement } })
function createElements(xCount: number, yCount: number) {
const elements = []
const ELEMENT_SIZE = 40
const MARGIN = 2
for (let x = 0; x < xCount; x++) {
for (let y = 0; y < yCount; y++) {
elements.push(
new ReactElement({
id: `${x}-${y}`,
position: { x: x * (ELEMENT_SIZE + MARGIN), y: y * (ELEMENT_SIZE + MARGIN) },
size: { width: ELEMENT_SIZE, height: ELEMENT_SIZE },
attrs: {
label: { text: `${x}-${y}` },
body: { fill: 'blue', stroke: 'black' },
},
})
)
}
}
return elements
}

graph.addCells(createElements(30, 30))
function RandomChange() {
const cells = useGraphCells()
// console.log('re-render Check with count:', cells.length)

return (
<button
onClick={() => {
// setCells((previousCells) =>
// previousCells.map((cell) => {
// cell.set({
// position: { x: Math.random() * 1000, y: Math.random() * 1000 },
// })
// return cell
// })
// )
}}
>
Random move {cells.length} elements
</button>
)
}

export const PaperStressTestNative: PaperStory = {
args: {
style: { border: '1px solid #ccc' },
},
render: () => {
console.log('re-render WithHooksAPI')
return (
<GraphProvider graph={graph}>
<RandomChange />
<div style={{ display: 'flex', flex: 1 }}>
<PaperProvider {...paperStoryOptions}>
<Paper />
</PaperProvider>
</div>
</GraphProvider>
)
},
}

export const PaperStressTestReact: PaperStory = {
args: {
style: { border: '1px solid #ccc' },
},
render: () => {
console.log('re-render WithHooksAPI')
return (
<GraphProvider graph={graph}>
<RandomChange />
<div style={{ display: 'flex', flex: 1 }}>
<PaperProvider {...paperStoryOptions}>
<Paper
renderElement={(element) => (
<div style={{ fontSize: 10 }} onClick={() => console.log('Click from React')}>
{JSON.stringify(element)}
</div>
)}
/>
</PaperProvider>
</div>
</GraphProvider>
)
},
}
Loading

0 comments on commit de3b197

Please sign in to comment.