jscrambler logo
open side menu
August 18, 2021
Build a Simple Game in Vanilla JS With the Drag and Drop API
By Ajdin Imsirovic | 16 min read
jscrambler-blog-building-game-with-drag-drop-api

The JavaScript language lives in a browser. Actually, let's rephrase that: a web browser has a separate part inside of it, called the JavaScript engine. This engine can understand and run JS code.

There are many other separate parts that, altogether, make up the browser. These parts are different Browser APIs, also known as Web APIs. The JS engine is there to facilitate the execution of the JS code we write. Writing JS code is a way for us (developers) to access various functionalities that exist in the browser and that are exposed to us via Browser APIs.

What Is a Web API?
Web APIs are known as "browser features". Arguably, the most popular of these various browser features⁠—at least for JavaScript developers⁠—is the browser console. The Console API allows us to log out the values of variables in our JavaScript code. Thus, we can manipulate values in our JS code and log out these values to verify that a specific variable holds a specific (expected) value at a certain point in the thread of execution, which is great for debugging. If you've spent any significant amount of time with the JS language, this should all be pretty familiar.

What some beginner JavaScript developers do not comprehend is the big picture of the browser having a large number of these "browser features" built-in⁠—and accessible to us via various JavaScript “facade” methods: methods that look like they’re just a part of the language itself, but are actually “facades” for features outside of the JS language itself. Some examples of widely used Web APIs are the DOM API, the Canvas API, the Fetch API, etc.

The JavaScript language is set up in such a way that we can't immediately infer that the functionality we're using is in fact a browser feature. For example, when we say:

let heading = document.getElementById('main-heading');
... we're actually hooking into a browser feature⁠—but there's no way of knowing this since it looks like regular JS code.

The Drag and Drop Web API
To understand how the Drag and Drop API works, and to effectively use it, all we have to do is know some basic concepts and methods it needs. Similar to how most front-end developers are familiar with the example from the previous section (namely, document.getElementById), we need to learn:

the basic concepts of the Drag and Drop Web API;
at least a few basic methods and commands.
The first important concept related to the Drag and Drop API is the concept of the source and target elements.

Source and Target Elements
There are built-in browser behaviors that determine how certain elements will behave when a user clicks and drags them on the viewport. For example, if we try click-dragging the intro image of this very tutorial, we'll see a behavior it triggers: the image will be displayed as a semi-transparent thumbnail, on the side of our mouse pointer, following the mouse pointer for as long as we hold the click. The mouse pointer also changes to the following style:

cursor: grabbing;
We've just shown an example of an element becoming a source element for a drag-and-drop operation. The target of such an operation is known as the target element.

Before we cover an actual drag and drop operation, let's have a quick revision of events in JS.

Events in JS: A Quick Revision
We could go as far as saying that events are the foundation on which all our JavaScript code rests. As soon as we need to do something interactive on a web page, events come into play.

In our code, we listen for: mouse clicks, mouse hovers (mouseover events), scroll events, keystroke events, document loaded events...

We also write event handlers that take care of executing some JavaScript code to handle these events.

We say that we listen for events firing and that we write event handlers for the events being fired.

Describing a Drag and Drop Operation, Step By Step
The HTML and CSS
Let's now go through a minimal drag and drop operation. We'll describe the theory and concepts behind this operation as we go through each step.

The example is as easy as can be: there are two elements on a page. They are styled as boxes. The first one is a little box and the second one is a big box.

To make things even easier to comprehend, let's "label" the first box as "source", and the second one as "target":

<div id="source">Source</div>
<div id="target">Target</div>
<style>
    #source {
        background: wheat;
        width: 100px;
        padding: 20px;
        text-align: center;
    }

#target {
    background: #abcdef;
    width: 360px;
    height: 180px;
    padding: 20px 40px;
    text-align: center;
    margin-top: 50px;
    box-sizing: border-box;
}
</style>
A little CSS caveat above: to avoid the added border width increasing the width of the whole target div, we’ve added the CSS property-value pair of box-sizing: border-box to the #target CSS declaration. Thus, the target element has consistent width, regardless of whether our drag event handlers are running or not.

The output of this code is fairly straightforward:

