The Lifecycle of an Aurelia Custom Element

I’ve been working with Aurelia pretty well recently, but I came across a problem early on. I had some code within my navigation bar (which is a custom element) and I wanted to manipulate the DOM on startup so that my initial state was correct. So, here was my code:

import {Authenticator} from "../lib/Authenticator";

export class NavBar {
    public constructor() {
        this.setIcon(Authenticator.isAuthenticated());
    }

    public setIcon(isAuthenticated: boolean): void {
        let appToolbar = document.getElementById("appToolbar");
        let elements = document.querySelectorAll(".sign-in,.sign-out");
        // Do something with the elements list
    }
}

This is pretty simple. My Authenticator object has a static method that returns true or false depending on if I am authenticated or not. It doesn’t work. I get an exception in the Javascript console at the highlighted line. The problem is that the DOM isn’t ready when the object is instantiated.

So, how do we fix this? Well, we need to know a little bit about the underlying code and the lifecycle functions that Aurelia uses on custom elements. Firstly, let’s do a little experiment. I replaced the code above with this:

import {Authenticator} from "../lib/Authenticator";

export class NavBar {
    public constructor() {
        //this.setIcon(Authenticator.isAuthenticated());
    }

    public attached() {
        this.checkit("attached");
    }

    public activated() {
        this.checkit("activated");
    }

    public created() {
        this.checkit("created");
    }

    public activate() {
        this.checkit("activate");
    }

    public canActivate() {
        this.checkit("canActivate");
    }

    public checkit(fn: string) {
        let appToolbar = document.getElementById("appToolbar");
        console.log("[NavBar] %s %o", fn, appToolbar);
    }

    public setIcon(isAuthenticated: boolean): void {
        let appToolbar = document.getElementById("appToolbar");
        let elements = document.querySelectorAll(".sign-in,.sign-out");
        // Do something with the elements list
    }
}

I basically guessed at the lifecycle functions on the initialization side of the fence. Each one calls checkit that then checks whether the DOM is ready (as referenced by looking for an element inside the DOM of the custom element) and then prints out the method that was called and the result of the check. Since I am printing the object, I can expand the object within the console and see what else is there. Here is what I got:

blog-0717-1

Note the highlighted lines. Aurelia calls created() first, but the DOM is not available at this point. It calls attached() next and then the DOM is ready – we have a DOM element as a result of our query.

It turns out I am not the only one here. There is a bug against the documentation asking that this be included in the documentation. But it begs the question – what other lifecycle methods do I have access to? The custom elements in Aurelia are based on Web Components (see an introduction here). the webcomponents.js polyfill has four lifecycle callbacks:

  1. createdCallback
  2. attachedCallback
  3. detachedCallback
  4. attributeChangedCallback

The createdCallback will call create(); the attachedCallback will call attached(). I’m guessing the detachedCallback calls detached() but I don’t have any documentation for that. The attributeChangedCallback is handled differently. When an attribute changes, a specific method for the attribute is called.

As a result of this investigation, I can now re-code my nav-bar.ts as follows:

import {Authenticator} from "../lib/Authenticator";

export class NavBar {
    public attached() {
        this.setIcon(Authenticator.isAuthenticated());
    }

    public setIcon(isAuthenticated: boolean): void {
        let appToolbar = document.getElementById("appToolbar");
        let elements = document.querySelectorAll(".sign-in,.sign-out");
        // Do something with the elements list
    }
}

Just remember – if you want to modify the DOM in your custom element, do it in the attached() method of your custom element class.