Search code examples
aureliaaurelia-templating

How to tear down an enhanced fragment


I am working on an existing SPA where we replace components with Aurelia components step by step. We use the enhance API of the TemplatingEngine. That works pretty well but we also need to tear down those enhanced fragments (remove event listeners, ...) when moving to another part of the application (no page reload).

My idea is to keep the aurelia instance in the page and reuse it.

Currently I enhance fragments like this:

function enhanceFragment(targetElement) {

    function proceed() {
        let aurelia = window.DFAurelia;
        let engine = aurelia.container.get(TemplatingEngine);
        engine.enhance({
            container: aurelia.container,
            element: targetElement,
            resources: aurelia.resources
        });
    }

    if (!window.DFAurelia) {
        bootstrap(async aurelia => {
            aurelia.use
                .defaultBindingLanguage()
                .defaultResources()
                .eventAggregator()
                .developmentLogging()
                .globalResources('app/df-element');

            await aurelia.start();
            window.DFAurelia = aurelia;
            proceed();
        });
    } else {
        proceed();
    }
}

The HTML I enhance looks like:

<df-element></df-element>

I tried this in a function of the custom element itself (DfElement::removeMyself()):

let vs: ViewSlot = this.container.get(ViewSlot);
let view: View = this.container.get(View);
vs.remove(view);
vs.detached();
vs.unbind();

but I get an error when getting the view from the container (Cannot read property 'resources' of undefined). I called this function from a click handler.

Main question: how to manually trigger the unbind and detached hooks of the DfElement and its children?

Bonus questions: properties of my aurelia instance (window.DFAurelia) root and host are undefined: is that a bad thing? Do you see any potential issue with this way of enhancing (and un-enhancing) fragments in the page?


Solution

  • Use the View returned from the enhance() method.

    The enhance() method returns the enhanced View object. It is a good practice to manage the teardown from the same location where you call enhance(), as you may not be able to trust an element to remember to tear itself down. However, you can always register the View instance with the enhance container to access it from within the custom element.

    function proceed() {
      let aurelia = window.DFAurelia;
      let container = aurelia.container;
      let engine = container.get(TemplatingEngine);
      let view = engine.enhance({
          container: container,
          element: targetElement,
          resources: aurelia.resources
      });
      container.registerInstance(View, view);
    }
    

    This will tell the DI container to respond to calls for View with this View.

    import { inject, Aurelia, View } from 'aurelia-framework';
    
    @inject(Aurelia, Element)
    export class DFCustomElement {
    
      // element is passed to the constructor
      constructor(aurelia, element) {
        this.container = aurelia.container;    
        this.element = element;
      }
    
      // but View is only available after attached
      attached() {
        this.view = this.container.get(View);
      }
    
      removeMyself() {
        this.element.remove();
        this.view.detached();
        this.view.unbind();
      }
    }
    

    Using the created(view) lifecycle method

    A much better practice would be to use the created(view) lifecycle method in your custom element.

    import { inject } from 'aurelia-framework';
    
    @inject(Element)
    export class DFCustomElement {
    
      constructor(element) {
        this.element = element;
      }
    
      created(view) {
        this.view = view;
      }
    
      removeMyself() {
        this.element.remove();
        this.view.detached();
        this.view.unbind();
      }
    }
    

    This is a much more straightforward, best practices way of having a custom element grab its own View. However, when trying to write this answer for you, I tested nesting a custom element in a <compose> element. The result was that the View in my custom element was actually the View for my <compose> element, and removeMyself() removed the <compose> entirely.