[]previous and next page

ShadowJS

Use CanvasFlow in a ShadowJS or Dusk app with a component that returns real DOM and signals for the editor's state.

ShadowJS renders real DOM with fine-grained signals and no virtual DOM, which is also how a canvas editor wants to live. canvasflow/shadow gives you <CanvasFlow>, <CanvasFlowCanvas> and functions that return ShadowJS signals. It needs @shadow-js/core, and works in Dusk apps too. This site is one: the headless demo on the Building your own UI page is a ShadowJS component.

A board in one component#

Board.tsxtsx
import { CanvasFlow } from 'canvasflow/shadow'

export function Board() {
  return <CanvasFlow persistenceKey="board" style="height: 100vh" />
}

The component returns its host element, and mounts the canvas and the default UI into it when it is attached. Give it a height, because an editor needs room.

Props#

Everything createCanvasFlow accepts is a prop (persistenceKey, dir, readonly, themes, plugins, shapes, tools, storage…), plus:

Prop What it does
ui false for no default UI, or an object that configures it (see Customizing the UI).
onMount Called once with (editor, app, ui). Return a function and it runs as a cleanup.
class, style Passed to the element. style is a string.

Options are read once, when the canvas mounts. dir and readonly follow later changes.

Get the editor, and follow it#

Keep the editor in a signal, set it from onMount, and hand the signal to the functions that follow it. Each returns a signal that is undefined until the canvas has mounted.

Board.tsxtsx
import { useSignal } from '@shadow-js/core'
import { CanvasFlow, useSelectedIds, useValue, type Editor } from 'canvasflow/shadow'

export function Board() {
  const [editor, setEditor] = useSignal<Editor | null>(null)
  const selected = useSelectedIds(editor)
  const shapes = useValue(editor, (e) => e.getCurrentPageShapes().length)

  return (
    <div>
      <CanvasFlow persistenceKey="board" style="height: 80vh" onMount={(e) => setEditor(e)} />
      <p>
        {selected()?.length ?? 0} selected of {shapes() ?? 0}
      </p>
    </div>
  )
}

useValue(editor, selector, isEqual?) re-reads on every change and updates the signal only when the result differs. useEditorEvent(editor, 'tool', handler) listens to an event for the owner's lifetime. useTool, useCamera, useCanUndo, useCanRedo, useTheme and useDirection are ready-made.

Headless#

<CanvasFlowCanvas> is the canvas without the default UI, and never references it, so your bundle does not carry the toolbar. Build your own:

Board.tsxtsx
import { useSignal } from '@shadow-js/core'
import { CanvasFlowCanvas, useTool, type Editor } from 'canvasflow/shadow'

export function Board() {
  const [editor, setEditor] = useSignal<Editor | null>(null)
  const tool = useTool(editor)
  return (
    <div>
      <div role="toolbar">
        {['select', 'draw', 'rectangle', 'arrow', 'text'].map((id) => (
          <button type="button" aria-pressed={tool() === id} onClick={() => editor()?.setTool(id)}>
            {id}
          </button>
        ))}
      </div>
      <CanvasFlowCanvas style="height: 80vh" onMount={(e) => setEditor(e)} />
    </div>
  )
}

See Building your own UI for the full tour.

Server rendering#

Importing canvasflow/shadow on a server touches no browser globals. The canvas itself mounts in the browser, in onMount.