Building a Drawing Board with Redwood

Milecia - Sep 1 '21 - - Dev Community

Sometimes you just want to be able to draw pictures to describe things and send them to others quickly. You don't want to use a whole service or deal with specific tools, you just want to hand draw a thing and send it. That's what we're going to make in this Redwood tutorial.

We'll make a drawing board app that saves drawings to a Postgres database so users can quickly download their drawings as images.

Setting up Postgres locally

Since we'll be working with a Postgres database, let's start by making a local instance we can connect to. You can download Postgres here for the OS you're working on.

While you're going through the initial set up, pay attention to the user name and password you create. You'll need those for the connection string in Redwood.

Setting up the Redwood app

Now go to a terminal and run:

yarn create redwood-app drawing-board
Enter fullscreen mode Exit fullscreen mode

This will create a new Redwood app with a bunch of directories and files. Feel free to check everything out, but we'll focus mainly on the api and web directories. One quick thing we need to handle is updating that database connection string.

Inside the .env file, uncomment the DATABASE_URL line and change it to match your local instance. That might look something like:

DATABASE_URL=postgres://postgres:admin@localhost:5432/drawing_board
Enter fullscreen mode Exit fullscreen mode

This will be referenced in the schema.prisma file we'll be updating, but as a sneak peek, here's where it's used. You don't have to change anything here.

datasource db {
  provider = "postgresql"
  url = env("DATABASE_URL")
}
Enter fullscreen mode Exit fullscreen mode

We'll need this in place so that the back-end of the app can communicate with the database.

Making the models

Let's write the model for the table that will hold the images. In api > db, open schema.prisma and the first thing we'll update is the provider value. This should be postgresql instead of sqlite.

Next, we'll remove the example user model from the file and replace it with our own. This model will represent the data we want to store for each canvas image we capture.

model Capture {
  id   Int    @id @default(autoincrement())
  name String
  url  String
}
Enter fullscreen mode Exit fullscreen mode

All we're doing is storing a name and URL string for the image. We'll be storing a data url for the image in Postgres, but this could also be a link to an online storage service like AWS or Cloudinary.

Run the migration

Since this is the only model we'll have for this app, it's safe to run the migration now:

yarn rw prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

You'll be prompted to give the migration a name, but everything else happens automatically. Now that the database is set up, we'll start working on the back-end.

Making the back-end

Redwood uses GraphQL to handle everything in the back-end. So we're going to need some types and resolvers to let the front-end make request and get the data to the database. We'll run this command to generate some types and resolvers for us based on the Capture model.

yarn rw g sdl capture
Enter fullscreen mode Exit fullscreen mode

Redwood creates new folders and files that have the GraphQL types in api > src > graphql and a query resolver in api > src > services for the Capture model. Now we just have to modify a few things.

Adding the create mutation for captures

We need to add new type to give us the ability to save captures from the drawing board to the database. Then we'll have to add the resolver that will actually add the data to the database.

First, we'll go to the captures.sdl.js file in the api > src > graphql directory. We're just going to add a new mutation type below the UpdateCaptureInput.

type Mutation {
  createCapture(input: CreateCaptureInput!): Capture!
}
Enter fullscreen mode Exit fullscreen mode

This will make the request accessible from the front-end. Next we'll go to the captures.js file and add the mutation that persists the data to the database right below the query.

export const createCapture = ({ input }) => {
  return db.capture.create({
    data: input,
  })
}
Enter fullscreen mode Exit fullscreen mode

We have a fully functioning GraphQL server right now! In fact, if you run the app with yarn rw dev and go to http://localhost:8911/graphql in your browser, you'll see the GraphQL explorer with the mutation we just added.

graphql explorer

Making the front-end

Since we have the back-end working, it's finally time to build the front-end. We'll have a canvas on the page that let's use capture the drawing with a button click. At the bottom of the page, we'll have small views of the existing captured drawings.

The drawing board

Let's start by making a page for the drawing board. We'll take advantage of another Redwood command for this.

yarn rw g page capture /
Enter fullscreen mode Exit fullscreen mode

This generates the page component, a Storybook story, and a Jest test for the component. It'll also automatically add the route for the new page to Routes.js. This page will point be at the base URL of the app, which is why we have the / int the page creation command above.

We also need to install the fabric package so that we can work with the canvas. In a terminal, go to the web directory and run:

yarn add fabric
Enter fullscreen mode Exit fullscreen mode

Make sure you go up one directory level in your terminal after this! That way you'll be running the commands in the right place.

Then we'll go to web > src > pages > CapturePage and edit the CapturePage.js file. You can delete everything inside of the CapturePage component because we won't be using any of it. Let's import a few things first.

