DEV Community

Cover image for Build a facebook story creator using ReactJS and FabricJS
Trung Hieu Nguyen
Trung Hieu Nguyen

Posted on

Build a facebook story creator using ReactJS and FabricJS

Back then those days, I'm on my way to building a very large project myself, it's a social media app. And there is a very interesting feature that I want my app to have, yeah, it's the story feature where people can share things that will be automatically hidden after 24 hours. I decided to build a simpler version of it and today I want to share with you guys the experience of building a Facebook story creator.

Live demo: https://trunghieu99tt.github.io/Facebook-story-mini/

1. Scope

First, let's define our scope. The story feature in the Facebook app on mobile is a very huge feature which a lot of smaller features in it, but the story feature on the Facebook website is not.

On the website, we only have 2 options. 1 is text story and 2 is image story with texts. In this blog, I will go for the story feature on the Facebook website which is much more simpler I think.

Okay, let's go a little bit further and see what we have to do

  • Text story: a paragraph in the middle and a changeable background
  • Image story: one image per story and we also can add text blocks

It seems to be simple, right? at least with the text story feature. Okay, let's move to the next part

2. Tools, libraries

I use ReactJS to develop this feature, with text story it's enough, but with image story, we need to find a library that helps us deal with add/remove text blocks, change direction, size,... and I came up with Fabric Fabric provides interactive object model on top of the canvas element, that's exactly what we want to do here. I think you'd better go to the Fabric website and read about it before continue reading.

3. Start coding

You can use whatever boilerplate you want, to me, I will stick with Create React App. I will assume that you guys have basic knowledge of React and know how to create and run a React App. Another small note is that in this project, I will use Typescript but I think people don't know about Typescript, it's not a big deal because it's just a small project.

In this project, we will need to add 2 more packages: fabric and fabricjs-react (actually we don't need this package, but to make things easier, it's okay to use).

Run this command:

yarn add fabric fabricjs-react
#or
npm install fabric fabricjs-react
Enter fullscreen mode Exit fullscreen mode

Okay, now we're good to go.

Before going to the next step, let's define our folder structure, we know that we have 2 main types of components: 1 is story form to create text or image story, 2 is viewer components where we show the data from the server after creating and save text/image story. I'll create a folder structure like this:

Untitled

constants folder will hold all the constant values we use in this app.

3.1. Text story

About text story, it's the easier one, we just have a div and text in the center of that div. we can also change the background of that div.

In StoryForm, create a folder called Text, in that folder, create 3 files: index.ts (our entry file), textStory.module.css, and TextStory.tsx.

Untitled

In TextStory.tsx:

import { ChangeEvent, useState } from "react";

import { BACKGROUND_LIST } from "../../../constants";

import classes from "./textStory.module.css";

const TextStory = () => {
    const [text, setText] = useState("");
    const [background, setBackground] = useState("#000");

    const onChangeText = (e: ChangeEvent<HTMLTextAreaElement>) => {
        const text = e.target.value;
        setText(text);
    };

    const saveToServer = () => {
        const data = {
            type: "text",
            background,
            text,
        };
        localStorage.setItem("data", JSON.stringify(data));
    };

    return (
        <div className={classes.root}>
            <aside className={classes.aside}>
                <textarea
                    className={classes.textarea}
                    onChange={onChangeText}
                    rows={7}
                />
                <p>Change color</p>
                <ul className={classes.backgroundList}>
                    {BACKGROUND_LIST.map((color) => {
                        return (
                            <li
                                onClick={() => setBackground(color)}
                                style={{
                                    background: color,
                                    cursor: "pointer",
                                    outline: `${
                                        color === background
                                            ? "2px solid blue"
                                            : ""
                                    } `,
                                }}
                            ></li>
                        );
                    })}
                </ul>
                <button onClick={saveToServer}>Save</button>
            </aside>
            <div
                className={classes.main}
                style={{
                    background: background,
                }}
            >
                <p className={classes.text}>{text}</p>
            </div>
        </div>
    );
};

export default TextStory;
Enter fullscreen mode Exit fullscreen mode

Above is the full code for that component. We have a state to store our text and a state to store the background color. About the saveToServer function, you can ignore it, we will go back to it later on this blog. With background color list, in this project, we will hardcode it (but you can change it to a color picker or whatever you want to make it better)

