Search code examples
angularrxjsobservableng2-dragula

Angular 2/4 Observable - why is ng2-dragula sending multiple drop events after a single drag and drop in the UI?


EDIT regarding question title change

The original question title was this:

Angular 2/4 Observable - how to modify the object that emits values inside the subscribe without firing more events?

After extensive investigation that was not the cause of the issue, BUT there are suggestions in the comments here about how one might go about addressing that particular problem. The ACTUAL problem was related to having multiple ng2-dragula subscribers alive at the same time, hence why I have updated the question title to reflect that.


I am using ng2-dragula plugin and subscribe to the dropModel event.

The problem I am facing is that inside the subscribe code I need to modify the model by re-arranging the other items in the model. This causes dropModel to fire the dropModel event again - it obviously thinks that because I changed the model positions in the list that the user did a drag and drop, and not my code.

I tried take(1) but that did not solve the problem - it just keeps taking one event, so when I change the model inside subscribe, obviously it takes the next (1) again.

This is the code:

this._dragulaService.dropModel.take(1).subscribe(() => {
  // For ease, we just copy all the values into the model and then destroy
  // the itemsControl, then patch all model values back to the form
  // This is easier than working out which item was dragged and dropped where
  // (and on each drop we want to save the model, so we would need to update it anyway)
  this.itemsControl['controls'].forEach((formGroup, index) => {
    this.template.template_items[index].sort = index; // ensures the API will always return items in the index order
    console.log('copying ID', formGroup['controls'].id.value);
    this.template.template_items[index].id = formGroup['controls'].id.value;
    this.template.template_items[index].item_type = formGroup['controls'].item_type.value;
    this.template.template_items[index].content = formGroup['controls'].content.value;
    this.template.template_items[index].is_completed = formGroup['controls'].is_completed.value;
  });
}

Ideally I would want to 'grab' the first drop event (user initiated), then inside the subscribe code, unsubscribe or stop receiving more events, then process the model and finally resubscribe afterwards.

I know this is kind of odd - inside the subscribe async code I need to somehow 'pause' the subscription. Although 'pause' is not quite right - I actually want to somehow prevent firing new events until I've finished processing the current event. Pause would just result in me processing my own events (if that makes sense). Is this possible?

Note

The model here that dragula is bound to here is a dynamic array of itemsControls and NOT a pure data model in the normal sense. Hence why I am extracting the data out of the form controls and inserting into the actual data model.

UPDATE 1

I decided to log what dragula was doing with my bound itemsControl (an array of AbstractControls).

Before the drag, I log what is actually inside the array:

itemsControl is now this: (4) [FormGroup, FormGroup, FormGroup, FormGroup]

In the dropModel subscribe handler, I log a "dropped" event and the length of the array. Here is the output when I drag and drop any item, always the same output:

dropped
length is 3
dropped
length is 3
dropped
length is 4
dropped
length is 5
dropped
length is 5
dropped
length is 4
dropped
length is 4

However, if I remove the code I posted above (ie. so I do not touch the underlying data model), this is the output:

dropped
length is 4
dropped
length is 4

So at least that proves that by re-sorting the data I am not only causing more events to fire (as I suspected) but also the strange side effect that the length of the controls array is increasing and decreasing (not sure why that is).

Given this output, I need a way to only act upon the very last event emitted.

Is there a way to only get the last event from an Observable?

UPDATE 2

According to this, the real problem here is that ng2-dragula does not support binding dropModel to a FormArray. But there appears to be a workaround...still searching!


Solution

  • EDIT 2022

    This answer is now out of date

    Dragula used to use EventEmitters, now it uses Observables. Please see the latest docs.


    So, after all this, it appears there is a RIGHT way and a WRONG way to subscribe to ng2-dragula:

    The wrong way, which is what I had:

    dragulaService.dropModel.subscribe((result) => { ... }
    

    The right way:

    private destroy$ = new Subject();
    
    constructor (private _dragulaService: DragulaService) {
      this._dragulaService.dropModel.asObservable()
        .takeUntil(this.destroy$).subscribe((result) => { ... }
    }
    
    ngOnDestroy() {
      this.destroy$.next();
    }
    

    Taken from here.

    Now I only get one event on each drop, and sorting the AbstractControls does not trigger further drop events.

    UPDATE

    Thanks to comments from @RichardMatsen I investigated further why the above code fixed the problem. It is not using asObservable() that does it, so I'm not really sure why that is recommended in the link to issue 714 I provided above. Possibly asObservable() is needed so that we can properly unsubscribe (given that dragulaService is an EventEmitter)?

    Previously I used what I read in the docs:

    dragulaService.destroy(name)

    Destroys a drake instance named name.

    to destroy the 'drake':

      ngOnDestroy(){
        this._dragulaService.destroy('bag-container');
      }  
    

    This did not unsubscribe from the dragulaService. So when I navigated back to the component, it was still emitting events and that is the reason I was getting multiple drops.

    It also turns out that subscribing and destroying is not the recommended way to use the dragulaService, so I've submitted this PR to update the readme for ng2-dragula to make this clear for future users.