import React, { useState, useEffect } from 'react'
import { fabric } from 'fabric'
Enter fullscreen mode Exit fullscreen mode

Next, we'll add a new state to the CapturePage component that will initialize the canvas object for us so that we can draw and save images.

const [canvas, setCanvas] = useState('')

useEffect(() => {
  setCanvas(initCanvas())
}, [])

const initCanvas = () =>
  new fabric.Canvas('capture', {
    height: 500,
    width: 500,
    backgroundColor: '#F6F6F6',
    isDrawingMode: true,
  })
Enter fullscreen mode Exit fullscreen mode

The initCanvas sets up a new instance of the fabric canvas object and it's targeting a canvas element with an id of capture. Then we've given it some dimensions to limit the size on the page. It has a background color so that users know that's it's a different element to interact with. Lastly, we have drawing mode enabled so users can draw on the canvas and we can save the drawing they make.

We use useEffect to initialize this canvas only when the page loads and we set the state so that we can access this canvas later.

We'll add the canvas element inside a fragment in the return statement.

return (
  <>
    <canvas id="capture" />
  </>
)
Enter fullscreen mode Exit fullscreen mode

If you run the app now with yarn rw dev, you'll see something like this in your browser.

initial drawing board

Saving drawings

Next we need to add that button to trigger the save action. We'll need to add another import so we can use our GraphQL create mutation.

import { useMutation } from '@redwoodjs/web'
Enter fullscreen mode Exit fullscreen mode

Then we'll need to write the definition of the GraphQL request we want to make. Below the import we just added, write the following:

const CREATE_CAPTURE_MUTATION = gql`
  mutation CreateCaptureMutation($input: CreateCaptureInput!) {
    createCapture(input: $input) {
      id
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

This defines the createCapture mutation we'll use to save the drawings. Now that we have the definition, we need to create the mutation request. Inside of the CapturePage component, just above the capture state, add:

const [createCapture] = useMutation(CREATE_CAPTURE_MUTATION)
Enter fullscreen mode Exit fullscreen mode

Using this hook gives us access to a function we can call for this mutation. Now we should define a method that gets called when the save button is clicked. Below the initCanvas call, add this:

const saveCapture = () => {
  const imageUrl = canvas.lowerCanvasEl.toDataURL()

  const input = {
    name: `Capture${Math.random().toString()}`,
    url: imageUrl,
  }

  createCapture({
    variables: { input },
  })
}
Enter fullscreen mode Exit fullscreen mode

If you take a look at the way Fabric handles canvases, there's an inner canvas that we want to get to we can save the drawing as an image. So once we have the right canvas, we get the data URL for the image and save it as part of the mutation input.

There are many better ways to make a random name for an image, but we're using a random number to give them unique names. Then we call the mutation with the input we just defined and it saves the drawing the database!

Displaying drawings

Just so you see that the drawings are actually there, scribble two or three things and save them. Now we'll add a query to get all of the images and show them in a flex element.

Let's start by adding a new hook to an existing import.

import { useMutation, useQuery } from '@redwoodjs/web'
Enter fullscreen mode Exit fullscreen mode

Then we'll add a query for all the captures right below the mutation.

const GET_CAPTURES = gql`
  query {
    captures {
      name
      url
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

This will return an array with all of the captures we have in the database. We'll make this definition into something we can use inside of the CapturePage component.

const { data } = useQuery(GET_CAPTURES)
Enter fullscreen mode Exit fullscreen mode

Lastly, we'll add an element that displays all of the returned captures. This can go right below the <canvas> element.

<div style={{ display: 'flex' }}>
  {data?.captures &&
    data.captures.map((capture) => (
      <img
        key={capture.name}
        style={{ padding: '24px', height: '100px', width: '100px' }}
        src={capture.url}
      />
    ))}
</div>
Enter fullscreen mode Exit fullscreen mode

It maps over all of the captures and displays them as small images on the page. So you'll see something similar to this.

saved drawings being displayed

That's it! Now you can tweak this to have a sleek interface or a fancier way of storing images.

Finished code

You can check out the front-end in this Code Sandbox or you can look at the full code in the drawing-board folder of this repo.

If you check out the Code Sandbox, you'll notice the code is a little different. This is because we can't run a Redwood mono-repo with this tool, so an example of how the front-end works is what you'll see in the Code Sandbox. To get the full code, it's better to take a look at the repo on GitHub.

Conclusion

Since a lot of the things we do are virtual, it helps to have ways to still interface with the web that feels more natural. Drawing boards are becoming more popular so hopefully, this is helpful!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player