ngx-sortablejs icon indicating copy to clipboard operation
ngx-sortablejs copied to clipboard

DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Open ScottMGerstl opened this issue 6 years ago • 5 comments
trafficstars

Hello,

I am running into an issue with drag and dropping across containers.

I have created a reusable sortable list that I am implementing multiple times on one page and I want to drag an item from one group to another. Here is an example of the implementation where app-sortable-list-form is the reused component

    <ng-container formArrayName="agendaBlocks">
        <!-- Iterate over schedule blocks -->
        <div class="schedule-block" *ngFor="let block of activeFormGroup.controls.agendaBlocks.controls; let i = index">

            <!-- Schedule block header -->
            <div class="block-header">
                <app-label color="midnight-blue" medium> {{ block.controls.name.value }} </app-label>
                <app-label color="dark" xsmall> {{ getTimePipeInputs(block) | timeRange }} </app-label>
            </div>

            <!-- Session list -->
            <ng-container [formGroupName]="i">
                <app-sortable-list-form
                    [showThumbnail]="true" thumbnailType="session"
                    formControlName="sessions" dragGroup="agendaBlocks">
                </app-sortable-list-form>
                <!-- Add Session button -->
            </ng-container>
        </div>
    </ng-container>

Here is the source code of the reusable sortable list form. It implements ControlValueAccessor so it can be added to a reactive form (not necessary info but wanted to make sure it doesn't cause confusion). You'll see the options called dragDropOptions and an input which allows the use of a group.

sortable-list-form.component.ts

import { Component, ElementRef, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { SortablejsOptions } from 'angular-sortablejs';
import { ThumbnailType } from '../../../thumbnail';
import { UniqueUtils } from '../../../utils';
import { SortableListItem } from '../../models';

export const SORTABLE_LIST_VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => SortableListComponent),
    multi: true,
};

@Component({
    selector: 'app-sortable-list-form',
    templateUrl: 'sortable-list-form.component.html',
    styleUrls: ['sortable-list-form.component.scss'],
    providers: [SORTABLE_LIST_VALUE_ACCESSOR]
})
export class SortableListComponent implements ControlValueAccessor {
    @Input() showThumbnail = false;
    @Input() thumbnailType: ThumbnailType = 'content';

    @Input()
    set dragGroup(value: string) {
        this.dragDropOptions.group = value;
    }

    private _dragging = false;

    get dragging(): boolean {
        return this._dragging;
    }
    set dragging(value: boolean) {
        this._dragging = value;

        const op = this._dragging === true ? 'add' : 'remove';
        (this.elementRef.nativeElement as HTMLElement).classList[op]('dragging');
    }

    dragDropOptions: SortablejsOptions= {
        ghostClass: 'drop-placeholder',
        filter: 'input, textarea, app-button, app-icon-hover-button',
        preventOnFilter: false,
        onStart: (event) => this.onDragStart(event),
        onEnd: (event) => this.onDragEnd(event),
        onSort: () => this.onSort()
    };

    uniqueContainerId: string = UniqueUtils.getRandomNumberString();

    listItems: SortableListItem[];

    // ControlValueAccessor properties
    onChange: Function;
    onTouched: Function;
    isDisabled = false; // NOT IMPLEMENTED

    constructor(protected elementRef: ElementRef) {
        (this.elementRef.nativeElement as HTMLElement).classList.add('sortable-list');
    }

    // Handlers
    onRemoveClicked(index: number) {
        this.listItems = [...this.listItems || []];
        this.listItems.splice(index, 1);

        this.notifyChanged();
    }

    onListItemFormSave(index: number, listItemData: SortableListItem) {
        if (index == null) {
            this.listItems = [...this.listItems, listItemData];
        }
        else {
            this.listItems.splice(index, 1, listItemData);
        }

        this.notifyChanged();
    }

    // This is here because Session's title property is names "Name". Everything else is "Title"
    getNameOrTitleText(value: SortableListItem) {
        if (value == null) {
            return null;
        }

        return value.title || value.name || null;
    }

    // ControlValueAccessor implementation
    writeValue(value: SortableListItem[]): void {
        this.listItems = value != null
            ? value
            : [];
    }

    registerOnChange(onChangeFunction: Function): void {
        this.onChange = onChangeFunction;
    }

    registerOnTouched(onTouchedFunction: Function): void {
        this.onTouched = onTouchedFunction;
    }

    setDisabledState?(isDisabled: boolean): void {
        this.isDisabled = isDisabled;
    }

    protected notifyChanged() {
        if (this.onChange) {
            // Allows the use of the standard required validator
            const notifyValue = (this.listItems || []).length === 0 ? null : this.listItems;
            this.onChange(notifyValue);
        }
    }

    // Drag drop functions
    protected onDragStart(event: Event) {
        console.log(event);

        this.listItems = [...this.listItems || []];
        this.dragging = true;
    }

    protected onDragEnd(event?: Event) {
        console.log(event);
        this.dragging = false;
    }

    protected onSort() {
        this.notifyChanged();
    }
}

sortable-list-form.component.ts

<div class="drag-list-container" [ngClass]="uniqueContainerId" [sortablejs]="listItems" [sortablejsOptions]="dragDropOptions">
    <ng-container *ngFor="let item of listItems; let i = index">
        <app-panel-list-item class="list-item stripe"
            [hideThumbnail]="showThumbnail !== true" [thumbnailUrl]="item.thumbnailUrl"
            [thumbnailType]="thumbnailType"
            [title]="getNameOrTitleText(item)" [description]="item.description" mode="remove"
            (removeClicked)="onRemoveClicked(i)">
        </app-panel-list-item>
    </ng-container>
