SortableJS / Sortable

Reorderable drag-and-drop lists for modern browsers and touch devices. No jQuery or framework required.
https://sortablejs.github.io/Sortable/
MIT License
29.61k stars 3.7k forks source link

Prevent Lib from manipulating the DOM (Vue.js) #546

Closed PhillippOhlandt closed 9 years ago

PhillippOhlandt commented 9 years ago

Is there a way to prevent the lib from manipulating the DOM? I use it in a VueJS component and adjust my data array in the sortablejs onSort function. My list will be rendered based on my data array so it's a little but dump to do with with this lib too.

And I have some problems with the manipulated DOM by this lib. See more here: https://github.com/yyx990803/vue/issues/1272

RubaXa commented 9 years ago

What does it mean to prevent? Sortable — based on DOM manipulation elements.

PhillippOhlandt commented 9 years ago

I just need the drag and drop feature and the info which items was changed. VueJS will do the DOM manipulation then.

RubaXa commented 9 years ago

Then you should not be used Sortable. P.S. https://github.com/RubaXa/Sortable/issues/176

onefriendaday commented 9 years ago

You can use Sortable with vuejs. Remove the dragged element and rebuild your data array:

Sortable.create(this.el, {
    animation: 150,
    handle: '.uk-nestable-handle',
    onEnd: function (e) {
        // Remove the dragged item from dom
        e.item.remove()

        // Clone container
        var clone = _a.clone(self._scope.model[self._scope.$key])
        self._scope.model[self._scope.$key] = []

        // Move to new position
        var swappedEl = clone.splice(e.oldIndex, 1)[0]
        clone.splice(e.newIndex, 0, swappedEl)

        // On next Tick update with clone
        self._scope.$nextTick(function() {
            self._scope.model[self._scope.$key] = clone
        })
    }
})
ghost commented 7 years ago

Is there any clean way to achieve this? There is no reason to not have a flag to disable DOM manipulation...

juanbermudez commented 7 years ago

@mgrandl Just in case you ever run into this I had a similar situation with Vue.js and Vue-Sortable.js Seems the component became disconnected.

My temp solution was creating an action (vuex) or you can do it against the local state for a component that forced an updated on the next tick, similar to what @onefriendaday shared. That refresh method runs on the onEnd event from the Sortable, it clones the last element in the list of items, then pop the last one and push the clone in. It is a bit messy but Vue-Sortable might need an updated to handle this properly.

miwucs commented 2 years ago

I ran into the same issue when using SortableJS with Lit. This is my solution, which should work for other frameworks too. Note it's in Typescript but you can convert it to javascript easily (just remove the type information and the '!' that tell typescript a variable is not null).

let childNodes: ChildNode[] = [];
Sortable.create(myContainer, {
  onStart: (e) => {
    const node = e.item as Node;
    // Remember the list of child nodes when drag started.
    childNodes =
      Array.prototype.slice.call(node.parentNode!.childNodes);
    // Filter out the 'sortable-fallback' element used on mobile/old browsers.
    childNodes = childNodes.filter(
      (node) =>
        node.nodeType != Node.ELEMENT_NODE ||
        !(node as HTMLElement).classList.contains('sortable-fallback')
    );
  },
  onEnd: (e) => {
    // Undo DOM changes by re-adding all children in their original order.
    const node = e.item as Node;
    const parentNode = node.parentNode!;
    for (const childNode of childNodes) {
      parentNode.appendChild(childNode);
    }
    if (e.oldIndex == e.newIndex) return;
    // Then move the element using your own logic.
    // (assuming this.myList is the array being sorted).
    const element = this.myList.splice(e.oldIndex, 1)[0];
    this.myList.splice(e.newIndex, 0, element);
    // Ask for component redraw (if needed).
    // This is how it's done in Lit. Adapt to your framework.
    this.requestUpdate();
  },
});
enkelmedia commented 1 year ago

@miwucs you saved my night =D I've spent several hours fiddling with this, my use case is simular but I need to drag between different containers. I was on the same track with "resetting" the DOM after the from but your example help me the last bit =D

Figured I'll share my code here if anyone needs to drag between containers.

let fromChildNodes: ChildNode[] = [];
let toChildNodes : ChildNode[] = [];

