I know that Aurelia allows us to return a Promise()
from the VM's activate()
method, and if so it'll wait for the promise to resolve before switching to the new view.
But, say I have a view that consists of one or several child components and they all make HTTP requests, how can I know when all children are finished from within my parent component?
Bonus question: Is it correct that only VM's that go in the <router-outlet>
utilize the activate()
method, whereas VM's that are used as custom elements utilize the attached()
method?
Edit: To elaborate a little, here's what one of my pages/routes/main views might look like:
<template>
<main>
<section id="item">
<h1>${item.title}</h1>
<img src="${item.image}">
<p>${item.description}</p>
<ul>
<li repeat.for="si of item.subItems">
${si.subItem}
</li>
</ul>
</section>
<aside>
<author-info></author-info>
<recent-items limit="3"></recent-items>
<random-quote></random-quote>
</aside>
</main>
</template>
I can easily wait for ${item}
to load and then resolve the promise in the main view's activate
method, but that doesn't guarantee that the three child elements in the aside
have loaded. This makes them pop up one after the other and it doesn't look great.
I'd very much like to use Aurelia's built in functionality if at all possible, but I guess I might have to resort to my own loader using the EventAggregator or a two-way binding like Ashley suggested.
Would love to hear from someone on the Aurelia team as to whether this is possible at all?
I'm not sure about such a complex chain with activate
and attached
, but you can always use Custom Events.
In activate
of parent element:
return new Promise((resolve, reject) => {
var subscription = this.eventAggregator.subscribe('child-element-loaded', e => {
subscription.dispose();
resolve();
});
});
And in the custom element you'll need to trigger this event whenever needed:
this.eventAggregator.publish('child-element-loaded');
And, of course, you'll need to import Event Aggregator
import {EventAggregator} from 'aurelia-event-aggregator';
And inject it into both child and parent elements.
Load all the data in the route's activate()
callback
As Ashley noted in the comment above, the best strategy is to load all of the data in the parent route and push that data into a custom element via bindings. You mentioned that this would lead to copy/pasted code in each route that contained that element, but we can solve this problem by moving the loading code to a service class and injecting that class into the route. This way, we can keep the code dry while also keep it straightforward and readable.
We'll demonstrate by creating a <random-quote>
custom element along with a service that will provide us with random quotes.
randomQuoteCustomElement.ts
export class RandomQuoteCustomElement {
@bindable quote: IQuote;
}
randomQuoteCustomElement.html
<template>
<i>${quote.text}</i>
<strong>${quote.author}</strong>
</template>
randomQuoteService.ts
export class RandomQuoteService {
getRandomQuote: Promise<IQuote>() {
return this.http.fetch('api/quote/random')
.then((response) => response.json())
});
}
}
Next, we'll include the custom element in our view, inject the service into our view model, fetch the data through the service, and have our activate()
method depend on the returned promise.
main.ts
import { RandomQuoteService} from './randomQuoteService';
@inject(RandomQuoteService)
export class MainViewModel {
quote: IQuote;
constructor(quotes: RandomQuoteService) {
this.quotes = quotes;
}
activate() {
return Promise.all([
this.quotes.getRandomQuote()
.then((quote) => this.quote = quote)
]);
}
}
main.html
<template>
<require from="./randomQuoteCustomElement"></require>
<random-quote quote.bind="quote"></random-quote>
</template>
Because we want our activate()
function to depend strongly on the results of the RandomQuoteService
, we need to include this code directly in our activate()
callback. We can also design the the custom element to allow binding data, but fall back to fetching its own data, leveraging the same service.
randomQuoteCustomElement.ts
export class RandomQuoteCustomElement {
@bindable quote;
constructor(quotes) {
if (!this.quote) {
quotes.getRandomQuote()
.then((quote) => !this.quote && this.quote = quote);
}
}
}
Here's a working example: https://gist.run/?id=c5570192afe5631355efe6b5da3e44b5
If you have several http calls that must be returned before rendering the view, you could use Promise.all
, then bind the results to children components. For instance:
activate () {
let promise1 = new Promise()...;
let promise2 = new Promise()...;
let promise3 = new Promise()...;
return Promise.all([promise1, promise2, promise3]);
//bind the results to children components
}
In this way, activate()
will await all promises. However, as @AshleyGrant said in his comment, you should be careful with this. This could result in a slow process.