Create an index.ts file in the constants folder and put this to it:

export const BACKGROUND_LIST = [
    'linear-gradient(138deg, rgba(168,74,217,1) 0%, rgba(202,88,186,1) 55%, rgba(229,83,128,1) 100%)',
    'linear-gradient(138deg, rgba(55,31,68,1) 0%, rgba(115,88,202,1) 55%, rgba(97,0,30,1) 100%)',
    'linear-gradient(138deg, rgba(31,68,64,1) 0%, rgba(202,88,155,1) 55%, rgba(90,97,0,1) 100%)',
    'linear-gradient(138deg, rgba(14,33,240,1) 0%, rgba(88,202,197,1) 55%, rgba(11,97,38,1) 100%)',
    'radial-gradient(circle, rgba(238,174,202,1) 0%, rgba(148,187,233,1) 100%)',
    'linear-gradient(138deg, rgba(14,33,240,1) 0%, rgba(88,202,197,1) 55%, rgba(11,97,38,1) 100%)',
    'radial-gradient(circle, rgba(198,76,129,1) 12%, rgba(218,177,209,1) 27%, rgba(148,187,233,1) 100%',
    'linear-gradient(180deg, rgba(62,66,105,1) 0%, rgba(233,225,107,1) 55%, rgba(11,97,38,1) 100%)',
    'radial-gradient(circle, rgba(117,67,81,1) 2%, rgba(107,233,164,1) 37%, rgba(97,11,11,1) 100%)',
    '#2d88ff',
    '#ececec',
    '#6344ed',
    '#8bd9ff',
    'linear-gradient(315deg, rgba(255,184,0,1) 0%, rgba(237,68,77,0.7175245098039216) 61%, rgba(232,68,237,1) 78%)',
];
Enter fullscreen mode Exit fullscreen mode

About the style file, it's a little bit long so I won't post it here. But I'll drop a link at the end of this blog so you can check it out later.

In the index.ts file, we just write a single line.

export { default } from './TextStory';
Enter fullscreen mode Exit fullscreen mode

This is our final result of text story form:

Untitled

The default color of text will be white (I set it using CSS, but you make a list of available colors and let the user choose the color if you want).

3.2. Image story

Okay, this is the main part of this blog and it will be a tougher one.

Because we have to do these things:

  • Display image (in this project we will read it from the URL, but you can change it to upload from your machine)
  • Add texts: We can add multi-text blocks and with each block, we can change the text in there, drag, rotate, resize it.

It's time for the fabric to come into play.

In story form, create a folder called Image. Then in that folder, create a file called ImageStory.tsx.

let's write some code in there

import React, { ChangeEvent, useState } from "react";
import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react";

import classes from "./imageStory.module.css";

const ImageStory = () => {
    const { editor, onReady } = useFabricJSEditor()
    return (
        <div className={classes.root}>
            <div className={classes.main}>
                <FabricJSCanvas className={classes.canvas} onReady={onReady} />
            </div>
        </div>
    );
};

export default ImageStory;
Enter fullscreen mode Exit fullscreen mode

Now add a form to hold our image URL and a submit function for that form.

import React, { ChangeEvent, useState } from "react";
import { fabric } from "fabric";
import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react";

import classes from "./imageStory.module.css";

const ImageStory = () => {
    const [image, setImage] = useState<string | null>(null);
    const [isSubmitted, setIsSubmitted] = useState<boolean>(false);

    const { editor, onReady } = useFabricJSEditor();

    const submitImage = () => {
        if (image && image.startsWith("http")) {
            fabric.Image.fromURL(image, function (img) {
                const canvasWidth = editor?.canvas.getWidth();
                const canvasHeight = editor?.canvas.getHeight();
                editor?.canvas.setWidth(500);
                editor?.canvas.setHeight(500);
                editor?.canvas.add(img);
                const obj = editor?.canvas.getObjects();
                obj?.forEach((o) => {
                    if (o.type === "image") {
                        o.scaleToHeight(canvasWidth || 100);
                        o.scaleToHeight(canvasHeight || 100);
                    }
                });

                editor?.canvas.centerObject(img);
                setIsSubmitted(true);
            });
        }
    };

        const onChange = (e: ChangeEvent<HTMLInputElement>) => {
            const { value } = e.target;
            setImage(value);
        };

    return (
        <div className={classes.root}>
            <div className={classes.main}>
                {!isSubmitted && (
                    <div className={classes.imageForm}>
                        <input type="text" onChange={onChange} />
                        <button onClick={submitImage}>Submit</button>
                    </div>
                )}
                <FabricJSCanvas className={classes.canvas} onReady={onReady} />
            </div>
        </div>
    );
};

