Search Lessons, Code Snippets, and Videos
search by algolia
X
#native_cta# #native_desc# Sponsored by #native_company#

Sortable Drag and Drop Lists in Firestore

Episode 142 written by Jeff Delaney
full courses for pro members

Health Check: This lesson was last reviewed on and tested with these packages:

  • Angular v6
  • RxJS v6
  • @angular/fire v5
  • draggable v0.18.beta

Find an issue? Let's fix it

Source code for Sortable Drag and Drop Lists in Firestore on Github

Sortable drag-and-drop lists are not an easy feature to code from scratch - you need to juggle a bunch of DOM events and change styles on the elements gracefully to make it look good. Generally speaking, it’s not a good idea to reinvent the wheel when building a sortable list, but instead use a solid library like Shopify’s draggable. It today’s lesson, you will learn how to use draggable in Angular, then provide backend persistence for the items in Firestore.

Initial Setup

The app we’re building is a sortable list of emojis, where the data is an embedded array on a Firestore document.

Sortable list of elements from a firestore document

This guide assumes that you’re working from an Angular app with @angular/fire installed. From there, we can install the @shopify/draggable. Specifically, we will be using the Sortable module built on top of draggable.

npm i @shopify/draggable

Our feature will include (1) a smart component that handles the database interaction and (2) a directive that manages the sortable DOM elements.

ng g component emoji
ng g directive sortable

Sortable Directive

Our first step is to wrap the draggable library in Angular. Most of the UI functionality will happen magically out of the box, but we need to listen to custom events from draggable to know when to update the database. In our case, we want to run a Firestore update on the sortable:stop event, which fires when the user has stopped dragging and the list is sorted.

The directive will take the initial document data via @Input, then emit a sorted array out via @Output. The parent component can listen to this event to know when a database write is needed.

import {
Directive,
ElementRef,
AfterViewInit,
Input,
Output,
EventEmitter
} from '@angular/core';

import { Sortable } from '@shopify/draggable';

@Directive({
selector: '[sortable]'
})
export class SortableDirective implements AfterViewInit {
@Input()
data: any[];

@Output()
stop = new EventEmitter();

sortable: Sortable;

constructor(private el: ElementRef) {}

ngAfterViewInit() {
this.sortable = new Sortable(this.el.nativeElement, {
draggable: 'li'
});

this.sortable.on('sortable:stop', e => this.handleStop(e));
}

handleStop(e) {
console.log(e);
const { newIndex, oldIndex } = e;
const next = this.data;
const moved = next.splice(oldIndex, 1);
next.splice(newIndex, 0, moved[0]);

this.stop.emit();
}
}

In the next steps, we will put the directive to use in the HTML

<ul sortable (stop)="updateFirestore($event)">
<li>One</li>
<li>Two</li>
<li>Thee</li>
</ul>

Sorting the List in Firestore

In the following section, we will use a parent smart component to listen to the sort stop event to trigger the database write.

Sorting the Entire Array

To sort an array in Firestore, we need to send the entire array in the data payload when running the update. Our directive already sends us the sorted array, so we just need execute the database write on the document.

import { Component, OnInit } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument } from '@angular/fire/firestore';
import { Observable } from 'rxjs';

@Component(...)
export class EmojisComponent implements OnInit {
constructor(private afs: AngularFirestore) {}

docRef: AngularFirestoreDocument;
doc$: Observable<any>;


ngOnInit() {
this.docRef = this.afs.doc(`emojis/userJeffD`);
this.doc$ = this.docRef.valueChanges();
}

update(e) {
this.docRef.update(e)
}

Updating Single Array Values in Firestore

Firebase recently introduced a two new methods for working with Array data in Firestore - arrayUnion and arrayRemove. This makes it easy to manage value uniqueness in an array field.

import { firestore } from 'firebase/app';

// ...omitted
appendItem() {
const emoji = '🍺 Beer Me'
this.docRef.update({
favs: firestore.FieldValue.arrayUnion(emoji)
})
}

removeItem(emoji) {
this.docRef.update({
favs: firestore.FieldValue.arrayRemove(emoji)
})

Making it Look Good

Draggable automatically toggles CSS classes on the elements, so we just need to write a little bit of CSS to make it look decent, but add CSS transition animations to these classes to really make them pop.

.draggable-container--is-dragging {
border: 5px blue solid;
}

.draggable-source--is-dragging {
transform: scale(0.8);
background: lightgray;
}

.draggable-source--placed {
background: green;
transform: scale(1);
transition: transform 700ms ease-in;
}

.draggable-mirror {
background: gray;
display: block;
}

The End

Using a library like Draggable adds a lot of magic to your app, but that’s often a tradeoff worth making. Encapsulating the code in a directive will means you can easily remove it or extend it with additional Angular functionality. You now have an easy starting point for building sortable lists in Firestore, let me know what you think on the comments or on Slack.