Description
TLDR: reporting a bug and have a proposed fix, but not sure how much of an edge case this is or not.
When inserting a component into the slot, if the component has multiple root nodes in it such that it doesn't get detected by the check at line 26 in renderHelper.js, then the __draggable_context
property that gets added to the DOM element is added to the wrong node. When this happens, you cannot drag/drop correctly and get a throw error in the onDragStart
event: Cannot read properties of null (reading 'element')
codesandbox.io link
https://codesandbox.io/s/bold-pike-xrddv
Step by step scenario
Take something like this for App.vue
<table class="table table-striped">
<thead class="thead-dark">
<tr>
<th scope="col">Id</th>
<th scope="col">First Name</th>
<th scope="col">Last Name</th>
<th scope="col">Age</th>
</tr>
</thead>
<draggable v-model="list" tag="tbody" item-key="name">
<template #item="{ element }">
<example :element="element" />
</template>
</draggable>
</table>
and something like this for the Example.vue component:
<template>
<!-- random comment here -->
<tr :data-draggable="true">
<td>{{ element.id }}</td>
<td>{{ element.firstName }}</td>
<td>{{ element.lastName }}</td>
<td>{{ element.age }}</td>
</tr>
</template>
The comment line in Example.vue causes the vue compiler to detect multiple root nodes and thus, it surrounds all nodes with an empty text element. So, something like this:
As you can see, this is the childNodes
of the tbody
element. Here is the first text
node expanded and you can see the __draggable_context
property has been added to it.
So, when the onDragStart
event happens and it calls getContext
here, it fails to find the element correctly and throws the exception.
Actual Solution
Here is a proposed solution at codesandbox.io: https://codesandbox.io/s/pedantic-joliot-drjm3.
Please note, for this sandbox, I ended up setting up VueDraggable locally within the sandbox as a subfolder. I do have a PR ready that is the same fix that's contained in this sandbox. You can see the proposed fix in the componentStructure.js file at the very beginning. I'm also manually assigning the data-draggable
attribute where I need it as that's another symptom of the overall issue and that is that this attribute is not being assigned correctly.
const getHtmlElementFromNode = (node) => {
const nodeType = node.el.nodeType;
const parentElementCount = node.el.parentElement.childElementCount;
const parentChildNodesCount = node.el.parentElement.childNodes.length;
// we might have a text node fragment that Vue3 uses when the parent element
// has multiple root nodes.
// so we test for it here and if so, we return the next sibling as we don't want the
// __draggable_context added to what is essentially an empty, unreachable page element
if (nodeType === 3 && parentElementCount !== parentChildNodesCount) {
node = node.el.nextElementSibling;
}
if (node.el) return node.el;
else return node; // then we probably are returning the nextSibling.
};
What I attempt to do is detect these empty text nodes that Vue 3 is inserting and based on that, I go find the next Sibling and return it instead of the empty text node.
All the above said, I'm not 100% sure this is the best way to go about addressing this scenario. For me specifically, this situation came up with a 3rd party table library where I wanted to be able to drag/sort rows and it wasn't a comment in the template
section, but it's use of multiple components via renderless functions and jsx elements to render a tr
that Vue thinks has multiple root nodes. My use of the comment in the template
section was my attempt to recreate the scenario.
Again, not sure I've gone about solving this the "best" way or if there is some other way that would be a better long term fix for this issue.