export default ImageStory;
Enter fullscreen mode Exit fullscreen mode

We have a state that stores our image URL

Because I want to show form only when we didn't submit the image, so I added isSubmitted state to deal with that. We only show image form if isSubbmitted = false.

Okay, let's take a look at the onSubmit function:

const submitImage = () => {
        if (image && image.startsWith("http")) {
            fabric.Image.fromURL(image, function (img) {
                                // Note that img now will be an fabric object

                                // get width and height of canvas container
                const canvasWidth = editor?.canvas.getWidth();
                const canvasHeight = editor?.canvas.getHeight();

                                // add image object 
                editor?.canvas.add(img);

                                // get all fabric objects in editor
                const obj = editor?.canvas.getObjects();

                                // This will not optimal way, but currently
                                // we only have one image, so It should be fine
                obj?.forEach((o) => {
                    if (o.type === "image") {
                                                // resize image to fit with editor width and height
                        o.scaleToHeight(canvasWidth || 100);
                        o.scaleToHeight(canvasHeight || 100);
                    }
                });

                editor?.canvas.centerObject(img);
                setIsSubmitted(true);
            });
        }
    };
Enter fullscreen mode Exit fullscreen mode

fabric supports read image from URL, it will return a fabric object then. in callback function, we add that object to current editor. One thing to keep in mind that the image now will keep its initial size so it might not fit with our editor area, we need to resize it to fit with editor area. My current solution is to get all objects in editor then resize it if it's image. Since we only have one image per story, this solution will work fine.

Now if you run your app and paste a valid image URL to form and hit submit, we will see it shows the image in the editor area. and you can interact with that image (drag, resize, rotate...). Good job. 😄

We finished our first goal, now let's move to the second one.

the fabric also supports text block, so adding text to our editor is easy.

Change our ImageStory component:

import React, { ChangeEvent, useState } from "react";
import { fabric } from "fabric";
import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react";

import classes from "./imageStory.module.css";

const ImageStory = () => {
    const [image, setImage] = useState<string | null>(null);
    const [isSubmitted, setIsSubmitted] = useState<boolean>(false);

    const { editor, onReady } = useFabricJSEditor();

    const onAddText = () => {
        try {
            editor?.canvas.add(
                new fabric.Textbox("Type something...", {
                    fill: "red",
                    fontSize: 20,
                    fontFamily: "Arial",
                    fontWeight: "bold",
                    textAlign: "center",
                    name: "my-text",
                })
            );
            editor?.canvas.renderAll();
        } catch (error) {
            console.log(error);
        }
    };

    const onChange = (e: ChangeEvent<HTMLInputElement>) => {
        const { value } = e.target;
        setImage(value);
    };

    const submitImage = () => {
        if (image && image.startsWith("http")) {
            fabric.Image.fromURL(image, function (img) {
                const canvasWidth = editor?.canvas.getWidth();
                const canvasHeight = editor?.canvas.getHeight();
                editor?.canvas.add(img);
                const obj = editor?.canvas.getObjects();
                obj?.forEach((o) => {
                    if (o.type === "image") {
                        o.scaleToHeight(canvasWidth || 100);
                        o.scaleToHeight(canvasHeight || 100);
                    }
                });

                editor?.canvas.centerObject(img);
                setIsSubmitted(true);
            });
        }
    };

    return (
        <div className={classes.root}>
            {isSubmitted && (
                <aside className={classes.aside}>
                    <button onClick={onAddText}>Add Text</button>
                    <button onClick={saveToServer}>Save</button>
                </aside>
            )}

            <div className={classes.main}>
                {!isSubmitted && (
                    <div className={classes.imageForm}>
                        <input type="text" onChange={onChange} />
                        <button onClick={submitImage}>Submit</button>
                    </div>
                )}
                <FabricJSCanvas className={classes.canvas} onReady={onReady} />
            </div>
        </div>
    );
};

export default ImageStory;
Enter fullscreen mode Exit fullscreen mode

Let's take a look at onAddText function. We create a new fabric Textbox object by calling new fabric.Textbox().

 editor?.canvas.add(
                new fabric.Textbox("Type something...", {
                    fill: "red",
                    fontSize: 20,
                    fontFamily: "Arial",
                    fontWeight: "bold",
                    textAlign: "center",
                    name: "my-text",
                })
            );
 editor?.canvas.renderAll();
Enter fullscreen mode Exit fullscreen mode

Let me explain the params we passed: The first argument will be the initial text and the second one will be an object that contains configuration for text in that textbox. In the above code, I'll create a text that contains a red bold text which has font-size is 20 and font-family is Arial, the text will be aligned center in the textbox. After creating the textbox, we will add it to our editor using editor.canvas.add(..), and finally, we re-render the editor to get the latest state.
This is our final result:

Untitled

Okay, up until now, we're done with adding the image and text. What's about deleting? With fabric, it's like a piece of cake, fabric has a removal method where we just need to pass objects we want to remove and fabric will handle it for us. But how do we get the object to pass to remove method?

Remember how we delete things, we will select it first, right? So fabric has a method called "getActiveObjects", by using that method, we can get all selected objects. Hah, problem solved, we just need to get all active objects, then loop through them and call remove method.

Like this:

const deleteSelected = () => {
        editor?.canvas.getActiveObjects().forEach((object) => {
            editor?.canvas.remove(object);
        });
    };
Enter fullscreen mode Exit fullscreen mode

Okay, so we're done with all the basic features. Now let's move to the next step.

3.3. Save and show data

We can add, move things so far, but our app is not just interaction things, we need to store it in our database and show data from the database right? So how could we do that with fabricjs?

In this small project, I will use local storage as our database to make it easier. About the form of data, I think text is the best way. We just need to create an object then use JSON.stringify with that object.

With the text story feature, we don't have many things to do. The information we need to store is text content and background color.

const saveToServer = () => {
        const data = {
            background,
            text,
        };
        localStorage.setItem("data", JSON.stringify(data));
    };
Enter fullscreen mode Exit fullscreen mode

Add this function to Text Story Form component and add a button which onClick event is saveToServer and we're done with it.

Now move to image story, again, thanks to fabric, we has a method called toJSON() which converts objects data in our editor to JSON, now we just need to call JSON.stringify with converted objects data and save it to local storage

const saveToServer = () => {
        const objects = editor?.canvas.toJSON();
        if (objects) {
            localStorage.setItem("data", JSON.stringify(objects));
        }
    };
Enter fullscreen mode Exit fullscreen mode

To show data, first, we get data from local storage and JSON.parse that data

const showResultFromServer = () => {
        const json = localStorage.getItem("data");
        if (json) {
            const objects = JSON.parse(json);
              // store it to component state. 
        }
    };
Enter fullscreen mode Exit fullscreen mode

With text story, after parsing data, we now have text content and background color. Using it to show data is easy, right? Our only concern is how to show image story because it was controlled by fabric. Luckily, fabric has a method called "loadFromJSON", we only need to pass JSON data we got from toJSON method and fabric will handle the rest for us.

For example, we can do this:

editor.canvas.loadFromJSON(
                data,
                () = {}
            );
Enter fullscreen mode Exit fullscreen mode

loadFromJSON has 2 params, the first is JSON data and the second is a callback function, the callback function will be called when JSON is parsed and corresponding objects (in this case, they're image objects and texts objects) are initialized. We don't need the callback function so let it be an empty function for now.

Okay, so we're all done with it.

The full source code can be found here:

https://github.com/trunghieu99tt/Facebook-story-mini

In this tutorial, I'm learning and writing this blog at the same time, so there might be better ways to use fabricjs or better ways of handling things I mentioned in this blog. :D If you have any suggestions, please feel free to drop a comment and I'll check it out. Thank you very much.

Discussion (1)

Collapse
trialfrial profile image
trialfrial

Great Blog!!!!

Get Great Canva Template for your brand rebrand.ly/canva_template_1_gumroad