1 Apr

Using ngDragula with ngPrime from Angular 4

Recently on a project I had the requirement to provided table row reordering. The challenge to that was getting access to the table rows that were created dynamically at runtime and within an ngPrime data table. To provide the drag and drop support I turned to ngDragula. This is a great plugin that provides plenty of events and html support. One thing it currently does not provide is the ability to override the container it uses for setting up the drag and drop. It currently only can use the element that the directive was placed on. As you can see with the use of 3rd party components placing the directive in the proper place can be a challenge.

To address these short comings in my project I created my own version of the ngDragula Directive. Lets walk through what you need to do to add support for drag and drop to your ngPrime data tables.

Creating your own ngDragula Directive

To add support for integration with ngPrime a couple of things need to happen. First we need to override the container that dragula will attach to. In regards to drag and drop with tables the container needs to be the closest parent to the items you want to drag. In our case that ends up being the tbody element. Unfortunately ngPrime data tables do not allow access to this element, so we have no way to place the dragula directive in the correct location. Secondly we need to allow for delayed binding to the data table. Since we provide a collection of rows to the ngPrime data table the tbody tag will not be available onInit of the component. To make this work we need to bind the ngDragula directive after the view is available from Angular. Luckily Angular provides us with the AfterViewInit lifecycle hook. Lets take a look at the code.

The first thing we need to do is create our own version of the ngDragula directive. ensure you give it a unique name that wont clash with existing libs. Also add AfterViewInit to the implements of the class

import { Directive, OnChanges, AfterViewInit, OnInit, Input, ElementRef, SimpleChange } from '@angular/core';
import { DragulaService, dragula } from 'ng2-dragula';

@Directive({ selector: '[primeDragula]' })
export class PrimeDragulaDirective implements OnChanges, OnInit, AfterViewInit { }

Next we change the container to protected so we can provide extension later

   protected container: any;

Now we can implement the Angular events OnInit, AfterViewInit. In OnInit we wire up the options, and set the initial container element. New options that can be provided to the directive are ‘initAfterView’ and ‘childContainerSelector’. Here we check to see if late binding is needed and if not initialize the directive like usual. If late binding is needed AfterViewInit handles that check.

ngOnInit(){
    this.options = Object.assign({}, this.dragulaOptions);
    this.container = this.el.nativeElement;

    if(!this.options.initAfterView){
      this.initialize();
    }
  }

  ngAfterViewInit() {
    if(this.options.initAfterView){
      this.initialize();
    }
  }

Lets move the initialization code to its own method for reuse. Here the only new code is the ‘childContainerSelector’ check this is what gives us the ability to use a different container then the one the ngDragula directive was placed on. Notice how we set the mirrorContainer as well. This is because since the container is a sub element of the parent we want to ensure mirror object (the drag object visual that shows the movement) is positioned relative to the correct parent.

NOTE: an upgrade to this directive would be to use another property to dictate overriding the defauly mirrorContainer = ‘document.body’

 protected initialize(){    
    if(this.options.childContainerSelector){
        //find the element starting at the directive element and search down
        this.container = this.el.nativeElement.querySelector(this.options.childContainerSelector);
        this.options.mirrorContainer = this.container;
      }

    let bag = this.dragulaService.find(this.primeDragula);
    let checkModel = () => {
      if (this.dragulaModel) {
        if (this.drake.models) {
          this.drake.models.push(this.dragulaModel);
        } else {
          this.drake.models = [this.dragulaModel];
        }
      }
    };
    if (bag) {
      this.drake = bag.drake;
      checkModel();
      this.drake.containers.push(this.container);
    } else {
      this.drake = dragula([this.container], this.options);
      checkModel();
      this.dragulaService.add(this.primeDragula, this.drake);
    }
  }

Finally, add in the pre-existing OnChanges method from the ngDragula directive

public ngOnChanges(changes: { dragulaModel?: SimpleChange }): void {
    if (changes && changes.dragulaModel) {
      if (this.drake) {
        if (this.drake.models) {
          let modelIndex = this.drake.models.indexOf(changes.dragulaModel.previousValue);
          this.drake.models.splice(modelIndex, 1, changes.dragulaModel.currentValue);
        } else {
          this.drake.models = [changes.dragulaModel.currentValue];
        }
      }
    }
  }

Using the new directive

So now the directive is create we are ready to implement it.

In our component template where we define our ngPrime data table lets add the dragula directive.

 <p-dataTable [value]="rows" [primeDragula]="bag" [dragulaModel]="rows" 
  [dragulaOptions]="{ childContainerSelector: 'tbody', initAfterView: true }">
  <p-column header="Move">
    <ng-template pTemplate="body" let-rowData="rowData">
      <i class="fa fa-bars"></i>
    </ng-template>
  </p-column>
  <p-column field="name" header="Name">          
  </p-column>      
</p-dataTable>

That is all there is to it. Pretty simple changes that allow a greater user experience.

Gotcha’s:

  • Dont forget to add the javascript dragula version to your package.json and angular CLI styles and scripts sections. This is a requirement for ngDragula to work as expected.

Versions used

"@angular/animations": "4.0.0",
"@angular/common": "4.0.0",
"@angular/compiler": "4.0.0",
"@angular/core": "4.0.0",
"@angular/forms": "4.0.0",
"@angular/http": "4.0.0",
"@angular/platform-browser": "4.0.0",
"@angular/platform-browser-dynamic": "4.0.0",
"@angular/router": "4.0.0",
"dragula": "^3.7.2",
"lodash": "4.17.4",
"ng2-dragula": "^1.3.0",
"primeng": "2.0.5",
"rxjs": "5.1.0",
"zone.js": "0.8.4"

Hopefully you found this helpful. You can see a working example here on Plunkr. Also these changes have been submitted for review to the guys over at valor-software. With a little luck I can just use the official version of ngDragula one day!