# Au 2.0RC: Mismatch error in the number of views of a repeat.for after external DOM manipulation (SortableJS)

**URL:** <https://discourse.aurelia.io/t/au-2-0rc-mismatch-error-in-the-number-of-views-of-a-repeat-for-after-external-dom-manipulation-sortablejs/6592>\
**Category:** Help Requests\
**Created:** [February 9, 2026, 9:11pm UTC](https://discourse.aurelia.io/t/au-2-0rc-mismatch-error-in-the-number-of-views-of-a-repeat-for-after-external-dom-manipulation-sortablejs/6592 "2026-02-09T21:11:37Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![estaylorco](https://yyz1.discourse-cdn.com/flex027/user_avatar/discourse.aurelia.io/estaylorco/32/1887_2.png) [@estaylorco](https://discourse.aurelia.io/u/estaylorco)\
**Post date:** [February 9, 2026, 9:11pm UTC](https://discourse.aurelia.io/t/au-2-0rc-mismatch-error-in-the-number-of-views-of-a-repeat-for-after-external-dom-manipulation-sortablejs/6592/1 "2026-02-09T21:11:37Z")

</div>

Hello,

In my Au 1.0 app, I have my own implementation of drag-and-drop. In migrating to Au 2.0, I have decided to utilize SortableJS (vanilla).

It was easy enough to get up and running, and is actually working just fine. But not quite as I would like.

**As you may know, SortableJS does, in fact, manipulate the DOM.**

My list component takes in items and wraps them in a list node. Here is the full template of the list:

```xml
<import from="./tsi-list-columns"></import>
<import from="./tsi-list-item-node"></import>

<div
  ref="$list"
  class="tsi-list-items"
  select-all.trigger="onSelectAllChanged($event)"
  node-selected.trigger="onNodeSelectionChanged($event)"
>
  <tsi-list-columns
    columns.bind="columns"
    config.bind="config"
    select-all.bind="selectAll"
    column-clicked.trigger="onColumnClicked($event)"
  ></tsi-list-columns>

  <template repeat.for="node of nodes & signal:'refresh-list'; key.bind: node.id">
    <tsi-list-item-node
      class="tsi-list-item-node-wrapper"
      columns.bind="columns"
      selected.bind="selected"
      node.bind="node"
      config.bind="config"
      data-id.bind="node.item.id"
    ></tsi-list-item-node>
  </template>
</div>

```

If I do the following in the `onEnd` handler of Sortable, I get an Au error indicating a mismatch between the number of views and what was expected:

```javascript
this.sortable = new Sortable(this.$list, {
  ...
  onEnd: (e) => {
    const node = this.nodes.splice(e.oldIndex - 1, 1)[0];
    this.nodes.splice(e.newIndex - 1, 0, node);
  },
});

```

But if do the splicing on the underlying items, and then re-wrap them, everything is fine:

```javascript
this.sortable = new Sortable(this.$list, {
  ...
  onEnd: (e) => {
    const item = this.items.splice(e.oldIndex - 1, 1)[0];
    this.items.splice(e.newIndex - 1, 0, item);

    this._createNodes(this._defaultNodeVisitor);
  },
});

```

I would rather not have to re-wrap the items. That would be less than ideal on a larger list.

What I don’t understand is that I’m splicing and reinserting all in the same tick, so the number of underlying items is changing.

Even when I wrap the `onEnd` code in a `queueAsyncTask` or a `setTimeout`, I still have problems.

**What is the guidance here for something like this?**

---

<div class="post-metadata">

**Author:** ![bigopon](https://yyz1.discourse-cdn.com/flex027/user_avatar/discourse.aurelia.io/bigopon/32/18_2.png) [@bigopon](https://discourse.aurelia.io/u/bigopon)\
**Post date:** [February 10, 2026, 12:34am UTC](https://discourse.aurelia.io/t/au-2-0rc-mismatch-error-in-the-number-of-views-of-a-repeat-for-after-external-dom-manipulation-sortablejs/6592/2 "2026-02-10T00:34:13Z")

</div>

For

> [@estaylorco](#):
>
> ```auto
> this.sortable = new Sortable(this.$list, {
> ...
> onEnd: (e) => {
> const node = this.nodes.splice(e.oldIndex - 1, 1)[0];
> this.nodes.splice(e.newIndex - 1, 0, node);
> },
> });
> 
> ```

If you just do this, then you’ll have mismatching between underlying model and the UI, i don’t think that is desired, but it shouldn’t cause any error, at least immediately like mentioned.

—

For

> [@estaylorco](#):
>
> ```auto
> this.sortable = new Sortable(this.$list, {
> ...
> onEnd: (e) => {
> const item = this.items.splice(e.oldIndex - 1, 1)[0];
> this.items.splice(e.newIndex - 1, 0, item);
> 
> this._createNodes(this._defaultNodeVisitor);
> },
> });
> 
> ```

Shouldn’t you be able to slice and dice the nodes the same way you did with the items? Maybe add a batch in so that it does the processing only once

```typescript
import { batch } from 'aurelia'

this.sortable = new Sortable(this.$list, {
  ...
  onEnd: (e) => {
    batch(() => {
      const item = this.items.splice(e.oldIndex - 1, 1)[0];
      this.items.splice(e.newIndex - 1, 0, item);

      const node = this.nodes.splice(e.oldIndex - 1, 1);
      this.nodes.splice(e.newIndex - 1, 0, node);
    });
  },
});

```

---

<div class="post-metadata">

**Author:** ![estaylorco](https://yyz1.discourse-cdn.com/flex027/user_avatar/discourse.aurelia.io/estaylorco/32/1887_2.png) [@estaylorco](https://discourse.aurelia.io/u/estaylorco)\
**Post date:** [February 10, 2026, 12:56am UTC](https://discourse.aurelia.io/t/au-2-0rc-mismatch-error-in-the-number-of-views-of-a-repeat-for-after-external-dom-manipulation-sortablejs/6592/3 "2026-02-10T00:56:59Z")

</div>

Unfortunately, your suggestions don’t solve the problem. Your `batch` code doesn’t agree with the logic of my application and it leads to other problems.

Working directly upon the nodes, but in a `batch`, does work. But eventually, sorting the list fails because the DOM and the model are out of sync. This eliminates the error, but fails logically eventually (my list eventually stops sorting when I click its column headings):

```javascript
const node = this.nodes.splice(e.oldIndex - 1, 1)[0];
this.nodes.splice(e.newIndex - 1, 0, node);

```

This is the only approach that works (illustrated in my original post):

```javascript
this.sortable = new Sortable(this.$list, {
  ...
  onEnd: (e) => {
    const item = this.items.splice(e.oldIndex - 1, 1)[0];
    this.items.splice(e.newIndex - 1, 0, item);

    this._createNodes(this._defaultNodeVisitor);
  },
});

```

Having to recreate the nodes is the clue here, I think: The DOM and the model are out of sync without this line, which is what causes my list to eventually stop sorting.

---

<div class="post-metadata">

**Author:** ![bigopon](https://yyz1.discourse-cdn.com/flex027/user_avatar/discourse.aurelia.io/bigopon/32/18_2.png) [@bigopon](https://discourse.aurelia.io/u/bigopon)\
**Post date:** [February 10, 2026, 3:53am UTC](https://discourse.aurelia.io/t/au-2-0rc-mismatch-error-in-the-number-of-views-of-a-repeat-for-after-external-dom-manipulation-sortablejs/6592/4 "2026-02-10T03:53:24Z")

</div>

then maybe takeover the dom as well, it seems simple enough?

```typescript
import { batch } from 'aurelia'

this.sortable = new Sortable(this.$list, {
  ...
  onEnd: (e) => {
    batch(() => {
      const item = this.items.splice(e.oldIndex - 1, 1)[0];
      this.items.splice(e.newIndex - 1, 0, item);

      const node = this.nodes.splice(e.oldIndex - 1, 1);
      this.nodes.splice(e.newIndex - 1, 0, node);

      const el = this.listWrapperEl.children[e.oldIndex - 1];
      const newSibling = this.listWrapperEl[e.newIndex - 1];
      this.listWrapperEl.insertBefore(el, newSibling);
    });
  },
});

```

For the reason changing the model stops the sort from working, maybe paste the full config of `new Sortable(` call? it seems weird that a dom only lib acts up when irrelevant model changes

---

<div class="post-metadata">

**Author:** ![estaylorco](https://yyz1.discourse-cdn.com/flex027/user_avatar/discourse.aurelia.io/estaylorco/32/1887_2.png) [@estaylorco](https://discourse.aurelia.io/u/estaylorco)\
**Post date:** [February 11, 2026, 2:08am UTC](https://discourse.aurelia.io/t/au-2-0rc-mismatch-error-in-the-number-of-views-of-a-repeat-for-after-external-dom-manipulation-sortablejs/6592/5 "2026-02-11T02:08:34Z")

</div>

Hello @bigopon,

I finally wrangled this and now have a working Aurelia 2.0 wrapper around SortableJS.

I must have missed `batch` in the documentation. But that feature is pretty cool, I have to say! And, yes, it was necessary.

This works just fine now (I no longer have to work with the underlying items and then re-wrap the items into nodes):

```javascript
onDragEnd(e) {
  const oldIndex = e.detail.evt.oldIndex;
  const newIndex = e.detail.evt.newIndex;

  batch(() => {
    const node = this.nodes.splice(oldIndex, 1)[0];
    this.nodes.splice(newIndex, 0, node);
  });

  ...
}

```

So, for the benefit of the community, should someone want to utilize vanilla SortableJS in Aurelia, let me share a few observations and pain points:

- The immediate child of a sortable instance should be a hard tag (`div`, `ul`, etc.), not the sortable items themselves;
- Placing `repeat.for` on a `template` tag appears to be a problem, not so much with SortableJS, but in terms of destabilizing the DOM downstream;
- Aurelia’s `batch` feature is absolutely essential for the the two splicing operations you will perform to manifest the DOM changes in the underling items on the model or viewModel;
- The `draggable` selector should represent a hard tag above any custom component you might have representing your items (in other words, not a class or an id on the custom component tag, or even the custom component tag itself);
- The `handle` selector shouldn’t be the same as the `draggable` selector (it appears that this causes confusion);
- If you want animation, don’t forget to set `animation` on the sortable instance (the SortableJS documentation will lead you to believe that it defaults to 150, but it defaults to 0 instead);
- Don’t use the default `ghostClass` or `dragClass`; define your own.

With respect to the first two bullets above, you should have something like this (I provide my wrapper below this code snippet):

```xml
<tsi-sortable
  direction="vertical"
  draggable=".tsi-list-item-node-frame"
  handle=".tsi-list-item-node"
  ghost-class="tsi-sortable-ghost-class"
  drag-class="tsi-sortable-drag-class"
  force-ballback.bind="true"
  fallback-on-body.bind="true"
  on-end.trigger="onDragEnd($event)"
>
  <div> <!-- <-- this div is necessary -->
    <div repeat.for="node of nodes; key.bind: node.id" class="tsi-list-item-node-frame">
      <tsi-list-item-node
        class="tsi-list-item-node-wrapper"
        columns.bind="columns"
        selected.bind="selected"
        node.bind="node"
        config.bind="config"
        data-id.bind="node.item.id"
      ></tsi-list-item-node>
    </div>
  </div>
</tsi-sortable>

```

I have thrown together an Aurelia wrapper around SortableJS. The below is a first pass at this. There are two files: **tsi-sortable.js** and **tsi-sortable.html**.

**tsi-sortable.html:**

```xml
<au-slot></au-slot>

```

**tsi-sortable.js:**

```javascript
import { bindable, customElement, INode, resolve } from 'aurelia';

import { TsiCustomEventDispatcher } from 'common/library/TsiCustomEventDispatcher';

import { Sortable } from 'sortablejs';

@customElement('tsi-sortable')
export class TsiSortable {
  @bindable animation = 150;
  @bindable chosenClass = 'sortable-chosen';
  @bindable dataIdAttr = 'data-id';
  @bindable delay = 0;
  @bindable delayOnTouchOnly = false;
  @bindable direction = 'horizontal';
  @bindable disabled = false;
  @bindable dragClass = 'sortable-drag';
  @bindable draggable = null;
  @bindable dragoverBubble = false;
  @bindable easing = 'ease-in-out';
  @bindable emptyInsertThreshold = 5;
  @bindable fallbackClass = 'sortable-fallback';
  @bindable fallbackOnBody = false;
  @bindable fallbackTolerance = 0;
  @bindable filter = null;
  @bindable forceFallback = false;
  @bindable ghostClass = 'sortable-ghost';
  @bindable group = null;
  @bindable handle = null;
  @bindable invertedSwapThreshold = 1;
  @bindable invertSwap = false;
  @bindable preventOnFilter = true;
  @bindable removeCloneOnHide = true;
  @bindable revertOnSpill = true;
  @bindable snapBackOnAnimation = true;
  @bindable sort = true;
  @bindable store = null;
  @bindable swapThreshold = 1;
  @bindable touchStartThreshold = 0;

  @bindable target = undefined;
  @bindable includeOrderedArray = false;

  constructor() {
    this.$el = resolve(INode);
    this.eventDispatcher = resolve(TsiCustomEventDispatcher);
    this.sortable = undefined;

    this._excludedOptions = ['target', 'includeOrderedArray'];
    this._toArrayEvents = [
      'drag-ended',
      'item-unchosen',
      'item-added',
      'item-updated',
      'sort-changed',
      'item-removed',
      'item-changed',
    ];
  }

  attached() {
    // Query for the target if a selector is provided; otherwise, assume the first child
    const $target = this.target ? this.$el.querySelectorAll(this.target)?.[0] : this.$el.children?.[0];

    if (!$target) {
      throw new Error('No DOM element was found either as the first child or at the selector provided');
    }

    this.sortable = new Sortable($target, {
      animation: this.animation,
      chosenClass: this.chosenClass,
      dataIdAttr: this.dataIdAttr,
      delay: this.delay,
      delayOnTouchOnly: this.delayOnTouchOnly,
      direction: this.direction,
      disabled: this.disabled,
      dragClass: this.dragClass,
      draggable: this.draggable,
      dragoverBubble: this.dragoverBubble,
      easing: this.easing,
      emptyInsertThreshold: this.emptyInsertThreshold,
      fallbackClass: this.fallbackClass,
      fallbackOnBody: this.fallbackOnBody,
      fallbackTolerance: this.fallbackTolerance,
      filter: this.filter,
      forceFallback: this.forceFallback,
      ghostClass: this.ghostClass,
      group: this.group,
      handle: this.handle,
      invertedSwapThreshold: this.invertedSwapThreshold,
      invertSwap: this.invertSwap,
      preventOnFilter: this.preventOnFilter,
      removeCloneOnHide: this.removeCloneOnHide,
      revertOnSpill: this.revertOnSpill,
      snapBackOnAnimation: this.snapBackOnAnimation,
      sort: this.sort,
      store: this.store,
      swapThreshold: this.swapThreshold,
      touchStartThreshold: this.touchStartThreshold,
      setData: (dataTransfer, dragEl) => this.setData(dataTransfer, dragEl),
      onChoose: (e) => this.dispatchMappedEvent('item-chosen', e),
      onUnchoose: (e) => this.dispatchMappedEvent('item-unchosen', e),
      onStart: (e) => this.dispatchMappedEvent('drag-started', e),
      onEnd: (e) => this.dispatchMappedEvent('drag-ended', e),
      onAdd: (e) => this.dispatchMappedEvent('item-added', e),
      onUpdate: (e) => this.dispatchMappedEvent('sort-updated', e),
      onSort: (e) => this.dispatchMappedEvent('sort-changed', e),
      onRemove: (e) => this.dispatchMappedEvent('item-removed', e),
      onFilter: (e) => this.dispatchMappedEvent('item-filtered', e),
      onMove: (e) => this.dispatchMappedEvent('item-moved', e),
      onClone: (e) => this.dispatchMappedEvent('item-cloned', e),
      onChange: (e) => this.dispatchMappedEvent('item-changed', e),
    });
  }

  detaching() {
    this.sortable.destroy();
    this.sortable = undefined;
  }

  // eslint-disable-next-line no-unused-vars
  propertyChanged(option, newValue, oldValue) {
    if (!this.sortable || this._excludedOptions.includes(option)) return;
    this.sortable.option(option, newValue);
  }

  setData(dataTransfer, dragEl) {
    this.eventDispatcher.dispatch(this.$el, 'setData', { dataTransfer, dragEl });
  }

  dispatchMappedEvent(eventName, e) {
    let options = {};

    if (this.includeOrderedArray && this._toArrayEvents.includes(eventName)) {
      options = {
        domOrderedArray: this.sortable.toArray(),
      };
    }

    e.stopPropagation();
    this.eventDispatcher.dispatch(this.$el, eventName, { evt: e, ...options });
  }
}

```

Animated GIF of sorting working on my own custom list component:

![SortableJS Wrapper](https://canada1.discourse-cdn.com/flex027/uploads/aurelia/original/2X/2/2a17baad1ccbfa15559f450aa99918a5ce39cf31.gif)