this.sortable = Sortable.create(rowsContainer,{
    onStart: (e) => {
        const node = e.item as Node;
        fromChildNodes = [];
        // Remember the list of child nodes when drag started.
        fromChildNodes = Array.prototype.slice.call(node.parentNode!.childNodes);
        // Filter out the 'sortable-fallback' element used on mobile/old browsers.
        fromChildNodes = fromChildNodes.filter(
            (node) =>
            node.nodeType != Node.ELEMENT_NODE ||
            !(node as HTMLElement).classList.contains('sortable-fallback')
        );
        },
        onMove: function (evt, originalEvent) {
            // Move is called when the drag enters a new "to"-container.
            // here we're storing the children on the two container to be
            // used to restore it if there is a drop.
            toChildNodes = [];
            toChildNodes = Array.prototype.slice.call(evt.to.childNodes);
            // Filter out the 'sortable-fallback' element used on mobile/old browsers.
            toChildNodes = toChildNodes.filter(
                (node) =>
                node.nodeType != Node.ELEMENT_NODE ||
                !(node as HTMLElement).classList.contains('sortable-fallback')
        );
    },
    onEnd : (e)=> {

        // Undo DOM changes by re-adding all children in their original order.

        for(const childNode of fromChildNodes){
            e.from.appendChild(childNode);
        }
        for (const childNode of toChildNodes) {
            e.to.appendChild(childNode);
        }

        const element = this.myList.splice(e.oldIndex, 1)[0];
        this.myList.splice(e.newIndex, 0, element);
        // Ask for component redraw (if needed).
        // This is how it's done in Lit. Adapt to your framework.
        this.requestUpdate();

    },

});
ChristopherJohnson25 commented 1 year ago

Is anyone handling a similar case here, but with Vue/Vuex?

sandeepps commented 1 year ago

@miwucs Thank you so much.. I had the same issue and am able to fix the issue with your approach.

Ben-Avrahami commented 1 year ago

i found a good solution here https://lightrun.com/answers/sortablejs-sortable-prevent-lib-from-manipulating-the-dom-vuejs event.item.remove() then the change to the dom by sortable will be reverted and the framework can do the rerendering

thomasklein1982 commented 11 months ago

It may be not the best answer, but it's very simple: I use sortablejs-vue3 which provides a very low-level and lightweight wrapper for sortable. I listen to the changes and make the corresponding changes in my data and then I simply force Vue to recreate the sortable-component after each change:

Template:

<Sortable
            v-if="renderSortable"
            :list="your-list-with-data-goes-here"
            item-key="id"
            :options="{
              group: {
                name: 'components',
                put: true
              },
              handle: '.handle',
              'ghost-class': 'drag-ghost-component',
            }"
            @add="handleAdd"
            @sort="handleSort"
            @clone="cloneItem"
            @remove="handleRemove"
>
</Sortable>

Methods:

    cloneItem(event){
      //make a copy of your data and append it to the clone-attribute
      let copy=JSON.parse(JSON.stringify(this.your-list-goes-here[event.oldIndex]));
      event.clone.listData=copy;
    },
    async updateSortable(){
      //brutish way to force complete recreation of the sortable-component:
      this.renderSortable=false;
      await this.$nextTick();
      this.renderSortable=true;
    },
    handleRemove(ev){
      this.component.components.splice(ev.oldIndex,1);
      this.updateSortable();
    },
    handleAdd(ev){
      //insert the listData you appended via clone-method:
      this.component.components.splice(ev.newIndex,0,ev.clone.listData);
      this.updateSortable();
    },
    handleSort(ev){
      //prevent cases that are already handled by add or remove:
      if(!ev.item.parentElement || ev.from!==ev.to) return;
      //swap the two elements:
      let comp1=this.your-list-goes-here[ev.oldindex];
      let comp2=this.your-list-goes-here[ev.newIndex];
      this.your-list-goes-here[ev.oldIndex]=comp2;
      this.your-list-goes-here[ev.newIndex]=comp1;
      this.updateSortable();
    },
dotnetprofessional commented 9 months ago

Adding my simple solution, to see if I'm missing something. You'll need to add this to each of the onAdd/onUpdate events, possibly others if you're using them. So create a func that takes the SortableEvent:

            // cancel the UI update so <framework> will take care of it
            evt.item.remove();
            if (evt.oldIndex !== undefined) {
                evt.from.insertBefore(evt.item, evt.from.children[evt.oldIndex]);
            }
alihardan commented 9 months ago

@dotnetprofessional, dude you saved my day. Your answer is the only one worked for me. ❤️ in my case, putting it into onEnd was enough.

Prinzhorn commented 3 months ago

You can skip the evt.item.remove(), insertBefore will move the item to the new place:

If the given node already exists in the document, insertBefore() moves it from its current position to the new position. (That is, it will automatically be removed from its existing parent before appending it to the specified new parent.)

https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore