javascriptangularangular9dragulang2-dragula

ng2-dragula after adding new item it's getting displayed at the top


I am using ng2-dragula for drag and drop feature. I am seeing issue when I drag and drop first element(or any element) at the end and then try to add new item to the array using addNewItem button, new item is not getting added to the end. If i don't drop element to the end, new item is getting added at the end in UI. I want new items to be displayed at the bottom in any scenario. Any help is appreciated. This issue is not reproducible with Angular 7. I see this happening with Angular 9

JS

export class SampleComponent {

  items = ['Candlestick','Dagger','Revolver','Rope','Pipe','Wrench'];
  constructor(private dragulaService: DragulaService) { 
    dragulaService.createGroup("bag-items", {
      removeOnSpill: false
    });
  }

  public addNewItem() {
    this.items.push('New Item');
  }
}

HTML

<div class="container" [dragula]='"bag-items"' [(dragulaModel)]='items'>
    <div *ngFor="let item of items">{{ item }}</div> 
</div>

<button id="addNewItem" (click)="addNewItem()">Add New Item

enter image description here

I edited the stackblitz from the comment to help visualize the issue. This seems to be triggered when a unit is dragged to the bottom of the list. Updated stackblitz : https://stackblitz.com/edit/ng2-dragula-base-ykm8fz?file=src/app/app.component.html ItemsAddedOutOfOrder


Solution

  • You can try to restore old item position on drop.

    constructor(private dragulaService: DragulaService) {
      this.subscription = this.dragulaService.drop().subscribe(({ name }) => {
        this.dragulaService.find(name).drake.cancel(true);
      });
    } 
    

    Forked Stackblitz

    Explanation

    There is some difference between how Ivy and ViewEngine insert ViewRef at specific index. They relay on different beforeNode

    Ivy always returns ViewContainer host(Comment node)ref if we add item to the end:

    export function getBeforeNodeForView(viewIndexInContainer: number, lContainer: LContainer): RNode|
        null {
      const nextViewIndex = CONTAINER_HEADER_OFFSET + viewIndexInContainer + 1;
      if (nextViewIndex < lContainer.length) {
        const lView = lContainer[nextViewIndex] as LView;
        const firstTNodeOfView = lView[TVIEW].firstChild;
        if (firstTNodeOfView !== null) {
          return getFirstNativeNode(lView, firstTNodeOfView);
        }
      }
    
      return lContainer[NATIVE]; <============================= this one
    }
    

    ViewEngine returns last rendered node(last <li/> element)ref

    function renderAttachEmbeddedView(
        elementData: ElementData, prevView: ViewData|null, view: ViewData) {
      const prevRenderNode =
          prevView ? renderNode(prevView, prevView.def.lastRenderRootNode!) : elementData.renderElement;
      ...
    }
    

    The solution might be reverting the dragged element back to original container so that we can let built-in ngForOf Angular directive to do its smart diffing.

    Btw, the same technique is used in Angular material DragDropModule. It remembers position of dragging element and after we drop item it inserts it at its old position in the DOM which is IMPORTANT.