Conclusion: The need to dispose of an enhanced view interferes with some third-party integrations

Hello,

(Please see the following post: Nested custom components issues (Devextreme and Aurelia 2.0 story) ).

I’ve come to the conclusion that the need to dispose of an enhanced view (what’s given to us upon enhancement) interferes with those third-party components that handle their own visibility (in some cases). We may not always receive a DOM-removal event for children of the parent component.

With the Devextreme components, Popover, Popup, and Tooltip, are hidden and shown under the power of the third-party component. This leaves inner enhanced content dangling. Hiding a popup doesn’t provide an opportunity to dispose of inner content, which leaves the controller of the consuming component in an unstable state.

This problem didn’t occur in Aurelia 1.0 because there was no need to dispose of the view. Interestingly, I never had to worry about whether the view was going to be available on which to call lifecycle hooks. With Aurelia 2.0, it is possible to have an enhancedView on which there is no longer a deactivate or dispose method.

In Aurelia 1.0, the aforementioned components work fine with enhanced inner content. In Aurelia 2.0, they do not.

How do we handle this scenario?

Normally you wouldnt need to go for dispose unless you are sure its never gonna ve touched again. The error and the behavior you described sounds to me like you are doing dispose rather than normal deactivation. Can you give some pseudo code?

The code given below was crafted with your help, and it works perfectly for all of the other components in the Devextreme suite (at least, the 33 I’ve wrapped so far). It’s just the Popover, Popup, and Tooltip components that are a problem because of how they handle visibility.

I’ve tried to guard around it by testing first for enhancedView, and then for the presence of deactivate and dispose methods. That eliminates the error, but causes memory to leak because the inner enhanced content never disposes.

The big problem here is that the 3 components I pointed out above do not dispatch dxremove when the popup is hidden (by the close box, or by clicking outside). So I have no way to hook in.

The createRenderer mehod:

createRenderer(rawTemplate, renderData, config) {
    // Necessary to avoid a hydration instruction mismatch
    const $template = rawTemplate.cloneNode(true);
    // Place the template into the container the Dx widget provides
    renderData.container.append($template);
   
    const enhancedView = Aurelia.enhance({
      container: config.container,
      host: $template,
      component: {
        data: renderData.model,
      },
    });

    // Register a Dx remove event, dxremove, so we can apply Aurelia conventions to deactivating and disposing of an enhanced view
    // The widget will clean this up itself upon its disposal
    dxEventOn($template, 'dxremove', (e) => {      
      if (!enhancedView) {
        return;
      }

      // This will trigger the tear-down lifecycle hooks of the enhanced view.
      onResolve(enhancedView.deactivate(enhancedView, config.controller), () => {
        enhancedView.dispose();
        this.templates = undefined;
      });
    });

    return $template;
  }

I think as long as you are holding on to the enhancedView reference, its likely that some inner content wont be propey cleaned up. Aurelia should do better but shouldnt you also release that reference by setting enhancedView to null?

Thats just a guess, there could be more complication, like even the fact we are not cleaning up the event on template could also be a lead, because the callback holds a reference to the enhancedView, maybe we have to do a snapshot to check deeper.

The big problem here is that the 3 components I pointed out above do not dispatch `dxremove` when the popup is hidden (by the close box, or by clicking outside). So I have no way to hook in.

Is this an issue with the widget, or something else? It doesnt sound like Aurelia related.

I think as long as you are holding on to the enhancedView reference, its likely that some inner content wont be propey cleaned up

I am holding on to a reference, yes, with the expectation that dxremove will fire and I’ll be able to dispose of enhancedView correctly.

Aurelia should do better but shouldnt you also release that reference by setting enhancedView to null?

That’s just it though: If dxremove doesn’t fire when closing the popup (popover, tooltip), when and by what means would I have an opportunity to set enhancedView to null?

Is this an issue with the widget, or something else? It doesnt sound like Aurelia related.

I’m not sure “issue” is the right word here, but it’s definitely related to Aurelia. These same components (same versions) work as I write this in Aurelia 1.0 without a problem. It’s irrelevant that dxremove doesn’t fire under 1.0 because Aurelia cleans up the view.

Under 2.0, I’m the one who has to dispose (not to say that that isn’t a good thing in general), but it does change the contours of this use case.

