Here is simplified example of what I have in my app. Everything else is a default webpack ts app.
When form or grid opens for the first time, data get not loaded as {property}Changed handler is not invoked.
So the main difficulty is that I need to track my myself, would or would not {property}Changed be called. For example, be adding boolean property, that I set to true in bind(). And if it would not be called, call it by myself. But this is not reliable as behavior could be changed.
Another option is to add something like @observable propObs:string.
In propChanged set this.propObs = this.prop.
In propObsChanges set this.prop = this.propObs and start actually data load.
So prop property will be for templating, propObs for actual data management.
This is looks more reliable but is kind of excess.
form.html
<template>
<require from="./grid"></require>
<!-- way to change id via ui - explicitly bound to input -->
<div>form for <input value.bind="id & debounce:500" /></div>
<div>
<!-- way to change id via routing, kind of paging -->
<a if.bind="id.length > 1" route-href="route: form; params.bind: { id: id.substring(0, id.length - 1) }">prev</a>
<span else>prev</span>
<a route-href="route: form; params.bind: { id: id + (id.length + 1) }">next</a>
</div>
<br />
<template if.bind="data || loading">
<div>ID is ${loading ? '...' : data.id}</div>
<div>Date is ${loading ? '...' : data.date}</div>
</template>
<template else>
<div>nothing found</div>
</template>
<br />
<div><a route-href="route: grid; params.bind: { q: id }">open full grid</a>(grid data will not load)</div>
<some-grid query.to-view="id" limit="3"></some-grid>
</template>
form.ts
import { bindable, autoinject } from "aurelia-framework";
import { Router } from "aurelia-router";
import { SomeExternalLoader } from "loader";
import { Logger } from "aurelia-logging";
@autoinject
export class SomeFormCustomElement {
@bindable id: string;
@bindable loading: boolean;
@bindable data: { id: string, date: Date };
constructor(private router: Router, private external: SomeExternalLoader, private log: Logger) {
this.log.id = 'form';
}
activate({ id }: { id: string }) {
this.log.info('activated');
this.id = id;
}
async idChanged(cur: string, prev: string) {
this.log.info(`id changed from ${prev} to ${cur}`);
this.router.navigateToRoute('form', { id: this.id });
this.loading = true;
const data = await this.external.formData(this.id);
if (cur === this.id) {
this.data = data;
this.loading = false;
}
}
}
grid.html
<template>
<div>search for <input value.bind="query & debounce:500" /></div>
<template if.bind="loading">
<li><ul repeat.for="item of query.length">...</ul></li>
</template>
<template else>
<li><ul repeat.for="item of data">${item}</ul></li>
</template>
</template>
grid.ts
import { bindable } from "aurelia-templating";
import { autoinject } from "aurelia-framework";
import { SomeExternalLoader } from "loader";
import { Logger } from "aurelia-logging";
@autoinject
export class SomeGridCustomElement {
@bindable query: string;
@bindable limit: number;
@bindable loading: boolean;
@bindable data: string[];
constructor(private external: SomeExternalLoader, private log: Logger) {
this.log.id = 'grid';
}
activate({ q }: { q: string }) {
this.log.info('activated');
this.limit = undefined;
this.query = q;
}
async queryChanged(cur: string, prev: string) {
this.log.info(`query changed from ${prev} to ${cur}`);
this.loading = true;
const data = await this.external.gridData(this.query, this.limit);
if (cur === this.query) {
this.data = data;
this.loading = false;
}
}
}
loader.ts
export class SomeExternalLoader {
public formData(id: string): Promise<{ id: string, date: Date }> {
return this.delay(id.length > 5 ? null : { id, date: new Date() }, 700);
}
public gridData(query: string, limit: number): Promise<string[]> {
return this.delay(query.split('').slice(0, limit).map(x => `${x} - data`), 1200);
}
private delay<T>(data: T, ms: number): Promise<T> {
return new Promise(r => setTimeout(() => r(data), ms));
}
}
app.html
<template>
<div><a href="/">home</a> (form data will not load)</div>
<br />
<router-view></router-view>
</template>
app.ts
import { PLATFORM } from "aurelia-pal";
import { Router, RouterConfiguration } from 'aurelia-router';
export class App {
public router: Router;
public configureRouter(config: RouterConfiguration, router: Router) {
config.map([
{ route: '', redirect: '/form/1234' },
{ route: '/form/:id', name: 'form', moduleId: PLATFORM.moduleName('./form'), title: 'Form' },
{ route: '/grid/:q', name: 'grid', moduleId: PLATFORM.moduleName('./grid'), title: 'Grid' },
]);
this.router = router;
}
}