Adding Objects to Your Konva Stage via Drag ’n’ Drop in React
— JavaScript, Software Development — 4 min read
I’m currently in the midst of building an editor for Reciprocal.dev so that users can build their own interactive user journey maps and as part of building this functionality I needed a means of allowing users to create the steps of the user journey easily.
After conducting a couple of UX experiments I settled on a Drag ’n’ Drop approach with a fallback of having a click of the item adding a new step in the centre of the map.
When building a mapping tool the Drag ’n’ Drop approach allows users a lot more accuracy in the initial placement of the items they add to the map, meaning less clicks are needed for them to achieve their goal of building their user journey map.
The fallback allows users who don’t use a pointing device (such as users with accessibility needs) to still add steps to their map, but as there’s no means of defining a location with this method we have to make the assumption on where it should go initially.
Implementing Drag ’n’ Drop
The Konva documentation has many examples of Drag ’n’ Drop being used to populate the canvas so the approach is well documented. Essentially the process requires two elements; the element to drag and the element to receive the drop event.
Neither of these elements is within the Konva canvas but the element that receives the drop event will need to be the parent element of the Konva canvas in order for the positioning coords to match.
Implementing the Drag part
The job of the dragging element is to set information in the application state that will be used by the drop event listener to create the object that will be rendered on the canvas.
In order to set the application state, we first need to create a context that both the dragging element and the drop receiving element can access via the useContext
hook.
With the context set up, we can then add the drag event listeners to the dragging element to set the state. To do this we add the draggable
, onDragStart
and onDragEnd
props to the element we want to drag.
Please note in order to keep the code examples shorter I’ve implemented them using
useState
Inside the callbacks of onDragStart
and onDragEnd
is where we set the state values that the drop receiving element will use, onDragStart
is used to set up the values and onDragEnd
is used to clear them if the user decides to cancel out of the drop.
Implementing the Drop part
The job of the drop receiving element is to listen for events of the drag entering its boundary and to the user dropping the dragging element onto it.
When this drop event is triggered the callback will access the shared state, get the values set when the drag started and create the appropriate data needed to then render an object on the canvas.
There are two event listeners we need to set up to achieve this; onDragOver
which listens for when the drag enters the element’s boundaries and onDrop
which listens to the user releasing the drag when over the element.
If you’re after a basic Drag ’n’ Drop UX then you can build a very basic callback for onDragOver
that just calls preventDefault()
on the event, but if like my implementation you’re providing the user with a preview of the final item on the map then you can use this to update the position for the preview.
The callback for the onDrop
event is where you’re creating the object that will be rendered on top the stage, but you’ll need to do some calculations of the position of where the drop event happened based on the pointer’s position on the stage to ensure the correct position is used.
Giving the user a preview of the dropped item
The basic implementation of Drag ’n’ Drop described above doesn’t have the best user experience. When the user is dragging the element they see the element they are dragging, not the final object on the stage, this is fine when the element being dragged is of the same scale and appearance as the final object, but it can be jarring if not.
In order to improve the Drag ’n’ Drop experience we first need to do three things; hide the dragging element while being dragged, capture the current position of the pointer over the stage and, render a preview of the final object at that position.
Hiding the dragging element while dragging
We can re-use the onDragStart
and onDragEnd
hooks here to control the visibility of the element when the user drags it. I did this by adding and removing a class to the element in the respective callbacks and then adding a CSS rule that tells the browser to render the element off canvas.
Showing the preview on the stage
As mentioned previously the onDragOver
callback can be used to capture the position of the pointer while it’s inside the boundary of the drop receiving element and update values in a state that can then be used for the position of the preview.
For the preview itself I added a new layer to the Konva Stage
that listens to changes in the preview position values in state and renders an object on that layer if they are set.
In order to clean up properly if the user cancels the drag or drops outside of the drop receiving element we need to be able to set this position value to null
so we don’t end up with objects in the preview layer when the user isn’t dragging anything.
Supporting non-pointer users
Not every user will be using a pointer so we need a means to allow them to add items to the stage without dragging them in. This is an easy problem to solve as we can simply add an onClick
event listener to the dragging element that will just add the object to the app state in the centre of the stage.
Demo
You can view a demo of the Drag ’n’ Drop implementation below along with the source code.
Summary
By giving users a means to drag elements onto the stage we allow them to accurately add items at the positions they want. For an application like Reciprocal.dev that I’m building, which relies on a user building a map of items, this makes for a far better UX.