Angular 2/4 Observable - 为什么 ng2-dragula 在 UI 中一次拖放后发送多个拖放事件?

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

关于问题标题更改的编辑

原题目是这样的:

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

经过广泛调查后发现这不是问题的原因,但此处的评论中有关于如何解决该特定问题的建议。实际问题与同时存在多个 ng2-dragula 订阅者有关,因此我更新了问题标题以反映这一点。


我正在使用 ng2-dragula 插件并订阅 dropModel 事件。

我面临的问题是,在订阅代码中,我需要通过 re-arranging 模型中的其他项目来修改模型。这导致 dropModel 再次触发 dropModel 事件 - 它显然认为是因为我更改了列表中的模型位置,用户进行了拖放,而不是我的代码。

我试过 take(1) 但这并没有解决问题 - 它只是一直在接受一个事件,所以当我在 subscribe 中更改模型时,显然它会再次接受下一个 (1)。

这是代码:

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;
  });
}

理想情况下,我希望 'grab' 第一个删除事件(用户发起),然后在订阅代码中取消订阅或停止接收更多事件,然后处理模型,最后重新订阅。

我知道这有点奇怪 - 在订阅异步代码中,我需要以某种方式 'pause' 订阅。尽管 'pause' 不太正确 - 我实际上想以某种方式阻止触发新事件,直到我完成当前事件的处理。暂停只会导致我处理自己的事件(如果这有意义的话)。这可能吗?

备注

这里dragula绑定的模型是itemsControls的动态数组,不是正常意义上的纯数据模型。因此,为什么我要从表单控件中提取数据并插入到实际数据模型中。

更新 1

我决定记录 dragula 使用我绑定的 itemsControl(抽象控件数组)所做的事情。

在拖动之前,我记录了数组中的实际内容:

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

在 dropModel 订阅处理程序中,我记录了一个 "dropped" 事件和数组的长度。这是我拖放任何项目时的输出,总是相同的输出:

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

但是,如果我删除上面发布的代码(即,这样我就不会触及底层数据模型),这就是输出:

dropped
length is 4
dropped
length is 4

所以至少这证明了通过 re-sorting 数据我不仅引发了更多事件(正如我所怀疑的那样)而且还引发了奇怪的副作用,即控件数组的长度在增加和减少(不确定为什么会这样)。

鉴于此输出,我需要一种方法来仅对发出的最后一个事件采取行动。

有没有办法只从 Observable 获取最后一个事件?

更新 2

根据 this,这里真正的问题是 ng2-dragula 不支持将 dropModel 绑定到 FormArray。但似乎有解决方法...仍在搜索中!

如果你总是得到两次发射,一个廉价的答案可能是使用标志来区分第一次和第二次

const haveModified = false;
this._dragulaService.dropModel.subscribe(() => {
  if (!haveModified) {
    this.itemsControl['controls'].forEach((formGroup, index) => {
      ...
    });
    haveModified = true;
  } else {
    haveModified = false;
  }
  });
}

更多 Rx 方法 - 检查下标值,如果两个发射相同(或 属性 相同)使用 distinctUntilChanged(compare: function).

更新

刚刚玩过 Plunker,对 class 中的数据数组进行简单排序,但我没有从 dragula 获得第二次发射。

  constructor(dragulaService: DragulaService) {
    dragulaService.dropModel.subscribe(value => {
      console.log(value)
      console.log('target before sort', this.target)
      this.target.sort();
      console.log('target after sort', this.target)
    })
  }

我无法完全理解您正在执行的操作,但我看到您正在使用对 HTML 控件的引用。您可以通过操纵基础数据来做到这一点吗?

更新#2

一直在为 distinctUntilChanged(comparer) 使用比较器,其中涉及比较项目的 索引 ,这应该使该解决方案可行。

但是,由于源代码(dragula.provider.ts,第 97 行)中的一些破解,无法获取索引

target.removeChild(dropElm); // element must be removed for ngFor to apply correctly

dropModel 事件是

this.dropModel.emit([name, dropElm, target, source]);

但是 dropElm 既不在目标也不在源中,所以我们不知道索引是什么。如果 dropIndex 也像这样发出会更好

this.dropModel.emit([name, dropElm, target, source, dropIndex]);

然后我们可以捕获重复事件。

编辑 2022

此答案现已过时

Dragula 以前使用 EventEmitter,现在使用 Observable。请查看最新文档。


所以,在这一切之后,订阅 ng2-dragula 似乎有正确的方式和错误的方式:

错误的方法,这是我的:

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

正确的方法:

private destroy$ = new Subject();

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

ngOnDestroy() {
  this.destroy$.next();
}

摘自 here.

现在每次掉落时我只会得到一个事件,并且对 AbstractControls 进行排序不会触发进一步的掉落事件。

更新

感谢@RichardMatsen 的评论,我进一步调查了上述代码解决问题的原因。它没有使用 asObservable() 来执行此操作,因此我不太确定为什么在 link 中建议我在上面提供的问题 714 中这样做。可能需要 asObservable() 以便我们可以正确取消订阅(假设 dragulaService 是一个 EventEmitter)?

以前我用的是我读到的in the docs:

dragulaService.destroy(name)

Destroys a drake instance named name.

摧毁'drake':

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

这并没有取消订阅 dragulaService。因此,当我导航回该组件时,它仍在发出事件,这就是我多次掉落的原因。

事实证明,订阅和销毁 不是推荐的方式 使用 dragulaService,所以我已经提交 this PR 更新 ng2 的自述文件- dragula 向未来的用户说明这一点。

2021 年 Angular

的解决方案

错误方式:this.dragulaService.dropModel.subscribe((result) => { ... }

右路:this.dragulaService.dropModel('GroupName').pipe(takeUntil(this.destroyDragulaEventHandler$)).subscribe((result) => { ... }

创建一个带有 rxjs 主题的实例变量: private destroyDragulaEventHandler$ = new Subject();

确保在 ngOnDestory 中进行一些清理:ngOnDestroy(){ this.destroyDragulaEventHandler$.next(); this.dragulaService.destroy("groupName"); }