referring to the following post StackOverflow Question I have a quite different scenario where I want to know if Aurelia has a solution for.
Scenario:
I have a user model:
export class User{
@bindable name: string;
@bindable address: Address
As you can see, "Address" is a sub-model.
I have a main view-model "registration". In this view model I have a model "user":
export class RegistrationView{
@bindable user: User
public attached(){
this.user = userService.fetchUserFromApi();
}
In addition to that I have a custom-element "user-address" where I have a "user-address"-model (because I want to have dedicated encapsulated custom-elements).
export class userAddress{
@bindable userAddress: Address
Now I want to request the user model only once from the API and send the user address it to the custom-element:
<template>
<require from="user-address"></require>
<user-address user.address.bind="${dj.address}"></user-address>
Finally I would (to have dedicated encapsulated custom-elements that I can use everywhere) check in attached method if the user is already load and if not then the custom-element would load all needed data:
export class userAddress{
@bindable userId: string
@bindable address: Address
public attached(){
if(!(typeof this.address === "undefined")){
this.address = this.addressAPIService.getAddressByUserId(id)
}
}
dj.address.bind
doesn't work. But now my question is, how can I handle that situation?If I understand your problem correctly, you simply need some form of client-side persistence.
If you need this persistence even after the user closed the browser, you'll want to use either localStorage
or some encapsulation thereof. There are many good plugins available such as localForage, LokiJS and a recently developed (still in beta) aurelia plugin aurelia-store
You probably want to encapsulate the retrieval of your user in a UserService
of some sort. This is nothing specific to Aurelia, just generally how you want to do this in most types of applications.
So in your viewmodel you might have something like this (skipping some of the implementation details such as checking the params, configuring the router etc for brevity):
@autoinject()
export class UserViewModel {
public user: User;
constructor(private userService: UserService){}
// happens before bind or attached, so your child views will always have the user in time
public async activate(params: any): Promise<void> {
this.user = await this.userService.getUserById(params.id);
}
}
And in your userservice:
// singleton will ensure this service lives as long as the app lives
@singleton()
export class UserService {
// create a simple cache object to store users
private cache: any = Object.create(null);
constructor(private client: HttpClient) {}
public async getUserById(id: number): Promise<User> {
let user = this.cache[id];
if (user === undefined) {
// immediately store the user in cache
user = this.cache[id] = await this.client.fetch(...);
}
return user;
}
}
Let your view model just be dumb and call the UserService whenever it needs to load a user, and let your service be clever and only fetch it from the API when it's not already cached.
I'd also like to point out that attached()
is not when you want to be grabbing data. attached()
is when you do DOM stuff (add/remove elements, style, other cosmetic things). bind()
is best restricted to grabbing/manipulating data you already have on the client.
In your routed view models during the routing lifecycle. That'll be configureRouter
, canActivate
, activate
, canDeactivate
and deactivate
. These will resolve recursively before any of the DOM
gets involved.
Not in your custom elements. Or you'll soon find yourself in maintenance hell with notification mechanisms and extra bindings just so components can let eachother know "it's safe to render now because I have my data".
If your custom elements can assume tehy have their data once bind()
occured, everything becomes a lot simpler to manage.
More often than you think, you can let an action be a route instead of a direct method. You can infinitely nest router-view
s and they really don't need to be pages, they can be as granular as you like.
It adds a lot of accessibility when little sub-views can be directly accessed via specific routes. It gives you extra hooks to deal with authorization, warnings for unsaved changes and the sorts, it gives the user back/forward navigation, etc.
For all other cases:
Call a service from an event-triggered method like you normally would during activate()
, except whereas normally the router defers page loading until the data is there, now you have to do it yourself for that element.
The easiest way is by using if.bind="someEntityThatCanBeUndefined"
. The element will only render when that object has a value. And it doesnt need to deal with the infrastructure of fetching data.