15

Working with ng2-dragula. I'm looking to update the orderNumber for every item in a database using the new dropped order.

dragulaService.dropModel.subscribe((value) => {
  var drake = dragulaService.find('bag-orders').drake
  var models = drake.models
  console.log(models)
})

The new model order that it returns does not reflect the order within the bag.

TL;DR: has anyone implemented reordering within a database onDrop with ng2-dragula?

Petr Adam
  • 1,355
  • 8
  • 22
Dan Feinstein
  • 316
  • 1
  • 3
  • 15
  • 1
    Having the same issue and can't seem to find an answer. I actually lose the DOM element I am dragging completely once I drop it. Simply removing the `[dragulaModel]='myList'` seems to fix this strange behavior (still able to drag and drop - but no longer can trigger the dragulaService.dropModel call). – Dan J Dec 07 '16 at 21:17
  • @DanJ, I'm seeing just a setup scenario, nothing about onDrop or the model returned from onDrop? – Dan Feinstein Dec 07 '16 at 21:37
  • sorry, must have been a copy paste issue - going around in circles trying to figure out the issue. – Dan J Dec 07 '16 at 22:29
  • Figured out my issue and this may help you. If not, post more of your code and I can try to help debug. http://stackoverflow.com/a/41048184/362576 – Dan J Dec 08 '16 at 20:34
  • Has the issue solved? I don't see any accepted answer here. – Saiyaff Farouk Jun 05 '17 at 08:15

4 Answers4

11

If you want to be able to drag items (without them disappearing) AND fire the dropModel event:

  • Put the [dragula] and [dragulaModel] directives in the parent element. (For example, contrary to the current doc where it says to put them in the <li>, you have to put them in the <ul>

  • Inject the dragularService, and, in the constructor of your component:

  • Call the dragulaService.setOptions for the bag (you can pass an empty dictionary). If you don't call it, dropModelcallback is not fired

  • Subscribe to dropModel

The result:

<!--thecomponent.html-->
<h2>Playlists</h2>
<ul [dragula]='"first-bag"' [dragulaModel]='playlists'>
  <li *ngFor="let playlist of playlists">

//Inside the component.ts  
playlists: Playlist[];

constructor(private youtubeService: YoutubeService, private dragulaService: DragulaService) {

    dragulaService.setOptions('first-bag', {})
    dragulaService.dropModel.subscribe((value) => {
      this.onDropModel(value);
    });
}

private onDropModel(args) {
    //Here, this.playlists contains the elements reordered
}
darksider
  • 900
  • 2
  • 12
  • 20
  • 1
    thanks, but I've gotten to this stage -- my issue is the object model returned by onDrop is in the wrong order. I'm fairly certain it's a limitation of the library since the dropped object moves to the correct index but the others are only shifted downwards, rather than reordering. This would be perfectly useful to find which slot the new one went to, but I need to replace **every** item's orderNumber in the list – Dan Feinstein Dec 14 '16 at 22:55
  • Here is a good discussion that might help others and also includes a link to a plunk that shows ordering of items using dropModel and capturing the correct order after a dropModel event fires. https://github.com/valor-software/ng2-dragula/issues/306 – rmcsharry Oct 18 '17 at 15:01
8

I was using the ng2-dragula and It's quite strange on something that I've noticed. The issue I had was the same. Server call is not updating the data object according to the dragged order.

I've just used if clause inside the ngDoCheck lifecycle hook to solve the issue.

It's sorted in the printed object in the console. After digging deeper a bit could find out in the network that the object that is sent with the update server call was the not updated one.

So, it's because the server call is made before the data object is updated.

All I did was adding a global variable which can keep track of drag has updated the data object.

private dropModelUpdated = false;

ngAfterViewInit() {
    // this method only changes a variable to execute a method in ngDoCheck
    this.dragulaService.drop.subscribe((value) => {
      this.dropModelUpdated = true;
    });
}

Then, in the ngDoCheck lifecycle hook,

ngDoCheck() {    
    if (this.dropModelUpdated) { // this excutes if this.dropModelUpdated is true only
        const drake = this.dragulaService.find('any_bag_name').drake;
        const models = drake.models;
        this.modelContent.groups = models[0];
        // ...Here... Make the server or DB call to update the model with the changed/sorted order
        this.dropModelUpdated = false; // make sure this is there for the next smooth execution
    }
}
Saiyaff Farouk
  • 3,135
  • 3
  • 22
  • 35
5

I finally found a solution. I have a horizontal list. The first two elements are the ones a I want to ignore and all have the ignore CSS class. And all elements have the item class. So markup:

<ul [dragula]='"foo-list"' class="list">
    <li class="list ignore">a</li>
    <li class="list ignore">b</li>
    <li class="list" *ngFor="let item of getList()" [attr.data-id]="item.id">{{ item.name }}</li>
</ul>

Then the TypeScript:

constructor(private dragulaService: DragulaService) {
    let dragIndex: number, dropIndex: number;

    dragulaService.setOptions('foo-list', {
        moves: (el, source, handle, sibling) => !el.classList.contains('ignore'),
        accepts: (el, target, source, sibling) => sibling === null || !sibling.classList.contains('ignore'),
        direction: 'horizontal'
    });

    dragulaService.drag.subscribe((value) => {
        // HACK
        let id = Number(value[1].dataset.id);
        if (!!id) {
            dragIndex = _.findIndex(this.getList(), {id: id});
        }
    });

    dragulaService.drop.subscribe((value:any[]) => {
        // HACK
        let elementNode = value[2].querySelectorAll('.item:not(.ignore)').item(dragIndex);
        if (elementNode) {
            let id = Number(elementNode.dataset.id);
            if (!!id) {
                dropIndex = _.findIndex(this.getList(), {id: id});
            }
        }

        if (value[2] === value[3]) { // target === source
            if (dragIndex >= 0 && dropIndex >= 0 && dragIndex !== dropIndex) {
                let temp: any = this.list[dragIndex];

                this.list[dragIndex] = this.list[dropIndex];
                this.list[dropIndex] = temp;
            }
        }

        // Custom code here
    });
}

getList(): any[] {
    return this.list;
}
Marçal Juan
  • 1,274
  • 14
  • 19
2

I found Marçal's answer incredibly helpful, and have even expanded on it a little to post an update to my DB to update the sequence value of each item (contacts within an organization, in my case) in the list:

HTML (container is the contact's organization. a contact, in my case, can't be moved between organizations):

<div class="container" [dragula]="'org-' + org.id" [dragulaModel]="org.contacts">
  <nested-contact *ngFor="let contact of org.contacts" [user]="contact" class="contact" [attr.data-id]="contact.id"></nested-contact>
</div>

JS (in my Contact Service, a PUT function to update the stored sequence values associated with each of my contacts, so that their orders persist):

contactsIdPut (id, body) {
  let url = '/api/v3/contacts/' + id + '?access_token=' + localStorage.getItem('access_token');
  let headers = new Headers({ 'Content-Type': 'application/json' });
  let options = new RequestOptions({ headers: headers });

  return this.http.put(url, body, options)
    .map((response: Response) => {
      return response.json();
    });

}

JS (in my organization view component, to outline the actions to be taken upon drag and drop, respectively):

export class nestedOrganizationComponent {
  orgs = [];
  thisOrg: any;
  thisContact: any;

  constructor (private _contactService: ContactService, private dragulaService: DragulaService) {
    this._contactService = _contactService;

    let dragIndex: any;
    let dropIndex: any;
    let elementNode: any;

    dragulaService.drag.subscribe((value) => {
      let id = Number(value[1].dataset.id);
      let orgId: Number = value[0].split('-')[1];
      elementNode = value[2].querySelectorAll('.contact:not(.ignore)').item(dragIndex);

      if (!!id) {
        // this renderedOrgs is just an array to hold the org options that render in this particular view
        this._organizationService.renderedOrgs.push(this.org);
        this.thisOrg = this._organizationService.renderedOrgs.filter(org => { return org.id == orgId; })[0];
        this.thisContact = this.thisOrg.contacts.filter(contact => { return contact.id == id; })[0];

        let arr = this.thisOrg.contacts.map(x => { return x.id; });
        dragIndex = arr.indexOf(id);
      }
    });

    dragulaService.drop.subscribe((value: any[]) => {
      if (elementNode) {
          let id = Number(elementNode.dataset.id);
          if (!!id) {
            let arr = this.thisOrg.contacts.map(x => { return x.id; });
            dropIndex = arr.indexOf(id);
          }
      }

      if (value[2] === value[3]) { // target container === source container
          if (dragIndex >= 0 && dropIndex >= 0 && dragIndex !== dropIndex) {
            this.thisOrg.contacts.forEach((contact, index) => {
              contact.sequence = index;
              this.updateSequence(contact.id, index);
            });
          }
      }
    });
  }

  updateSequence (id: Number, index: Number) {
    let contactBody = {
      avatar: {
        sequence: index,
      }
    };

    return this._contactService.contactsIdPut(id, contactBody)
      .subscribe(
        (data: any) => {
          // nothing is needed, the same view can apply because the contact has already been moved.
        },
        (error: any) => {
          console.error(error);
        }
      );
  }
}

Hopefully this provides a bit more clarity on the matter to someone else in a spot similar to where I found myself today.

Kate Sowles
  • 71
  • 1
  • 7