jscrambler-blog-build-a-game-in-js-1
Convert the “Plain” HTML Element Into a Drag and Drop Source Element
To do this, we use the draggable attribute, like so:

<div id="source" draggable="true">Source</div>
What this little addition does is changing the behavior of the element. Before we added the draggable attribute, if a user click-dragged on the Source div, they'd likely just highlight the text of the div (i.e the word "Source")⁠—as if they were planning to select the text before copying it.

However, with the addition of the draggable attribute, the element changes its behavior and behaves exactly like a regular HTML img element — we even get that little grabbed cursor⁠—giving an additional signal that we've triggered the drag-and-drop functionality.

Capture Drag and Drop Events
There are 8 relevant events in this API:

drag
dragstart
dragend
dragover
dragenter
dragleave
drop
dragend
During a drag and drop operation, a number of the above events can be triggered: maybe even all of them. However, we still need to write the code to react to these events, using event handlers, as we will see next.

Handling the Dragstart and Dragend Events
We can begin writing our code easily. To specify which event we’re handling, we’ll just add an on prefix.

For example, in our HTML code snippet above, we’ve turned a “regular” HTML element into a source element for a drag-and-drop operation. Let’s now handle the dragstart event, which fires as soon as a user has started dragging the source element:

let sourceElem = document.getElementById('source');
sourceElem.addEventListener('dragstart', function (event) {
    confirm('Are you sure you want to move this element?');
})
All right, so we’re reacting to a dragstart event, i.e. we’re handling the dragstart event.

Now that we know we can handle the event, let’s react to the event firing by changing the styles of the source element and the target element.

let sourceElem = document.getElementById('source');
let targetElem = document.getElementById('target');
sourceElem.addEventListener('dragstart', function (event) {
    event.currentTarget.style = "opacity:0.3";
    targetElem.style = "border: 10px dashed gray;";
})
Now, we’re handling the dragstart event by making the source element see-through, and the target element gets a big dashed gray border so that the user can more easily see what we want them to do.

It’s time to undo the style changes when the dragend event fires (i.e. when the user releases the hold on the left mouse button):

sourceElem.addEventListener('dragend', function (event) {
    sourceElem.style = "opacity: 1";
    targetElem.style = "border: none";
})
Here, we’ve used a slightly different syntax to show that there are alternative ways of updating the styles on both the source and the target elements. For the purposes of this tutorial, it doesn’t really matter what kind of syntax we choose to use.

Handling the Dragover and Drop Events
It’s time to deal with the dragover event. This event is fired from the target element.

targetElem.addEventListener('dragover', function (event) {
    event.preventDefault();
});
All we’re doing in the above function is preventing the default behavior (which is opening as a link for specific elements). In a nutshell, we’re setting the stage for being able to perform some operation once the drop event is triggered.

Here’s our drop event handler:

targetElem.addEventListener('drop', function (event) {
    console.log('DROP!');
})
Currently, we’re only logging out the string DROP! to the console. This is good enough since it’s proof that we’re going in the right direction.

Sidenote: notice how some events are emitted from the source element, and some other events are emitted from the target element. Specifically, in our example, the sourceElem element emits the dragstart and the dragend events, and the targetElem emits the dragover and drop events.
Next, we’ll use the dataTransfer object to move the source element onto the target element.

Utilize The dataTransfer Object
The dataTransfer object “lives” in an instance of the Event object⁠—which is built-in to any event. We don’t have to “build” the event object⁠—we can simply pass it to the anonymous event handler function⁠—since functions are “first-class citizens” in JS (meaning: we can pass them around like any other value)⁠—this allows us to pass anonymous functions to event handlers, such as the example we just saw in the previous section.

In that piece of code, the second argument we passed to the addEventListener() method is the following anonymous function:

function(event) {
  console.log('DROP!');
}
The event argument is a built-in object, an instance of the Event object. This event argument comes with a number of properties and methods, including the dataTransfer property⁠, which itself is an object.

In other words, we have the following situation (warning: pseudo-code ahead!):

event: {
…,
dataTransfer: {…},
stopPropagation: function(){…},
preventDefault: function(){…},
…,
…,
}
The important thing to conclude from the above structure is that the event object is just a JS object holding other values, including nested objects and methods. The dataTransfer object is just one such nested object, which comes with its own set of properties/methods.

