February 24, 2022
  • All
  • Stencil
  • Tutorials
  • Design Systems
  • stencil
  • web components

Building with Stencil: Drag-and-Drop Components

Anthony Giuliano

Drag-and-drop functionality is growing more and more popular on the web, and with good reason. Drag-and-drop features are both intuitive and provide a really enjoyable user experience. In this tutorial, we’re going to take a look at how you can build your very own drag-and-drop components using Stencil. For this tutorial, we’re going to be building a Kanban board, but the principles can be used to build any kind of drag-and-drop experience. Here’s what our final product will look like.

You can find all of the code for this tutorial on the Stencil Sortable Github repo here. If you prefer to watch a video tutorial, I recently created a video where I walk through this entire process. Check it out!

Sortable JS

Building a drag-and-drop experience is notoriously difficult; so, to help us out, we’re going to be using a JavaScript library called Sortable JS. Sortable is going to help simplify our drag-and-drop logic.

NOTE: This tutorial assumes that you have a fundamental understanding of building components in Stencil. This tutorial will focus on the aspects related to implementing the drag-and-drop functionality.

Getting Started

As always, I want to start with an idea of what our component is going to look like in an html file. In this tutorial, we’re going to build two elements. We’ll have a drag-and-drop-container with a container-title and then, as children of that container, we’ll have some draggable-items. The draggable-item component can really be anything we want, but for this tutorial I’m going to make it a task on a kanban board. To do that, I’m going to provide a task-title, due-date, and assignee-avatar. This scaffold will now serve as our guide as we build out these components.

<drag-and-drop-container container-title="Backlog">
  <draggable-item task-title="Edit tutorial video" due-date="12/25/21" assignee-avatar="https://avatars.dicebear.com/api/micah/example0.svg"></draggable-item>
  <draggable-item task-title="Publish video on Youtube" due-date="12/25/21" assignee-avatar="https://avatars.dicebear.com/api/micah/example1.svg"></draggable-item>
</drag-and-drop-container>

Creating the Draggable Item

With that in mind, let’s go ahead and create our draggable-item component. This component is going to be largely presentational. We’ll take in our properties as strings, and then render them in an organized way. Let’s take a look at it.

@Component({
  tag: 'draggable-item',
  styleUrl: 'draggable-item.css',
  shadow: true,
})
export class DraggableItem {
  @Prop() taskTitle: string;
  @Prop() dueDate: string;
  @Prop() assigneeAvatar: string;

  render() {
    return (
      <Host>
        <h1>{this.taskTitle}</h1>
        <div class="bottom-row">
          <ion-icon size="32" name="calendar-outline"></ion-icon>
          <p>{this.dueDate}</p>
          <img src={this.assigneeAvatar} />
        </div>
      </Host>
    );
  }
}

Let’s add some CSS as well in our draggable-item.css file.

:host {
  display: block;
  min-width: 300px;
  padding: 8px;
  border-radius: 8px;
  margin-bottom: 8px;
  color: #03060b;
  background: white;
}

:host(:hover) {
  cursor: pointer;
  outline: 1px solid #03060b;
}

h1 {
  font-size: 1.375rem;
  margin: 0;
}

p {
  font-size: 1rem;
  margin: 0;
}

.bottom-row {
  display: flex;
  align-items: flex-end;
  gap: 8px;
}

ion-icon {
  font-size: 1rem;
}

img {
  height: 32px;
  width: 32px;
  padding: 2px;
  border-radius: 50%;
  border: 1px solid black;
  margin-left: auto;
}

Our draggable-item is composed of only markup and styles because all of our drag-and-drop logic is going to go in our drag-and-drop-container component. By putting all of our business logic there, we give ourselves a very flexible setup where any component can become a draggable item by just being placed within a drag-and-drop-container.

Creating the Drag-and-Drop Container

Let’s go ahead and start building our drag-and-drop-container by creating a new Stencil component. First we’ll take in a Prop for our containerTitle and display it in our render method.

@Component({
  tag: 'drag-and-drop-container',
  styleUrl: 'drag-and-drop-container.css',
  shadow: true,
})
export class DragAndDropContainer {
  @Prop() containerTitle: string;

  render() {
    return (
      <Host>
        <h1>{this.containerTitle}</h1>
      </Host>
    );
  }
}

Next we’ll add some styles in our drag-and-drop-container.css file.

:host {
  display: flex;
  flex-direction: column;
  color: #03060b;
  background: rgb(230, 230, 230);
  padding: 8px;
  border-radius: 8px;
}

h1 {
  margin: 0;
  margin-bottom: 16px;
}

Turning back to our component, we can now start working towards our drag-and-drop functionality. The first thing we’ll want to do is create an area within our container where elements can be dragged to and from. I’m going to call this the “draggable area”. We can do that by adding a div to our component that will serve as this area. We’ll put our slot element within this div so all of the container’s children will be included in the draggable area.

render() {
  return (
    <Host>
      <h1>{this.containerTitle}</h1>
      <div>
        <slot></slot>
      </div>
    </Host>
  );
}

Using Sortable

Next we’ll use Sortable to add the drag-and-drop functionality to our draggable area. To do that, Sortable is going to need a reference to the draggable area. To create a reference, we’ll create a private class member called container and assign it to our div with the ref attribute.

