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

How can I use Fabric.js in something like React? #5951

Closed
maniac0s opened this issue Nov 5, 2019 · 27 comments
Closed

How can I use Fabric.js in something like React? #5951

maniac0s opened this issue Nov 5, 2019 · 27 comments
Labels
stale Issue marked as stale by the stale bot

Comments

@maniac0s
Copy link

maniac0s commented Nov 5, 2019

I have already asked a question to SO but got no replies except a comment that points to an old, similar question with an answer that uses outdated technology.

https://stackoverflow.com/questions/58689343/how-can-i-use-something-like-fabric-js-with-react

How would I do this in ES6, latest React and without relying on some third-party modules that are abandoned for years?

@asturur
Copy link
Member

asturur commented Nov 5, 2019

takes a bit of time to organize an answer, but the things you mentioned are more or less good.
You need to make the fabricCanvas accessible to all components with something like context. You should initialize it in the component and not at app level. All the rest is plain fabric stuff and dispatching actions.
I'll try to answer there.

@maniac0s
Copy link
Author

maniac0s commented Nov 5, 2019

Thanks for your response, it helps me a lot to see if I am looking into the right direction.

I already started to fiddle around with React.Context and try to implement it this way. I thought about having the fabric.Canvas object somehow in the Context, so a thing like "DrawButton" can toggle fabricObject.isDrawingMode = true/false. This should give me the basic understanding how to construct the data store and access it from children. Starting from there I probably can figure out how to do stuff like getting the active object (to determine which toolbox I should open), a list of all objects on the canvas (to implement something like a "layer" list) and eventually rendering the whole canvas to something like SVG or PNG using fabrics .toXYZ method.

As a first prototype I had thrown everything into App.js and just trigger App.js methods that access this.the_canvas directly. But it went messy pretty fast so now I am trying to find a way to make it more modular.

@kingschnulli
Copy link
Contributor

I'm in no way a React expert, but what we did for vuejs was a wrapper component that exposes all the needed methods of Fabric and handles the communication in-between. I can give you an example, but just in vuejs if you are firm with this.

@maniac0s
Copy link
Author

maniac0s commented Nov 6, 2019

I am not firm with Vue, even less than with React, but maybe it could give some ideas. But basically that's what I thought it would work with React.Context: You write a context provider that holds all the stuff in it's state and provides it as a context to it's toolbar children and the canvas component.
But I am not sure if that's the best practice to do that...
Also I noticed a strange issue when playing with this: I create the fabric canvas in a component FabricCanvas:

componentDidMount() {
    this.the_canvas = new fabric.Canvas('main-canvas', {
        preserveObjectStacking: true,
        height: 375,
        width: 375
    });
}

and there I can set things on the object like

componentDidUpdate() {
    this.the_canvas.isDrawingMode = this.context.drawMode;
    console.log(" FabricCanvas this.the_canvas.isDrawingMode: " + this.the_canvas.isDrawingMode);
}