In our case, we’re interested in the setData() and getData() methods on the dataTransfer object.

The setData() and getData() Methods on The dataTransfer Object
To be able to successfully “copy” the source element onto the target element, we have to perform a few steps:

We need to hook into an event handler for an appropriate drag-and-drop related event, as it emits from the source object;
Once we’re hooked into that specific event (i.e. once we’ve completed the step above), we’ll need to use the event.dataTransfer.setData() method to pass in the relevant HTML⁠—which is, of course, the source element;
We’ll then hook into an event handler for another drag-and-drop event⁠—this time, an event that’s emitting from the target object;
Once we’re hooked into the event handler in the previous step, we’ll need to get the data from step two, using the following method: event.dataTransfer.getData().
That’s all there is to it! To reiterate, we’ll:

Hook into the dragstart event and use the event.dataTransfer.setData() to pass in the source element to the dataTransfer object;
Hook into the drop event and use the event.dataTransfer.getData() to get the source element’s data from the dataTransfer object.
So, let’s hook into the dragstart event handler and get the source element’s data:

sourceElem.addEventListener('dragstart', function(event) {
event.currentTarget.style="opacity:0.3";
targetElem.style = "border: 10px dashed gray;";
event.dataTransfer.setData('text', event.target.id);
})
Next, let’s pass this data on to the drop event handler:

targetElem.addEventListener('drop', function(event) {
console.log('DROP!');
event.target.appendChild(document.getElementById(event.dataTransfer.getData('text')));
})
We could rewrite this as:

targetElem.addEventListener('drop', function(event) {
console.log('DROP!');
const sourceElemData = event.dataTransfer.getData('text');
const sourceElemId = document.getElementById(sourceElemData);
event.target.appendChild(sourceElemId);
})
Regardless of how we decide to do this, we’ve completed a simple drag-and-drop operation, from start to finish.

Next, let’s use the Drag and Drop API to build a game.

Write a Simple Game Using the Drag and Drop API
In this section, we’ll build a very, very, simple game. We’ll have a row of spans with jumbled lyrics to a famous song.

The user can now drag and drop the words from the first row onto the empty slots in the second row, as shown below.

jscrambler-blog-build-a-game-in-js-2
The goal of the game is to place the lyrics in the correct order.

Let’s begin by adding some HTML structure and CSS styles to our game.

Adding The game’s HTML and CSS
<h1>Famous lyrics game: Abba</h1>
<h2>Instruction: Drag the lyrics in the right order.</h2>
<div id="jumbledWordsWrapper">
  <span id="again" data-source-id="again" draggable="true">again</span>
  <span id="go" data-source-id="go" draggable="true">go</span>
  <span id="I" data-source-id="I" draggable="true">I</span>
  <span id="here" data-source-id="here" draggable="true">here</span>
  <span id="mia" data-source-id="mia" draggable="true">mia</span>
  <span id="Mamma" data-source-id="Mamma" draggable="true">Mamma</span
</div>
<div id="orderedWordsWrapper">
  <span data-target-id="Mamma"></span>
  <span data-target-id="mia"></span>
  <span data-target-id="here"></span>
  <span data-target-id="I"></span>
  <span data-target-id="go"></span>
  <span data-target-id="again"></span>
</div>
The structure is straightforward. We have the static h1 and h2 tags. Then, we have the two divs:

jumbledWordsWrapper, and
orderedWordsWrapper
Each of these wrappers holds a number of span tags: one span tag for each word. The span tags in the orderedWordsWrapper don’t have any text inside, they’re empty.

We’ll use CSS to style our game, as follows:

body {
  padding: 40px;
}
h2 {
  margin-bottom: 50px;
}
#jumbledWordsWrapper span {
  background: wheat;
  box-sizing: border-box;
  display: inline-block;
  width: 100px;
  height: 50px;
  padding: 15px 25px;
  margin: 0 10px;
  text-align: center;
  border-radius: 5px;
  cursor: pointer;  
}
#orderedWordsWrapper span {
  background: #abcdef;
  box-sizing: border-box;
  text-align: center;
  margin-top: 50px;
}