</div>

Here is the start event: image

Here is the end event: image

Here is the error text:

at EmulatedEncapsulationDomRenderer2.push.../../node_modules/@angular/platform-browser/fesm5/platform-browser.js.DefaultDomRenderer2.removeChild (http://localhost:4200/vendor.js:69796:20)
    at DebugRenderer2.push.../../node_modules/@angular/core/fesm5/core.js.DebugRenderer2.removeChild (http://localhost:4200/vendor.js:61146:23)
    at execRenderNodeAction (http://localhost:4200/vendor.js:57614:22)
    at visitRenderNode (http://localhost:4200/vendor.js:57591:13)
    at visitSiblingRenderNodes (http://localhost:4200/vendor.js:57540:13)
    at visitRootRenderNodes (http://localhost:4200/vendor.js:57534:5)
    at renderDetachView (http://localhost:4200/vendor.js:58279:5)
    at detachEmbeddedView (http://localhost:4200/vendor.js:58237:5)
    at ViewContainerRef_.push.../../node_modules/@angular/core/fesm5/core.js.ViewContainerRef_.remove (http://localhost:4200/vendor.js:58502:24)
    at http://localhost:4200/vendor.js:4509:38
    at DefaultIterableDiffer.push.../../node_modules/@angular/core/fesm5/core.js.DefaultIterableDiffer.forEachOperation (http://localhost:4200/vendor.js:55958:17)
    at NgForOf.push.../../node_modules/@angular/common/fesm5/common.js.NgForOf._applyChanges (http://localhost:4200/vendor.js:4502:17)
    at NgForOf.push.../../node_modules/@angular/common/fesm5/common.js.NgForOf.ngDoCheck (http://localhost:4200/vendor.js:4496:22)
    at checkAndUpdateDirectiveInline (http://localhost:4200/vendor.js:58959:19)
    at checkAndUpdateNodeInline (http://localhost:4200/vendor.js:60220:20)
    at checkAndUpdateNode (http://localhost:4200/vendor.js:60182:16)
    at debugCheckAndUpdateNode (http://localhost:4200/vendor.js:60816:38)
    at debugCheckDirectivesFn (http://localhost:4200/vendor.js:60776:13)
    at Object.eval [as updateDirectives] (ng:///SharedModule/SortableListComponent.ngfactory.js:57:5)
    at Object.debugUpdateDirectives [as updateDirectives] (http://localhost:4200/vendor.js:60768:21)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60164:14)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execComponentViewsAction (http://localhost:4200/vendor.js:60347:13)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60170:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execEmbeddedViewsAction (http://localhost:4200/vendor.js:60368:17)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60165:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execComponentViewsAction (http://localhost:4200/vendor.js:60347:13)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60170:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execEmbeddedViewsAction (http://localhost:4200/vendor.js:60368:17)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60165:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execComponentViewsAction (http://localhost:4200/vendor.js:60347:13)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60170:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execComponentViewsAction (http://localhost:4200/vendor.js:60347:13)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60170:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execEmbeddedViewsAction (http://localhost:4200/vendor.js:60368:17)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60165:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execEmbeddedViewsAction (http://localhost:4200/vendor.js:60368:17)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60165:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execComponentViewsAction (http://localhost:4200/vendor.js:60347:13)
    at checkAndUpdateView (http://localhost:4200/vendor.js:60170:5)
    at callViewAction (http://localhost:4200/vendor.js:60405:21)
    at execEmbeddedViewsAction (http://localhost:4200/vendor.js:60368:17)

please let me know if you need more information.

Also, I love this library!

ScottMGerstl avatar Feb 26 '19 23:02 ScottMGerstl

I added unique id classes to the containers to help show the container differences between from and to

ScottMGerstl avatar Feb 26 '19 23:02 ScottMGerstl

Hi @ScottMGerstl

thank you for a very detailed description.

Which version of sortable library do you use? If 1.8+ then could you try whether it is reproducible with v1.7.0?

smnbbrv avatar Feb 27 '19 09:02 smnbbrv

@smnbbrv

sortablejs was on 1.8.0. I downgraded and tried it on 1.7.0 but I am getting the same error.

For angular-sortablejs I am on 2.6.0.

ScottMGerstl avatar Feb 27 '19 15:02 ScottMGerstl

Doing a bit more digging, It looks like maybe the wrong parent is being passed to the removeChild function.

// For reference

    DefaultDomRenderer2.prototype.removeChild = function (parent, oldChild) {
        if (parent) {
            parent.removeChild(oldChild);
        }
    };

When I drag and drop inside a single list, oldChild.parentElement is the same as parent. When I drag and drop across lists, oldChild.parentElement is the container the item originated from and parent is the list I dropped it in. Not sure if that helps. It is failing on the first removeChild attempt when I drop across lists.


Here are visual before and after states of the lists that have been dragged across (It's mid development so, please forgive the unrefined UI).

Initial State

image

After State

image

In the After State, the item "Purpose, Values, Vision, Mission" is sort of duplicated but I believe it is a result of the error on drop. The top item is the real one. the bottom item when dragged and dropped creates empty items as seen below. It happens as many times as I drag and drop a "fake" item

image

ScottMGerstl avatar Feb 27 '19 16:02 ScottMGerstl

hi @ScottMGerstl

thank you for investigation. This is a weird behaviour... Is there a public repository the issue can be reproduced? It's hard to say anything without trying and failing :)

smnbbrv avatar Feb 28 '19 09:02 smnbbrv