Which I can trigger as a function in the context (let's say a button sets context.drawMode = true which triggers component update and sets the isDrawingMode). But when I try to move that whole thing, including creation of the fabric.Canvas object into the context provider one level above FabricCanvas, the canvas gets created, the html-canvas has the default blue fabric selection box and I can console.log it and see it's a fabric canvas object but when I then set isDrawingMode = true on that object, it shows isDrawingMode: true in console.log however the actual canvas never switches to draw mode, it stays as it is (I get the selection box, no freehand drawing). I don't know why this happens must be something with how Javascript handles the DOM tree I guess?

Which makes it awkward, because I now have to decide in contextDidUpdate what change happened and set the this.the_canvas object attributes accordingly respectively need to call a local function that does things like new fabric.Text to the canvas.

@asturur
Copy link
Member

asturur commented Nov 17, 2019

So i m probably late to the party.
I do not use context os much part for sharing some global object that is good to call fabric from every compontent ( the alternative is to import a global variable in a single file everywhere, making your canvas a singleton and the app very difficult to refactor later if you need more than one canvas for any reason).

Consider creating an hook like withFabricCanvasAccess that uses useContext and lets you have the fabricCanvas availabe to trigger some changes.

Different case is for UI updates. You will have a store and you have to put some fabric information there to update your UI.

Use fabric events like selection:created and selection:updated to send in the store for example, the information of the object that has been selected, type, color, stroke or all of it. Use this store information to draw your UI with the classic react-redux binding of your choice ( or for the store you chose ).

Be careful with re-renders. Every state update will start to redraw all of your app if you do not put gates somewhere ( a react Memo, a connect with plain props, a PureComponent )

I'm not sure in those 15 days what you did so far and how is going.

@maniac0s
Copy link
Author

The context thing did not work in object oriented. When I had a component set a parameter of fabrics canvas (like canvas.isDrawingMode), it did not get propagated to the canvas created, the mode was set in the fabric object but didnt affect the real canvas. After moving the whole thing to react hooks and functional approach, it worked. I don't know why, it seems like it was working on a copy of the canvas object.

However eventually I got stuck on accessing fabric's .on event listeners. The function canvas.on() is always undefined when I try to access it from the context.

@asturur
Copy link
Member

asturur commented Dec 1, 2019

probably the object was somehow serializing? can it be?

Can you post your work with hooks? did you use useRef or something different? this would be a valuable guide for other developers.

@maniac0s
Copy link
Author

maniac0s commented Dec 9, 2019

I didn't have yet the need for useRef hook.

I mostly use useState and useEffect. I put everything related to the canvas into a state, keep there also the objectList and all flags and settings.

The canvas is it's own component which calls a function from the context provider upon mount which initializes the fabric.Canvas

const FabricCanvas = (props) => {
    const context = useContext(FabricContext);

    useEffect(() => {
        context.initCanvas('c');
    }, []); //runs only once on mount

const ContextProvider = (props) => {
    const [canvas, setCanvas] = useState(false);

    const initCanvas = (c) => {
        setCanvas(new fabric.Canvas(c));
    }

To work with fabric events, I have an own useEffect hook that gets called everytime something changes on the canvas, thus I can set functions to handle different events

const [objectList, setObjectList] = useState(['none']);

useEffect(() => {
    if(!canvas) {
        return;
    }
    function handleEvent(e) {
        if(!e) {
            return;
        }
        setObjectList(canvas.getObjects());
    }
    handleEvent();
    canvas.on("object:added", handleEvent);
    canvas.on("object:removed", handleEvent);
    return () => {
        canvas.off("object:added");
    }
}, [canvas]);

Everything is put in a context provider function that provides the context to it's children in App.js:

const App = (props) => {
    return (
        <ContextProvider>
                    <DrawButton />
                    <TextButton />
                    <FabricCanvas />
                    <ObjectList />
        </ContextProvider>
    );
}

The use of hooks allows me to put the components where ever I want in the whole context (and maybe even in the future cascade multiple contexts) thus giving me the freedom to logically arrange the app's components as I need them which was impossible using class based way.

Keep in mind that this is a work in progress any many things are not working yet and solutions I found so far might not be the best way to do it and could thus change in the future.

@stale
Copy link

stale bot commented Jan 25, 2020

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale Issue marked as stale by the stale bot label Jan 25, 2020
@asturur asturur closed this as completed Jan 25, 2020
@murtyjones
Copy link

Here's my solution using hooks/context:

import React, { createContext, useState } from 'react';

// Here are the things that can live in the fabric context.
type FabContext = [
  // The canvas
  fabric.Canvas | null,
  // The setter for the canvas
  (c: fabric.Canvas) => void
];

// This is the context that components in need of canvas-access will use:
export const FabricContext = createContext<FabContext>([null, () => {}]);

/**
 * This context provider will be rendered as a wrapper component and will give the
 * canvas context to all of its children.
 */
export const FabricContextProvider = (props: {children: JSX.Element}): JSX.Element => {
  const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);

  const initCanvas = (c: fabric.Canvas): void => {
    setCanvas(c);
  };

  return (
    <FabricContext.Provider value={[canvas, initCanvas]}>
      {props.children}
    </FabricContext.Provider>
  );
};

Then, when the usage is needed:

const [canvas, initCanvas] = useContext(FabricContext);

// either use the canvas or set the canvas after initializing it using `new fabric.Canvas('my-id')`

@maniac0s
Copy link
Author

maniac0s commented May 5, 2020

export const FabricContext = createContext<FabContext>([null, () => {}]);

What's this for?

@murtyjones
Copy link

murtyjones commented May 5, 2020

export const FabricContext = createContext<FabContext>([null, () => {}]);

What's this for?

That creates the context that components can import and use to get/set the canvas. In my example there's also some typescript (<FabContext>) but if you're using plain old JS you can leave that out.

import React, { useContext } from 'react';
import { FabricContext } from './somewhere';

const MyComponent = () => {
    const [canvas, initCanvas] = useContext(FabricContext);
    useEffect(() => {
        const localCanvas = new fabric.Canvas('c');
        initCanvas(localCanvas);
    }, []);

  return (
    <canvas
      id='c'
      width={TEMPLATE_WIDTH}
      height={TEMPLATE_HEIGHT}
    />
  );
}

@asturur
Copy link
Member

asturur commented May 6, 2020

if you want to do it more reacty, instead of the ID you can pass a element ref. You can make a sort of custom hook useFabricCanvas(canvasRef). I still trying to get an hold on hooks before feeling like giving suggestions.

@maniac0s
Copy link
Author

maniac0s commented May 7, 2020

Yeah that's basically how I did it in my example. Just that I create the fabric canvas in an own component and give the created canvas back into the context by calling context.initCanvas(c) from the component which sets the whole canvas reference into state variable accessible by the context. I just didn't use a type, don't know what's that for.

@saninmersion
Copy link

@maniac0s How did you go about doing "stuff like getting the active object (to determine which toolbox I should open)" and getting default values for tools based on selected layers?

@maniac0s
Copy link
Author

maniac0s commented May 27, 2020

@maniac0s How did you go about doing "stuff like getting the active object (to determine which toolbox I should open)" and getting default values for tools based on selected layers?

Writing this from my head since I am currently on a different project for a few weeks: Again mostly hardcoded stuff (can be made more dynamically with react tho).
I save like everything on the context and thus there's also an activeObject variable. The context provider has a state variable "activeObject" and can update this on events which then gets promoted to the context. That always get's set when an object is clicked which I get with an onClick event on the canvas and calling the fabric function active = canvas.getActiveObject() which returns the currently activated Object, so I can use setActiveObjiect(active) to update the state variable.

A toolbox component then decides, according to what's in "activeObject", the properties of the actual toolbox to be displayed. I have made local functions for that and here you can endlessly cascade the components depending on what you need. For instance if activeObject === "textbox" I display fontlist, fontsizes, colorpicker, alignment ect inputfilelds and for that I have in the toolbox component a function that implements a <TextTools> component which has locally hardcoded a list of fonts, sizes ect and includes a <ColorPicker type="fill"> for the text fill color.
Toolbox in it's return only defines JSX when activeObject !== "none", so the toolbox component displays only when there's an object with a type selected.

@Robbie-Cook
Copy link
Contributor

Could we get some docs for React added to the website? I am keen to help for this.

Also keen to help add demos for React too.

@asturur
Copy link
Member

asturur commented Jul 18, 2020

If you have some idea to standardize a react approach, please do!

i use now creatRect to get the canvas reference and then i mount the fabricJS canvas on top of the element with a useEffect, the main problem to solve are:

  • make the canvas accessible in all the app ( context? )
  • make the same canvas accessible in utility functions that are possibly tirggered by events and are outside react domain
  • state! how do i tell another component to re-render because the active object change from red to blue?

any small tutorial is welcome!

The latest demos use codepen prefill embed, thos should allow for JS transpilation and react too.

@Robbie-Cook
Copy link
Contributor

Robbie-Cook commented Jul 19, 2020

Hey @asturur , thanks!

I made a small demo https://codesandbox.io/s/react-fabric-example-87hh4 ,

However it doesn't cover make the canvas accessible in all the app, but it is accessible in utility classes / functions when canvas is passed as a parameter. We just need to check whether it is null.

In terms of how we can tell another component to re-render because the active object change from red to blue, we can hook into the react component state for this, e.g. add a listener to the canvas that triggers the setState of another component.

The canvas is rendered after component render, and can be set to change when we update the color state (e.g. with useEffect(() => {...}, [props.color]), or never change (e.g. with useEffect(() => {...}, []),

Thanks for those points, I don't know the answer to

make the canvas accessible in all the app ( context? )`

yet

@saninmersion
Copy link

I did start pushing my code that I used to create and test a feature for my project that was created from the help of this thread a while back.
Here is the repo

@asturur
Copy link
Member

asturur commented Jul 19, 2020

The repo seems good to me. If it works good we should totally link it somewhere.

@Robbie-Cook
Copy link
Contributor

Great @asturur , I am super keen.

It might be good if other colloborators can make changes to the codesandbox, so that it's not just me. Maybe I could add you as a collaborator, or there could be a fabric.js codesandbox account?

@asturur
Copy link
Member

asturur commented Jul 19, 2020

Do codepen allow for the same?
if it has to be editable live as an example on the website we need to use codepen, otherwise you can clone/move the repo in the fabricjs org and get write access to it together with whoever wants to collaborate on it, or it can be there and someone will just open prs on it if they want to improve a part.

@Robbie-Cook
Copy link
Contributor

Cool, I'm sure it will. I will move the example to CodePen and send you the link!
Cheers!

@niels-van-den-broeck
Copy link
Contributor

@saninmersion I wouldn't recommend storing mutative objects like a canvas in state variables. For the rest that looks like a great intake.

@nathan-ch
Copy link

Hi, I tried @Robbie-Cook solution, it's pretty simple but i can't understand why, the state inside the context is not updated.

I can see that fabric events work well, it triggers the callback and set the new state but inside my consumers, the state doesn't change. It only changes when i move to another activeObject.

So for example i can't move and object and display the left and top values changing in real time.
Any help would be awesome

@GM1957
Copy link

GM1957 commented Mar 9, 2024

To use fabricJS with NextJS like framework, where you have to target the canvas using ref and edit with fabricjs capabilities you can follow the below work around.

  1. First make a custom Canvas component (as shown in nextjs example)
"use client";

import { fabric } from "fabric";
import React, { useCallback, useEffect, useRef } from "react";

export function useCanvas(
  ref?: React.ForwardedRef<HTMLCanvasElement>,
  init?: (canvas: fabric.Canvas) => any,
  saveState = false,
  deps: any[] = []
) {
  const elementRef = useRef<HTMLCanvasElement>(null);
  const fc = useRef<fabric.Canvas | null>(null);
  const data = useRef<any>(null);

  const setRef = useCallback(
    (el: HTMLCanvasElement | null) => {
      elementRef.current = el;
      ref && (ref.current = elementRef.current);

      // dispose canvas
      fc.current?.dispose();
      // set/clear ref
      if (!el) {
        fc.current = null;
        return;
      }
      const canvas = new fabric.Canvas(el);
      window.canvas = fc.current = canvas;
      // invoke callback
      init && init(canvas);
    },
    [saveState, ...deps]
  );
  useEffect(() => {
    // disposer
    return () => {

    // we avoid unwanted disposing by doing so only if element ref is unavailable
      if (!elementRef.current) {
        fc.current?.dispose();
        fc.current = null;
      }
    };
  }, [saveState]);
  return [fc, setRef] as [typeof fc, typeof setRef];
}

export const Canvas = React.forwardRef<
  HTMLCanvasElement,
  {
    onLoad?: (canvas: fabric.Canvas) => any;
    saveState?: boolean;
  }
>(({ onLoad, saveState }, ref) => {
  const [canvasRef, setCanvasElRef] = useCanvas(ref, onLoad, saveState);
  return <canvas ref={setCanvasElRef} />;
}); 
  1. Now we can call the Canvas component whenever needed, and in onLoad function we can init the fabricRef.
    Here fabricRef.current will be containing all the fabric methods which which we can now edit the canvas as shown in onAddClick function.
"use client";

import { useCallback, useRef } from "react";
import { fabric } from "fabric";
import { Canvas } from "@/components";

export function CanvasMaker() {
  const canvasRef: any = useRef(null);
  const fabricRef: any = useRef(null);

  const onCanvasLoad = useCallback(async (initFabricCanvas: fabric.Canvas) => {
    const text = new fabric.Textbox("fabric.js sandbox", {
      originX: "center",
      top: 0,
    });
    initFabricCanvas.add(text);
    initFabricCanvas.centerObjectH(text);

    fabricRef.current = initFabricCanvas;
  }, []);

  const onAddClick = () => {
    const rect = new fabric.Rect({
      height: 280,
      width: 200,
      fill: "yellow",
      selectable: true,
      hasControls: true,
    });
    fabricRef.current.add(rect);
  };

  return (
    <div>
      <Canvas onLoad={onCanvasLoad} ref={canvasRef} saveState />
      <button onClick={onAddClick}>Add React</button>
    </div>
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stale Issue marked as stale by the stale bot
Projects
None yet
Development

No branches or pull requests

9 participants