Search code examples
aureliacustom-element

Know when child element has loaded


I've created three Google Map related elements in Aurelia, and I'm using them in a nested fashion where I have a base GoogleMap element, and on top of that I have a GoogleMapLocationPicker element and on top of that a GoogleMapAutocomplete element.

This all works mostly fine, but the problem I'm having is that when I try to access my child GoogleMap element from within my GoogleMapAutocomplete element I have to put my code in an arbitrary setTimeout or I get errors for trying to access non-existent properties.

I really don't like just slapping 200ms on a setTimeout and hope it never fails, so I'd like to know if there's some official way of knowing when my child elements have loaded? (Without using the EventAggregator which I might do as a last resort).

I basically have this (please note the code is extremely simplified and in some cases outright incorrect, but this is for demonstration purposes only - the actual google maps code works fine):

google-map

export class GoogleMap {
    @bindable markers;

    constructor () {
        this.map = new google.maps.Map(this.element);

        this.markers.forEach((marker) => {
            this.addMarker(marker);
        });
    }
}

<template>
    <div class="google-map"></div>
</template>

google-map-location-picker

export class GoogleMapLocationPicker {
    constructor {
        this.markers = [{
            draggable: true,
            dragend: e => {
                alert('Nice drag');
            }
        }];
    }
}

<template>
    <google-map markers.bind="markers" view-model.ref="map"></google-map>
</template>

google-map-autocomplete

export class GoogleMapAutocomplete {
    constructor () {
        this.autocomplete = new google.maps.places.Autocomplete();

        // This fails unless I wait like 200ms because GoogleMap hasn't yet created its map
        this.map.map.map.controls.push(this.autocomplete);
    }
}

<template>
    <google-map-location-picker view-model.ref="map"></google-map-location-picker>
</template>

Update:

I've created a Plunker to further explain this: http://plnkr.co/edit/8fVbjZhJoC8tzAfQOmKP?p=preview

As you can see App includes <parent-element> (in my case this would be the GoogleMapAutocomplete for example) and it in turn includes a <child-element> (in my case GoogleMap).

The child element does some async stuff and only after that is complete can I access properties of the child from the parent. As you can see in parent-element.js trying to alert the child's property returns undefined.

How can I, from parent-element.js, know when child-element.js has finished all its async stuff?


Solution

  • The cleanest solution here might be to dispatch a CustomEvent when the child-element is done, then .delegate that to the parent-element, like so:

    (I've taken your plunkr as the basis)

    In child-element.js:

    import { inject } from "aurelia-framework";
    
    @inject(Element)
    export class ChildElement {
      constructor (element) {
        this.isLoaded = false;
        this.element = element;
      }
    
      attached () {
        setTimeout(() => {
          this.isLoaded = true;
          this.aPropertyOnlyAvailableAfterLoadingIsComplete = true;
          this.element.dispatchEvent(new CustomEvent("loaded", {
              bubbles: true
          }));
        }, 2000);
      }
    }
    

    In parent-element.html:

    <child-element view-model.ref="child" loaded.delegate="onChildLoaded($event)">
    </child-element>
    

    In parent-element.js:

    onChildLoaded($event) {
        alert(this.child.aPropertyOnlyAvailableAfterLoadingIsComplete);
    }
    

    See this gist for a running version

    EDIT (Event Aggregator vs CustomEvent)

    Your comment regarding the difference between using Event Aggregator and a CustomEvent is an important question, so I'll answer it here.

    The blog post working with the aurelia event aggregator gives some additional pointers:

    It allows you to fire off events that don’t have a specific target, events potentially multiple listeners could be looking out for. They are not essential, they are like observers, when something happens the subscriber methods are notified and allow you to act accordingly.

    Everything comes with a cost, so don’t abuse it and use it for every change in your application, it is very much for those situations where cross component communication is essential.

    1. events that don't have a specific target

    In this case, your target is the parent of a certain element. That's a very specific target and should be an indicator that a CustomEvent is probably the best fit.

    1. don't use it for every change in your application

    It's easy to use the Event Aggregator for every change in your application. All of it will just work. Aside from potential timing issues with components that depend on it (like aurelia-router), overusing the Event Aggregator will reduce cohesion between closely related components for little to no gain.

    I would like to add to that:

    1. Use a CustomEvent when the components are tightly coupled / closely related, and Event Aggregator when they loosely coupled

    2. Use a CustomEvent when the components are tied to the DOM (such as custom elements), and Event Aggregator when they are not (such as stand-alone modules/classes, in which case a CustomEvent cannot be used)

    3. For DOM-tied components where a CustomEvent cannot solve your problem in a clean, straight-forward manner (for example with sibling elements), the Event Aggregator may be a better idea

    4. Using the Event Aggregator means you have subscriptions to dispose, which adds work and complexity.

    5. The Event Aggregator is a dependency you need to import, while CustomEvents are native to JavaScript

    CustomEvents are usually simpler to implement/manage and have clearer limitations in their use. They also make the views more readable (consider some-event.delegate="onSomeEvent" versus having to scan through both classes for publish/subscribe methods and their keys + handlers).

    For this reason I tend to start with a CustomEvent, and only if it doesn't work / gets too complex, I resort to the Event Aggregator.