How To Upload Images in Sapper With Drag and Drop

Ashutosh K Singh

In this Media Jam, we will discuss how to build an Image Gallery in Sapper with Cloudinary and upload images to Cloudinary with drag and drop operation.

If you want to jump right into the code, check out the GitHub Repo here.

Sandbox

Before we get started, I wanted to point out that you can play around with the code described in this Jam on CodeSandbox, and you can find the live version here.

How To Setup and Install a Sapper Project

In your project's root directory, run the following commands in the terminal to quickly set up the initial Sapper project using the sapper-template.

1npx degit "sveltejs/sapper-template#webpack" sapper-cloudinary-example
2cd sapper-cloudinary-example
3npm install
4npm run dev

The last command will start the Sapper development server on http://localhost:3000/.

Head over to your Cloudinary dashboard and copy the API keys.

Create a new file named .env in your project's root directory and paste the API keys in it, as shown below.

1CLOUDINARY_CLOUD=''
2CLOUDINARY_KEY= ''
3CLOUDINARY_SECRET=''

To use these environment variables, you will also need to install the dotenv package.

Run the following command to install dotenv and cloudinary libraries where cloudinary is the Node.js/server-side SDK you will use to upload the images to Cloudinary.

1npm i dotenv cloudinary

How To Display Images on the App

The first step is to build the image gallery where you will display all the uploaded images. Create a new file named index.json.js inside the src directory, where you will create the server route to fetch the images from Cloudinary.

Server routes in Sapper are used to create JSON API, which exports functions, having HTTP request and response objects as arguments, corresponding to HTTP methods like GET, POST, etc.

You can create both the GET method to fetch the images and the POST method to upload the image inside the same index.json.js file, and Sapper will differentiate between them based on the type of request made.

Run the following command to create the index.json.js file.

1touch src/routes/index.json.js

Add the following code to index.json.js.

1require("dotenv").config();
2const cloudinary = require("cloudinary").v2;
3
4cloudinary.config({
5 cloud_name: process.env.CLOUDINARY_CLOUD,
6 api_key: process.env.CLOUDINARY_KEY,
7 api_secret: process.env.CLOUDINARY_SECRET,
8});
9
10export function get(req, res) {
11 res.writeHead(200, {
12 "Content-Type": "application/json",
13 });
14
15 let secureUrls;
16
17 cloudinary.api.resources({type: 'upload'}, function (error, result) {
18 secureUrls = JSON.stringify(
19 result.resources.map((pic) => {
20 return { secureUrl: pic.secure_url };
21 })
22 );
23
24 res.end(secureUrls);
25 });
26}

In the above code, you first access the environment variables using require("dotenv").config(); and pass them to cloudinary. Then, you create and export the get function where the images are fetched from Cloudinary using Cloudinary's Admin API.

The image URLs are sent as a JSON response to the GET request. These URLs are then used within the src attribute of the img tag to display the images on the app.

Update the src/routes/index.svelte file like this.

