Nested custom components issues (Devextreme and Aurelia 2.0 story)

Hello,

UPDATE #2

@bigopon I think I can move the needle on this now. It turns out that the Popup widget never did fire dxremove when the popup is hidden, even in our Aurelia 1.0 version. But with Aurelia 1.0, handling the dxremove event didn’t involve disposing of the enhanced view. Here’s the code from the Aurelia 1.0 app:

...

let newElement = element.cloneNode(true);

...

const view = this._createAureliaViewFromTemplate(
  newElement,
  itemBindingContext,
  itemOverrideContext,
  scope.owningView.resources,
);

...

dxEventOn(newElement, 'dxremove', () => {
  view.detached();
  view.unbind();
});

...

It would appear that Aurelia 1.0 handled the disposing of the resources itself. Compare this to Aurelia 2.0:

...

const $template = rawTemplate.cloneNode(true);

...

const enhancedView = Aurelia.enhance({
  container: config.container,
  host: $template,
  component: {
    data: renderData.model,
  },
});

...

dxEventOn($template, 'dxremove', () => {  
  if (!enhancedView) {
    return;
  }

  onResolve(enhancedView.deactivate(enhancedView, config.controller), () => {
    enhancedView.dispose();  // <== And added step with Aurelia 2.0
    this.templates = undefined;
  });
});

...

The added step of having to dispose is where the hangup is. The inner custom elements (the content of the popup) are indeed attaching (I’ve confirmed that), but they’re not detaching because dxremove is never fired, and so I have no opportunity to dispose of the enhanced views for those inner templates. When the popup has visible set to false, the popup detaches, but none of the inner content does; hence the memory leaks.

How do you recommend I handle the explicit disposal of the inner content? Where might I hook in?

I think that solving this problem will address the memory leak issues.

UPDATE #1

I would like to put this on hold for just a moment. I’m not convinced that nested components are the problem.

I just determined that the Devextreme Popup widget doesn’t dispatch the dxremove event when the popup is hidden, either by clicking outside, or by clicking the close box. The detaching hook fires for the tsi-dx-popup itself, but not for the popup-content element it contains (the attaching hook does fire for popup-content).

I think popup-content is left stranded, because navigating away from where it’s consumed causes a framework error indicating that deactivate is not available on ehancedView.

I’m going to reach out to Devextreme support.

ORIGINAL POST

In my Devextreme (Dx) showcase (which I just posted), in order to eliminate memory leaks, I actually have to remove components where templates that are ingested into Dx widgets contain other Devextreme widgets that in turn contain other Dx widgets, and so on. In addition, Dx widgets that “pop up” (whose visibility is controlled by the widget, not by template controllers) cause additional problems.

The Specific Case of this Post: tsi-dx-popup

The problems I outline below have to do with tsi-dx-popup (but apply to any widget where Dx widgets are nested). I do believe that the leaking of memory has to do with the few popup-type widgets in the markup, which is why I’m focusing on solving my problems with them first.

The Problems

  • In the case of the popup widget, lifecycle creation hooks are called (binding, attaching, etc.), but not teardown lifecycle hooks (I think this is due to the fact that the widget itself controls visibility through its visible option, which I bind back to the consuming component).
  • Probably because of the former problem, when I navigate away from the showcase, I get an error (see below). This happens only when I show the popup (or popover, or tooltip, or toast), then hide it, then navigate away from the showcase.
  • This is definitely causing memory to leak (apart from crippling the application). If I remove all of the popup-like custom elements from the markup, I have no leaks and no errors.

Do you have any ideas what might be the problem? How do I handle the situation where a third-party component manages its own visibility?

PARTIAL SOLUTION

If I set the visible option to true on the popup widget, and then add an Aurelia if-binding to the widget, I can compel the teardown lifecycle hooks, but then I get this error when I close the popup:

THE CODE

Consider the following (from my dx-showcase markup):

<test-popup container=".dx-showcase" visible.two-way="showPopup"></test-popup>

This is typical approach (which I used in my Aurelia 1.0 app), where an application-level custom element wraps the Dx popup wrapper, which in turn wraps the Dx widget itself. test-popup.html contains the popup content in the form of a dx-template, and that content contains other custom elements that wrap Dx widgets…and so on.

test-popup.js:

import { bindable, BindingMode, customElement } from 'aurelia';

@customElement('test-popup')
export class TestPopup {
  @bindable({ mode: BindingMode.twoWay }) visible = true;
  @bindable({ mode: BindingMode.twoWay }) container = false;

  constructor() {
    this.showPopup = false;
  }

  onHidden() {
    this.visible = false;
  }

  ...

  visibleChanged(newValue) {
    this.showPopup = newValue;
  }
}

test-popup.html:

<import from="common/components/dx/tsi-dx-popup"></import>
<import from="modules/dx-showcase/popup-content"></import>

<tsi-dx-popup
  container.bind="container"
  visible.two-way="visible"  
  hidden.trigger="onHidden()"
>
  <tsi-dx-template name="content">
    <popup-content></popup-content>
  </tsi-dx-template>
</tsi-dx-popup>

popup.content.js:

import { customElement } from 'aurelia';

@customElement('popup-content')
export class PopupContent {
  constructor() {
    this.text = 'Some value';
  }

  onClickButton(e) {
    alert('It worked!');
  }

  onValueChanged(e) {
    console.log('Value changed on popup!');
  }
}

popup.content.html:

<import from="common/components/dx/tsi-dx-button"></import>
<import from="common/components/dx/tsi-dx-text-box"></import>

<div value-changed.trigger="onValueChanged($event)">
  <tsi-dx-button text="Some Button"></tsi-dx-button>
  <tsi-dx-text-box value.bind="text"></tsi-dx-text-box>
</div>

Its a bit of a long post so I “saved” it for later the last weekend and completely forgot. I guess the new post you have is where we discuss this?

I think we can discuss it in the new post, yes. But there’s information in this one that might help.

Sorry for the long posts, but it’s an interesting—and complex—migration story I think for those who are using enhance outside the mainstream use cases, and who are migrating from 1.0 to 2.0!