1

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.

Community
  • 1
  • 1
Jerrad
  • 5,240
  • 1
  • 18
  • 23

1 Answers1

2

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.

Lucas Gabriel Sánchez
  • 40,116
  • 20
  • 56
  • 83
Matthew James Davis
  • 12,134
  • 7
  • 61
  • 90
  • I've implemented something similar to your suggestion here: http://plnkr.co/edit/p943Er?p=preview and it's working. The next step is to load x number of additional components based on the selected city. These components could also use the propertyObserver to listen for changes, but I would like to guard against an invalid city being used (so all x components don't try to load data for the invalid city). That's why I wanted the parent to check the city, and then notify the other components if everything was ok. – Jerrad Feb 18 '16 at 14:44
  • I guess the bigger question (my example aside) is, if you have a bunch of loosely-coupled components that need to communicate with each other, how do you know when they are ready to be communicated with? – Jerrad Feb 18 '16 at 14:46
  • oh i see, it depends on the thing. for example, if its a module being used in other views and view models, then constructor will be invoked before it is included in the page, so putting listeners in the constructor will be safe. if you're using custom elements, like in your example, you might want to leverage the `attached()` callback. See here for more info: http://aurelia.io/docs.html#/aurelia/framework/1.0.0-beta.1.1.3/doc/article/creating-components – Matthew James Davis Feb 18 '16 at 19:45
  • The parent is constructed, activated, and attached all before the state and city pickers are even constructed. That's why I was having the problem. Anyway, thanks for the help. – Jerrad Feb 19 '16 at 17:15
  • This was quite some time ago, but I'd suggest using an `if.bind` expression set to the variable you are waiting to load data from. The custom element shouldn't be loaded until `value` in `if.bind="value"` is set in the parent view-model. – bburc Oct 18 '17 at 16:28