1<script context="module">
2 export function preload() {
3 return this.fetch(`/index.json`)
4 .then((r) => r.json())
5 .then((images) => {
6 return { images };
7 });
8 }
9 </script>
10
11 <script>
12 export let images;
13 </script>
14
15 <svelte:head>
16 <title>Sapper Cloudinary Example</title>
17 </svelte:head>
18 <h2>Sapper Image Gallery</h2>
19
20 <div class="container">
21 {#each images as image (image.secureUrl)}
22 <section class="image">
23 <img src={image.secureUrl} alt={image.secureUrl} width="400px" />
24 </section>
25 {/each}
26 </div>
27
28 <style>
29 h2 {
30 font-size: 2.4em;
31 text-transform: uppercase;
32 font-weight: 400;
33 margin: 0 0 0.5em 0;
34 text-align: center;
35 }
36
37.container {
38 column-count: 3;
39 column-gap: 20px;
40 }
41 section > img {
42 flex: 100%;
43 max-width: 100%;
44 margin-top: 1rem;
45 border-radius: 10px;
46 }
47 .image {
48 margin-bottom: 1rem;
49 display: flex;
50 }
51
52 @media only screen and (max-width: 600px) {
53 .container {
54 column-count: 1;
55 }
56 }
57 </style>

You use Sapper's preload() function to fetch the images using this.fetch() method. This preload() function runs before the component is created and loads the data, i.e., images required by the gallery before the page is loaded.

1<script context="module">
2 export function preload() {
3 return this.fetch(`/index.json`)
4 .then((r) => r.json())
5 .then((images) => {
6 return { images };
7 });
8 }
9</script>

To display the images, you iterate over the images array, exported from the script tag, using the each block. You can read more about each block here.

1{#each images as image (image.secureUrl)}
2<section class="image">
3 <img src="{image.secureUrl}" alt="{image.secureUrl}" width="400px" />
4</section>
5{/each}

Here is how this Sapper Image Gallery looks like.

How To Upload Images to Cloudinary

In this section, we will write the code to upload the image to Cloudinary and then show it in the Image Gallery created in the last section.

Update the script tag in index.svelte file like this.

1<script>
2 export let images;
3 let preview, fileinput;
4
5 const onFileSelected = (e) => {
6 let image = e.files[0];
7 let reader = new FileReader();
8 reader.readAsDataURL(image);
9 reader.onload = (e) => {
10 preview = e.target.result;
11 };
12 };
13
14 const uploadImage = () => {
15 uploadImageToCloudinary(preview);
16 };
17
18 const uploadImageToCloudinary = async (imageDataUrl) => {
19 const res = await fetch("/index.json", {
20 method: "POST",
21 headers: {
22 "Content-Type": "application/json",
23 },
24 body: JSON.stringify({ imageDataUrl }),
25 });
26 const json = await res.json();
27 if (json.imageUrl) {
28 window.location.reload();
29 }
30 };
31</script>

Before uploading the image to Cloudinary, the web application needs to read the selected picture; this is done with the help of FileReader Web API.

The files object containing the selected file, obtained either from the FileList object when the user selects a file using the <input> element or drag and drop operations' DataTransfer object is passed to the FileReader Web API. In this project, you will use both the <input> element and drag and drop operation.

The FileReader Web API reads the user's selected file and then converts it to a data: URL using the .readAsDataURL() method. You can read more about this method here.

Using the .onload() method of FileReader, triggered each time the reading operation is successfully completed, the data: URL is stored inside the preview variable.

You have created another function named uploadImageToCloudinary () which takes the data URL as an argument and makes a POST request to /index.json using the fetch API. The data URL is sent to the server route as a JSON object.

Once the image has been uploaded, and a successful response is returned from the POST request, the entire page is reloaded to trigger the Sapper's preload() function to update the Image Gallery with the newly uploaded image.

Add the code for the Upload component before the code for Image Gallery in the index.svelte file.

1<div class="app">
2 <h2>Upload Image</h2>
3 <div
4 class="dropzone"
5 on:click={() => {
6 fileinput.click();
7 }}
8 on:drop={(e) => {
9 e.preventDefault();
10 onFileSelected(e.dataTransfer);
11 }}
12 on:dragenter={(e) => {
13 e.preventDefault();
14 }}
15 on:dragleave={(e) => {
16 e.preventDefault();
17 }}
18 on:dragover={(e) => {
19 e.preventDefault();
20 }}
21 >
22 {#if preview}
23 <img class="preview" src={preview} alt="preview" />
24 {/if}
25
26 <img
27 class="upload-icon"
28 src="https://static.thenounproject.com/png/625182-200.png"
29 alt=""
30 on:click={() => {
31 fileinput.click();
32 }}
33 />
34 <input
35 style="display:none"
36 type="file"
37 accept=".jpg, .jpeg, .png"
38 on:change={(e) => onFileSelected(e.target)}
39 bind:this={fileinput}
40 />
41 {#if !preview}
42 <div
43 class="chan"
44 on:click={() => {
45 fileinput.click();
46 }}
47 >
48 Drag N Drop Images or Click on Upload.
49 </div>
50 {/if}
51 </div>
52 {#if preview}
53 <button
54 on:click={() => {
55 uploadImage();
56 }}>Upload Image</button
57 >
58 {/if}
59</div>

You can refer to the code to style the Upload component here.

In the above code, you create a div element with class="app". In this div element, you add the event handlers required for the drag and drop operation. The on:dragenter, on:dragover, and on:dragover event just the page from reloading. The on:drop() event, triggered when the file is dropped on the div, passes the DataTransfer object to the onSelectedFile() function.

You bind the fileinput variable to the input element to get a reference to the input element. This input element only accepts .jpg, .jpeg, .png image types. You can change the accepted image types according to your application needs.

When the upload icon is clicked, the on:click() event is triggered, which runs the fileinput.click() function and the user is prompted to select the image.

After the user has selected the image, the on:change() event of the input element is triggered, which passes the FileList object to the onSelectedFile() function.

Whether an image is selected or not, either the Upload button is displayed or the text Drag N Drop Images or Click on Upload. is shown using the if block and preview variable.

Here is how the Upload Component looks when no file is selected.

Here is how the Upload Component changes if an image is selected.

When the upload button is clicked, the uploadImage() function sends the POST request to the /index.json server route with the data URL of the image in the request body, and the image is uploaded to Cloudinary.

In the above code, you are sending the image data URL as a JSON request body. To parse this JSON request body in the POST request, you will need to install the body-parser package. You can read more about this library here.

Run the following command in your terminal to install body-parser.

1npm install body-parser

You will also need to update the src/server.js file to include the body-parser library.

1import sirv from "sirv";
2import polka from "polka";
3import compression from "compression";
4import * as sapper from "@sapper/server";
5const bodyParser = require("body-parser");
6
7const { PORT, NODE_ENV } = process.env;
8const dev = NODE_ENV === "development";
9
10polka() // You can also use Express
11 .use(
12 bodyParser.json({
13 limit: "50mb",
14 extended: true,
15 }),
16 bodyParser.urlencoded({
17 limit: "50mb",
18 extended: true,
19 }),
20
21 compression({ threshold: 0 }),
22 sirv("static", { dev }),
23 sapper.middleware()
24 )
25 .listen(PORT, (err) => {
26 if (err) console.log("error", err);
27 });

The last step is to create the POST method in the index.json.js file or the /index.json server route.

Add the following code for the POST method in the index.json.js file.

1export function post(req, res) {
2 const image = req.body.imageDataUrl;
3
4 res.writeHead(200, {
5 "Content-Type": "application/json",
6 });
7
8 cloudinary.uploader.upload(image).then((response) => {
9 res.end(JSON.stringify({ imageUrl: response.secure_url }));
10 });
11}

The above code uses Cloudinary's Upload API to upload the image to Cloudinary. After a successful response, the Cloudinary URL of the image is returned as a JSON response.

You can also pass additional and optional parameters to the Upload API like the upload_preset, signature, folder, etc.

Conclusion

In this Media Jam, we discussed how to build an Image Gallery in Sapper with images being fetched from Cloudinary. We also saw how to upload images to Cloudinary using both the <input> element and drag and drop operation.

Here are some additional resources that can be helpful:

Happy coding!

Ashutosh K Singh

JavaScript Developer

I'm a JavaScript Developer & Technical Writer. I develop awesome stuff with JavaScript and love to write about them.