export class DragAndDropContainer {
  @Prop() containerTitle: string;

  private container: HTMLElement;

  render() {
    return (
      <Host>
        <h1>{this.containerTitle}</h1>
        <div ref={el => (this.container = el as HTMLElement)}>
          <slot></slot>
        </div>
      </Host>
    );
  }
}

Speaking of Sortable, we’re now ready to add it to our project. Let’s install Sortable via npm.

npm install sortablejs

We’ll also need to add the Sortable script tag in the head of our index.html file.

<script type="module" src="/build/sortable.esm.js"></script>

Moving back to our drag-and-drop-container component, we can import Sortable at the top of our file.

import { Component, Host, h, Prop } from '@stencil/core';
import Sortable from 'sortablejs';

With that in place, we can make use of the Sortable.create method.

private container: HTMLElement;

componentDidLoad() {
  Sortable.create(this.container, {
    animation: 150,
  });
}

Sortable.create takes two parameters. The first parameter is the element we want to turn into a draggable area, so we’ll pass the reference to our container. The second parameter allows us to specify different options that we’ll revisit later, but for now we’ll just set the animation speed to 150ms.

Now you may be wondering why we put this block in the componentDidLoad method. The reason is that in order for us to use our container reference, we have to wait until it’s assigned in our render method. Once it’s assigned, then we can make use of it. So we put this in componentDidLoad because it runs after our render method in the component lifecycle.

Working with the Shadow DOM

If you’re coding along, you might notice that our drag-and-drop component doesn’t work yet, even though all of the pieces seem to be in place. This is a common sticking point for many developers and it has to do with our use of the shadow DOM. The shadow DOM provides both style and DOM encapsulation. Because of that, Sortable isn’t able to reach in and manipulate the DOM nodes in our component. Fortunately, we can make our component a bit more flexible by changing shadow to scoped in the component decorator.

@Component({
  tag: 'drag-and-drop-container',
  styleUrl: 'drag-and-drop-container.css',
  scoped: true,
})

In doing this, we still get style encapsulation, and now Sortable is able to manipulate the elements within our component.

Drag Elements Across Containers

So now our drag-and-drop component works in isolation, but what if we want to drag elements between different containers? Fortunately, Sortable has a solution for that. We can do this using a property called group and then setting the group option in the Sortable.create method.

export class DragAndDropContainer {
  @Prop() containerTitle: string;
  @Prop() group: string;

  private container: HTMLElement;

  componentDidLoad() {
    Sortable.create(this.container, {
      animation: 150,
      group: this.group,
      ghostClass: 'ghost',
    });
  }

Now, drag-and-drop-containers that have the same group will be able to share elements. Let’s turn back to our index.html file to see what this looks like in practice.

<drag-and-drop-container container-title="Backlog" group="kanban">
  <draggable-item task-title="Edit tutorial video" due-date="12/25/21" assignee-avatar="https://avatars.dicebear.com/api/micah/example0.svg"></draggable-item>
  <draggable-item task-title="Publish video on Youtube" due-date="12/25/21" assignee-avatar="https://avatars.dicebear.com/api/micah/example1.svg"></draggable-item>
</drag-and-drop-container>

<drag-and-drop-container container-title="In Progress" group="kanban">
  <draggable-item task-title="Film tutorial video" due-date="12/25/21" assignee-avatar="https://avatars.dicebear.com/api/micah/example2.svg"></draggable-item>
  <draggable-item task-title="Push code to Github" due-date="12/25/21" assignee-avatar="https://avatars.dicebear.com/api/micah/example3.svg"></draggable-item>
</drag-and-drop-container>

<drag-and-drop-container container-title="Done" group="kanban">
  <draggable-item task-title="Create code demo" due-date="12/25/21" assignee-avatar="https://avatars.dicebear.com/api/micah/example4.svg"></draggable-item>
  <draggable-item task-title="Write video script" due-date="12/25/21" assignee-avatar="https://avatars.dicebear.com/api/micah/example5.svg"></draggable-item>
</drag-and-drop-container>

Here, I’ve created three drag-and-drop-containers, each with two draggable-item children. You’ll notice that each drag-and-container has the group attribute set to “kanban.” Because they all share the same group, we’ll be able to drag and drop elements between these containers.

One Final Flourish

To add a little style to our drag-and-drop components, we can take advantage of the ghostClass option of Sortable.create. This option allows us to provide a class name that we can use to style the space where a draggable element will be dropped.

componentDidLoad() {
  Sortable.create(this.container, {
    animation: 150,
    group: this.group,
    ghostClass: 'ghost',
  });
}

Now we can go into our draggable-item.css file and add a style to be applied whenever the ghost class is given to our item. Sortable adds this class to an item any time it is dragged.

:host(.ghost) {
  background: rgba(76, 72, 255, 0.75);
}

Conclusion

And with that, our drag-and-drop component is complete! To me, the most exciting aspect of this is that we have barely scratched the surface of what we can do with Stencil and Sortable. So pull down the code from Github, take a look at Sortable’s documentation, and go build something amazing. Happy coding!


Anthony Giuliano