Ionic 2+ and Dragula issue with scrolling

I’ve been writing an Ionic app for quite some time now.  The purpose of the app is for micro timeline management.  The key to the app is a drag and drop functionality that allows the user to drag and drop different steps inside of an event for easy reordering.

In comes Dragula (with the angular 2 bridge of course).  Dragula is supposedly the most simple drag and drop (dnd) out there.  The setup was nice and easy and I was dragging and dropping in no time.

Unfortunately, there was a problem…

The Problem

If the list of draggable items was longer than the page, I obviously made the container scrollable.  But Dragula (out of the box) uses the body to append the cloned element (the thing you’re dragging).  This means it’s trying to scroll the body of the page, rather than the child container that its supposed to scroll.

Dragula has an option to handle this, though I wasn’t able to make it work with ionic.  Their option is called mirrorContainer.  What this is supposed to do is clone the draggable item into whichever container you specify with mirrorContainer.  Unfortunately, the script has to load after the DOM loads, but when the script loads after, it tells me I already have a bag with the same name. Damned if I do, damned if I don’t.

Here’s how I have my Dragula set up in my page:

constructor(..., private dragulaService: DragulaService) {
    ...
    dragulaService.setOptions('bag', {
      //moves: is telling dragula that there's a "handle" to use to drag
      moves: function (el, container, handle) {
        return handle.className === 'step__menu__button';
      }
    });
    dragulaService.drag.subscribe((value) => {
      this.onDrag(value.slice(1));
    });
    dragulaService.dragend.subscribe((value) => {
      this.onDragEnd(value.slice(1));
    });
    dragulaService.drop.subscribe((value) => {
      this.onDrop(value.slice(1));
    });
}


onDrag(args) {
    console.log("Dragging", args);
}

onDrop(args) { //onDrop is called before onDragEnd
    console.log("An item has been moved AND...");
}

onDragEnd(args) {
    console.log("Drag has ended");
    $('.step-container').css({'overflow':'scroll'});
}

I’ve set up Dragula to use their drag, dragend and drop subscriptions.  They all go to functions at the bottom of the page.  I’ve found that onDrop is called before onDragEnd.  I have them both in here, because if I drop an item back where it started, onDrop is never called.  So, that’s where onDragEnd comes in. Warning: They are both called when you drop an item in a new spot.

The solution

After reading the very few issues in github about this problem, there was only one that suggested a fix outside of Dragula.  It was to use dom_autoscroller.  Dom Autoscroller was built for specific problems like I’m having here.  It’s supposed to scroll any container you choose when dragging an item to an edge.

Here’s how I applied this to my ionic project
  1. npm install dom_autoscroller --save
  2. In the page script, import the autoscroller: import autoScroll from 'dom-autoscroller';
  3. Then inside of ionViewDidLoad() add the autoScroll code below:
ionViewDidLoad() {
    //My container that I want to scroll has the class .step-container
    autoScroll(document.querySelector('.step-container'), {
        margin:30,
        maxSpeed: 10,
        scrollWhenOutside: true,
        autoScroll: function() {
            console.log("Dragging", this.scrolling, this.down);
            if(this.down) {
                $('.step-container').css({'overflow':'hidden'});
            } else {
                $('.step-container').css({'overflow':'scroll'});
            }
            return this.down;
        }
    })
}

Now, the container that I had all of my drag elements in (the container that was supposed to be scrolling) is called .step-container.  You should rename this to whatever scrolling container you want.  Also, some of my code uses jQuery, so you may have to import jQuery into your project: npm install jquery --save, then import * as & from 'jquery';

You can see in the `autoScroll` function, I give my scrolling container the CSS property of overflow:hidden.  In my case, this disables the default scroll, and only relies on the dom-autoscroller.  If you go back up and look at how I set up my dragula code, you’ll notice in the onDragEnd() function, I re-instated the overflow:scroll when I drop the item.  This is important, otherwise the container will not scroll when you try after you drop.

Update note:

If you are using a component that is loaded after ionicViewDidLoad(), you’ll have to use something like this:

import {OnAfterViewInit} from 'angular2/core';

@component({...})
export class test implements OnAfterViewInit {
  ngOnAfterViewInit() {
     // DO IT
  }
}

I hope this helps at least one person.  Please comment and let me know!

Leave a Reply

Your email address will not be published. Required fields are marked *