Fabric.js Document Engine

Search documentation

Find a page or section

中文

A Vue 3 component with undo, redo and save status.

Fabric.js Vue 3 apps have one classic bug: putting the Fabric.js canvas in a deep ref. Vue wraps it in a reactive proxy, and Fabric.js then fails in strange ways, such as loadFromJSON not rendering. Keep the canvas and the engine in shallowRef or plain variables, and use createDocumentStateStore for the toolbar state.

The component

<script setup lang="ts">
import { Canvas, Rect } from "fabric";
import {
  createDocumentEngine,
  createDocumentStateStore,
  type DocumentEngine,
  type DocumentState,
} from "fabricjs-document-engine";
import { onBeforeUnmount, onMounted, shallowRef, useTemplateRef } from "vue";
 
const element = useTemplateRef<HTMLCanvasElement>("element");
const engine = shallowRef<DocumentEngine>();
const state = shallowRef<DocumentState>();
let cleanup = () => {};
 
onMounted(() => {
  const canvas = new Canvas(element.value!, { width: 800, height: 500 });
  const created = createDocumentEngine({ canvas });
  const store = createDocumentStateStore(created);
 
  state.value = store.getSnapshot();
  const unsubscribe = store.subscribe(() => {
    state.value = store.getSnapshot();
  });
  engine.value = created;
 
  cleanup = () => {
    unsubscribe();
    store.destroy();
    created.destroy();
    canvas.dispose();
  };
});
 
onBeforeUnmount(() => cleanup());
 
function addSquare() {
  engine.value?.canvas.add(
    new Rect({ width: 80, height: 60, fill: "#2a9d8f" })
  );
}
</script>
 
<template>
  <div role="toolbar">
    <button @click="addSquare">Square</button>
    <button :disabled="!state?.canUndo" @click="engine?.undo()">
      Undo {{ state?.undoLabel }}
    </button>
    <button :disabled="!state?.canRedo" @click="engine?.redo()">Redo</button>
  </div>
  <canvas ref="element" aria-label="Drawing canvas" />
</template>

Why shallowRef

shallowRef only tracks when you replace .value. It never wraps the Fabric.js objects inside, so Fabric.js sees the real objects. The state snapshot is a new object on each change, so shallowRef still updates the template.

When you already have a reactive canvas

If a Fabric.js canvas ended up inside reactive() or a deep ref, pass the raw object with toRaw:

import { toRaw } from "vue";
 
const engine = createDocumentEngine({ canvas: toRaw(canvasRef.value) });

The same applies in Pinia stores: keep Fabric.js objects out of store state, or mark them with markRaw.

The rest of the Fabric.js Vue 3 setup is the same as in plain JavaScript. For saving, add a storage adapter.