Hello Community,
I didn’t hear back on this post: Au 2.0RC: Guidance requested: Revisiting the Enhance API in a new context .
But I managed to figure it out! I had the following design goals:
- SortableJS’s drag container must host a full Aurelia component
- My
TsiTreeViewandTsiListcomponents must be the first to receive this feature TsiTreeViewandTsiListmust contain default drag containers (that are also full Aurelia components)- Custom drag containers should be able to be specified in the markup in the owning view, harvested with
processContent, and enhanced with Aurelia’s Enhance API
Before getting to the code, here is the net result (note the changing yellow text):

I included a contrivance for the sake of this post. You can see the yellow text in the container is responding to changes in the drag container’s position, reflecting the parent node under which the item will be dropped.
I achieve that this way:
tsi-tree-view-default-drag-container.js
import { bindable, customElement, resolve } from 'aurelia';
import { TsiMessageBus } from '../../../library/TsiMessageBus';
@customElement('tsi-tree-view-default-drag-container')
export class TsiTreeViewDefaultDragContainer {
@bindable node = undefined;
@bindable count = 0;
@bindable textExpr = 'description';
constructor() {
this.messageBus = resolve(TsiMessageBus);
this.parentNode = null;
}
attaching() {
this.messageBus.subscribe('drag-container', 'changed', this.onMessageChanged).context(this);
}
onMessageChanged(payload) {
// This is leveraged in the drag container's view
this.parentNode = payload.node;
}
unbinding() {
this.messageBus.unsubscribe(this);
}
}
I resolve a TsiMessageBus, which is a thin wrapper around PostalJS, a full client-side message bus. The container subscribes to the changed topic on the drag-container channel (a named channel since there can be only one active drag container), which is a promotion of SortableJS’s event called changed.
The publishing of this message is done by TsiTreeView:
tsi-tree-view.js
...
onDragChanged(e) {
const evt = e.detail.sortableEvent;
const cache = this.tvmgr.nodeCache;
const node = cache.get(evt.to.dataset.listId) ?? cache.get('root');
const messagePayload = { node };
this.messageBus.publish('drag-container', 'changed', messagePayload);
}
...
MULTIPLE DI CONTAINERS AND DISCUSSION
Here I borrow from the knowledge I acquired in this thread: Conclusion: The need to dispose of an enhanced view interferes with some third-party integrations , which is in the context of wrapping DevExtreme’s widgets.
The challenge lay in recognizing the need for two different DI containers. Consider this from
TsiTreeView’s constructor:
tsi-tree-view.js
...
this.diContainer = resolve(IContainer);
this.$controller = resolve(IController);
this.$context = resolve(IHydrationContext);
...
// Two different containers: one for the default drag container, and one for a custom container defined in markup at the owning view
this.diContainer.register(TsiTreeViewDefaultDragContainer);
this.scopedDiContainer = this.$context.controller.container.createChild({ inheritParentResources: true });
Here’s the full relevant bits of the constructor, with a static processContent method above:
tsi-tree-view.js
import { TsiTreeViewDefaultDragContainer } from './tsi-tree-view-default-drag-container';
...
@processContent()
static processContent(node, _, instructionData) {
// Custom drag container markup is harvested (if present)
// Is picked up in the ctor
instructionData.dragContainer = Array.from(node.children).find((_n) => _n.tagName === 'TSI-DRAG-CONTAINER');
if (instructionData.dragContainer) {
instructionData.dragContainer.remove();
}
return true; // Continue with normal compilation
}
constructor() {
...
this.diContainer = resolve(IContainer);
this.$controller = resolve(IController);
this.$context = resolve(IHydrationContext);
this.$dragContainer = undefined;
this.$dragContainerHost = undefined;
this.dragContainerView = undefined;
this.dropInsideTarget = undefined;
this.draggedNode = undefined;
...
const { data } = resolve(IInstruction);
// Null if there is no custom container
this.dragContainerTemplate = data?.dragContainer;
this.diContainer.register(TsiTreeViewDefaultDragContainer);
this.scopedDiContainer = this.$context.controller.container.createChild({ inheritParentResources: true });
...
}
The default drag container, TsiTreeViewDefaultDragContainer, is defined in the context of the TsiTreeView component, whereas a custom drag container is defined in the context of the owning view.
From the start drag event handler:
tsi-tree-view.js
onStartDrag(e) {
...
// Get a hold of the SortableJS drag container in the DOM
this.$dragContainer = document.querySelectorAll('.tsi-sortable-drag-class')?.[0];
if (!this.$dragContainer) return;
// Clear SortableJS's drag container's content
this.$dragContainer.innerHTML = '';
// Contextual switch between the two containers
if (this.useCustomDragContainer) {
this.$dragContainerHost = this.dragContainerTemplate.cloneNode(true);
this.$dragContainer.appendChild(this.$dragContainerHost);
} else {
this.$dragContainerHost = document.createElement('div');
// prettier-ignore
this.$dragContainerHost.innerHTML =
`<tsi-tree-view-default-drag-container
node.bind="node"
text-expr.bind="textExpr"
count.bind="count">
</tsi-tree-view-default-drag-container>`;
this.$dragContainer.appendChild(this.$dragContainerHost);
}
...
}
In the animated GIF at the beginning, I’m using the default drag container for TsiTreeView.
Here’s how a custom drag container is specified (I have a custom container in another application component that uses TsiList, which works the same way as it does for TsiTreeView):
my-application-component.html
...
<tsi-list use-custom-drag-container.bind="true" ...>
<my-list-item ...></my-list-item>
<tsi-drag-container>
<custom-drag-container node.bind="node"></custom-drag-container>
</tsi-drag-container>
</tsi-list>
...
The tsi-drag-container tag is just a marker tag (not an actual component) that indicates markup for a custom drag container, which is actually a full-blown Aurelia component in this case. It is harvested and removed in processContent, and then enhanced with Aurelia’s Enhance API.
Using marker tags is something that Rob Eisenberg covered in his Vimeo videos in the context of a lesson on processContent.