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
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
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")
}
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
}
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
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
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!
}
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,
})
}
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.
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 /
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
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'
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,
})
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" />
</>
)
If you run the app now with yarn rw dev
, you'll see something like this in your browser.
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'
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
}
}
`
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)
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 },
})
}
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'
Then we'll add a query for all the captures right below the mutation.
const GET_CAPTURES = gql`
query {
captures {
name
url
}
}
`
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)
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>
It maps over all of the captures and displays them as small images on the page. So you'll see something similar to this.
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!