I was only speaking in general related to cleaning up the reference. From the look of the code, im not sure were to look. Maybe lets have a repro fitst? Please use stackblitz so its easier to run and test. Aurelia app - conventions - Vite - StackBlitz

A couple of things. I’m using Webpack and javascript. Also, I don’t know if there’s a licensing issue with DevExpress/StackBlitz.

Let me check into it…

For webpack you can use this link Aurelia app - conventions webpack - StackBlitz

The link you gave me involves typescript. I’m using webpack and javascript. It’s having trouble recognizing the ‘@’ character at @customElement(…)

Also, I would need basic routing since navigating away from the consuming component (page) it where the error is produced.

Do you have a webpack and babel StackBlitz with routing?

Hello @bigopon ,

Wow! I had never used StackBlitz before. Amazing!

I have a repro for you at node.new Starter - StackBlitz .

I figured out that I could start a node.new project and just run npx makes aurelia with the same setup I have. I had to move some files out into the root and adjust the webpack configuration slightly, but it was pretty straightforward.

To reproduce the error:

  1. Navigate to the About Page;
  2. Click the Show Popover button;
  3. Close the popover;
  4. Navigate to the Welcome Page.

The error you see will appear for the Popup, Popover, and Tooltip widgets. For those widgets, visibility is a fundamental part of the widget and managed by the widget itself. So using the visibility bindings of Aurelia doesn’t solve the problem.

I hope this helps.

1 Like

this almost looks like a router bug, let’s check with @Sayan751 and @dwaynecharrington if they recall something first. Thanks @estaylorco

2 Likes

With that said, would it be helpful if I tried this with dynamic composition instead of routing, switch between two views—one containing the popover and the other not—to see if that produces the same error?

@estaylorco It seems that the enhanced view needs to be handled correctly. Here is a working version.

-  const enhancedView = Aurelia.enhance({
+  onResolve(
+    Aurelia.enhance({
      container: config.container,
      host: $template,
      component: {
        data: renderData.model,
      },
-   });
+   }),
+   (enhancedView) => {
      // Register a Dx remove event, dxremove, so we can apply Aurelia conventions to deactivating and disposing of an enhanced view
      // The widget will clean this up itself upon its disposal
      dxEventOn($template, 'dxremove', (e) => {
        console.log('dxremove dispatched...', __cloneDeep(e));
  
        if (!enhancedView) {
          return;
        }
  
        // This will trigger the tear-down lifecycle hooks of the enhanced view.
        onResolve(
          enhancedView.deactivate(enhancedView, config.controller),
          () => {
            enhancedView.dispose();
            this.templates = undefined;
          }
        );
      });
  
      return $template;
+    }
+  );

Explanation: the Aurelia.enhance can return a promise which needs to be handles properly. During deactivation of the about-view, the disposeof the enhanced view failed as .dispose() is not a method of promise (see below), which leads to the failure of the view deactivation and consequently the error observed.

image

Happy coding :v:

@Sayan751 @bigopon

You have no idea what a relief that is to me! I worked hard on the Aurelia 1.0 solution a number of years ago, and it worked flawlessly. I was able to marry the best web framework in the world with the best component suite in the world! To see that again under Aurelia 2.0 is just wonderful.

Thanks so much to both of you!

What’s interesting, though, is that I travelled down the path of async on enhance early on, but because it didn’t work, I assumed that my scenario wasn’t actually async.

I initially had this:

async createRenderer(rawTemplate, renderData, config) {
    const $template = rawTemplate.cloneNode(true);
    renderData.container.append($template);

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

but that didn’t work because Devextreme widgets can’t handle async renderers.

Then I tried this, and it didn’t work either:

createRenderer(rawTemplate, renderData, config) {
    const $template = rawTemplate.cloneNode(true);
    renderData.container.append($template);

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

The documentation doesn’t mention onResolve, but it looks like this is the magic dust we have to sprinkle on enhance.

Shouldn’t I have been able to refactor from async-await to a then? Or am I missing something here?

You can also use async-await. There should not be any issue as well. The onResolve is just used widely in Aurelia internally to ensure immediate callback if the return value is not a promise, resulting in a synchronous operation, that’s all.

Fell free to use whatever matches your coding practices.

1 Like