Jerrad Jerrad - 16 days ago 6
Javascript Question

How can Aurelia custom elements interact?

In Aurelia, I have a parent component that is composed of several other components. To keep this example simple, say that one component is a State dropdown, and another component is a City dropdown. There will be other components that depend on the selected city, but those two are enough to illustrate the issue. The parent view looks like this:

<template>
<compose view-model="StatePicker"></compose>
<compose view-model="CityPicker"></compose>
</template>


The idea is that you would pick a state from the StatePicker, which would then populate the CityPicker, and you would then pick a city which would populate other components. The routes would look like
/:state/:city
, with both being optional. If
:state
is omitted, then the url will automatically redirect to the first available state.

I'm using the EventAggregator to send messages between the components (the parent sends a message to the CityPicker when a state is selected). This is working, except for the initial load of the application. The parent is sending the message to the CityPicker, but the CityPicker component hasn't been activated yet, and it never receives the message.

Here's a Plunker that shows the problem. You can see that the city dropdown is initally empty, but it starts working once you change the state dropdown. Watch the console for logging messages.

So the question is: Is there a way to know that all the components are loaded before I start sending messages around? Is there a better technique that I should be using? Right now, the StatePicker sends a message to the parent that the state has changed, and then the parent sends a message to the CityPicker that the state has changed. That seems a little roundabout, but it's possible that the user could enter an invalid state in the url, and I liked the idea of being able to validate the state in one place (the parent) before all the various other components try to load data based on it.

Answer

The view/viewModel Pattern

You would want your custom elements to drive data in your viewModel (or in Angular / MVC language, controller). The viewModel captures information about the current state of the page. So for example, you could have a addressViewModel route that has state and city properties. Then, you could hook up your custom elements to drive data into those variables. Likewise, they could listen to information on those variables.

Here's an example of something you might write:

address.html

<state-picker stateList.one-way="stateList" value.bind="state" change.delegate="updateCities()"></state-picker>
<city-picker cityList.one-way="cityList" value.bind="city"></city-picker>

address.js

class AddressViewModel {

    state = null;
    city = null;

    stateList = ['Alabama', 'Alaska', 'Some others', 'Wyoming'];
    cityList = [];

    updateCities() {
        let state = this.state;
        http.get(`API/cities?state=${state}`) // get http module through dependency injection
            .then((response) => { 
                var cities = response.content;
                this.cities.length = 0; // remove current entries
                Array.prototype.push.apply(this.cities, cities);
            });
    }
}

If you wanted to get a little more advanced and isolate all of your state and city logic into their respective custom elements, you might try following this design pattern:

address.html

<state-picker value.bind="state" country="US"></state-picker>
<city-picker value.bind="city" state.bind="state"></city-picker>

address.js

class cityPickerViewModel {

    @bindable
    state = null;

    cities = [];

    constructor() {
        // set up subscription that listens for state changes and calls the update
        //  cities function, see the aurelia documentation on the BindingEngine or this 
        // StackOverflow question: 
        // http://stackoverflow.com/questions/28419242/property-change-subscription-with-aurelia
    }

    updateCities() {
        /// same as before
    }
}

The EventAggregator Pattern

In this case, you would not want to use the EventAggregator. The EventAggregator is best used for collecting various messages from disparate places in one central location. For example, if you had a module that collected app notifications in one notification panel. In this case, the notification panel has no idea who might be sending messages to it, so it would just collect all messages of a particular type; likewise, any component could send messages whether or not there is a notification panel enabled.