Hello Everyone,
(For this post tsi-dx-button and my-dx-button are synonymous).
Following on from a topic I posted at the beginning of the year (Migrating Au1's #enhance to Au2's #enhance), I was finally able to get Aurelia 2.0 and Devextreme to play well in terms of templating (so that a Devextreme widget hosts one or more dx-templates as Aurelia-enhanced HTML fragments). I had this working flawlessly in Aurelia 1.0 with no memory leaks, but have had trouble getting to that same level in Aurelia 2.0.
NOTE: I’m not referring to simply providing a render function to a template prop (such as
itemTemplate,fieldTemplate,rowTemplate, etc.). I’m referring to providing templates in the markup (HTML) withdx-templatetags, harvesting those fragments, enhancing them, removing them from the DOM, and then supplying them to a Devextreme widget through theintegrationOptionsprop (according to the Devextreme documentation).
BACKGROUND
Consider the following example (my-dx-button.html):
<my-dx-button text.bind="userValue" click.trigger="handleButtonClick()">
<dx-template name="content">
<div class="tsi-test-class">Button: ${data.text}</div>
</dx-template>
</my-dx-button>
The my-dx-button component wraps Devextreme’s Button widget. Button is one of those widgets that has a *template prop, the default name of which is content. There can be an unlimited number of dx-templates, all with different names, and either targeting different props of the widget, or offering polymorphic options where one will be selected at runtime (think of a template selector pattern).
FIRST PASS
This first pass doesn’t address events yet, and it involves only the Button’s text prop. Also, I’m not at a point where I can begin to factor all of this out into a service, as so forth. I would appreciate commentary on my approach here, which is essentially the same as the approach I took with Aurelia 1.0.
NOTE: Devextreme calls the
renderfunction associated with the template in thetemplateDicteach time data changes, which precipitates an Aurelia-based re-enhancement of the HTML.
Here’s the entirety of my-dx-button.js, with code comments:
import Aurelia, { bindable, customElement, inject, processContent, resolve } from 'aurelia';
import { IInstruction } from '@aurelia/template-compiler';
import Button from 'devextreme/ui/button';
// This is my own model of the dx-template
import { DxTemplateModel } from './DxTemplateModel';
// processContent callback
function harvestDxTemplates(node, _, instructionData) {
instructionData.templates = node.querySelectorAll('dx-template');
// Now that we've harvested the dx-templates from the DOM, we don't need them anymore; we must do this here in the processContent callback
instructionData.templates.forEach((_template) => {
node.removeChild(_template);
});
// Signals the absence of content when we're done
// Equivalent to processContent(false) in Aurelia 1.0
return false;
}
@customElement({
name: 'tsi-dx-button',
template: null, // Equivalent to @noView in Aurelia 1.0
})
@processContent(harvestDxTemplates)
@inject(Element, CustomEventDispatcher)
export class MyDxButton {
// Curated bindable for this example
@bindable text = 'Default Text';
constructor($el, eventDispatcher) {
this.$el = $el;
this.eventDispatcher = eventDispatcher;
this.widgetHost;
this.widgetInstance;
this.templateDict = {};
this.enhancedTemplates = [];
const { data } = resolve(IInstruction);
this.templates = data.templates; // Pushed into the instructionData from harvestDxTemplates above
this.templates.forEach((_template) => {
this.ingestTemplate(_template);
});
binding() {
// With Devextreme, NEVER host right off of Element, which is this.$el above; we host one additional level down instead
this.widgetHost = document.createElement('div');
this.$el.appendChild(this.widgetHost);
// According to the Devextreme documentation
const integrationOptions = {
templates: this.templateDict, // Built up in the constructor where we iterate over the harvested templates and ingest them
};
// Mount Devextreme component on the widget DOM host
this.widgetInstance = new Button(this.widgetHost, {
template: 'content',
text: this.text,
type: this.type ?? 'default',
integrationOptions: integrationOptions, // Devextreme's integration point
});
}
unbinding() {
this.enhancedTemplates.forEach((_template) => {
_template.dispose();
});
this.enhancedTemplates.splice(0);
this.enhancedTemplates = null;
this.widgetInstance.dispose(); // If we don't do this, we'll leak memory
this.widgetHost.remove(); // If we don't do this, we'll leak memory
this.widgetInstance = null;
this.widgetHost = null;
this.templateDict = null;
}
// TODO: Train Aurelia how to bind to a Devextreme component so we can eliminate the need for this...possibly
textChanged(newValue) {
this.widgetInstance.option('text', newValue);
}
ingestTemplate(template) {
// Devextreme expects dx-templates to have a name
const templateName = template.getAttribute('name');
const templateRenderer = (renderData) => {
return this.createRenderer(template, renderData.container, renderData.model);
};
// Devextreme expects, simply, that for each named template, we have a render function assigned to the *render* property;
// This is the dictionary we pass to the Devextreme component at the integration point
this.templateDict[templateName] = {
render: templateRenderer,
};
}
createRenderer(rawTemplate, container, model) {
// Append the harvested raw markup (template) to the container provided by Devextreme, and then enhance it
const $enhancementHost = rawTemplate.cloneNode(true);
container.appendChild($enhancementHost);
const templateModel = new DxTemplateModel();
templateModel.data = model;
templateModel.templateName = rawTemplate.getAttribute('name');
const enhancedTemplate = Aurelia.enhance({
host: $enhancementHost,
component: templateModel,
});
const ti = this.enhancedTemplates.findIndex(
(_template) =>
_template.controller.viewModel.templateName ===
enhancedTemplate.controller.viewModel.templateName
);
if (ti >= 0) {
this.enhancedTemplates[ti].dispose();
this.enhancedTemplates.splice(ti, 1);
}
this.enhancedTemplates.push(enhancedTemplate);
// // Register a Dx remove event, dxremove, so that we have an opportunity
// // to call the Aurelia "destroy" lifecycle callbacks (careful to call
// // #detaching first, then #unbinding, then #dipose)
// dxEventOn($enhancementHost, 'dxremove', () => {
// enhancedTemplate?.detaching?.();
// enhancedTemplate?.unbinding?.();
// enhancedTemplate?.dispose?.(); // If we don't call this, we'll leak memory
// });
// Return the host of the enhancement
return $enhancementHost;
}
}
QUESTIONS AND PROBLEMS
My solution above suffers from the following problems:
- I’m leaking memory, which I suspect comes from how I’m storing the enhanced templates for disposal later
- I’m unable to use custom components within the
dx-template, whether I register the custom components globally or locally - The
DxTemplateModel’s lifecycle hooks never get called, nor does its constructor
DxTemplateModel.js looks like this:
export class DxTemplateModel {
constructor() {
this.templateName = '';
this.data = null;
}
unbinding() {
console.log('DX template model unbinding...');
}
}
As for my questions:
- In Aurelia 1.0, one has to register a
dxremoveevent on the enhancement host in order to tear down the component associated with thedx-template(Devextreme fires the event and we can then hook into it). Is this necessary in Aurelia 2.0?dxremovefires, but I don’t know how to cause the lifecycle hooks to fire. - Am I handling the enhanced templates correctly by storing them on the instance, and then iterating over them to dispose of each one?
- How do I get custom components to work within the enhanced templates?
Concerning the latter question, consider the following in the About page, which is where I’m testing my-dx-button:
<import from="./tsi-dx-button"></import>
<!-- <import from="./button-text-provider"></import> -->
<h1>The about landing page.</h1>
<input value.bind="userValue" />
<tsi-dx-button
text.bind="userValue"
click.trigger="handleButtonClick()"
>
<dx-template name="content">
<div class="tsi-test-class">Button: ${data.text}</div>
<!-- <button-text-provider text.bind="data.text"></button-text-provider> -->
</dx-template>
</tsi-dx-button>
I can’t get the commented code above to work. When I supply the button’s text with a custom component, it doesn’t work.
Here is a GIF to show that the uncommented code is working (the HTML with ${data.text} as the text):

I set up a two-way binding with an input just to prove to myself that I can effect a change of the button’s text. Changing the text causes the textChanged handler to fire on the wrapper, a handler that uses the Devextreme widget’s API to push the change into the widget, which then causes the template render function to be called by the widget.
NOTE: The
dx-templateHTML fragment that you’re seeing in the inspector at the right is the version that is reinserted by Devextreme (which is Aurelia-enhanced by then), not the originaldx-templatein the markup that is harvested, removed, and then processed by theprocessContentcallback.
I would appreciate commentary and insight. I’m so close!
Thank you in advance!
