In Angular 1.x, UI-Router was my primary tool for this. By returning a promise for "resolve" values, the router would simply wait for the promise to complete before rendering directives.
Alternately, in Angular 1.x, a null object will not crash a template - so if I don't mind a temporarily incomplete render, I can just use $digest
to render after the promise.then()
populates an initially empty model object.
Of the two approaches, if possible I'd prefer to wait to load the view, and cancel route navigation if the resource cannot be loaded. This saves me the work of "un-navigating". EDIT: Note this specifically means this question requests an Angular 2 futures-compatible or best-practice method to do this, and asks to avoid the "Elvis operator" if possible! Thus, I did not select that answer.
However, neither of these two methods work in Angular 2.0. Surely there is a standard solution planned or available for this. Does anyone know what it is?
@Component() {
template: '{{cats.captchans.funniest}}'
}
export class CatsComponent {
public cats: CatsModel;
ngOnInit () {
this._http.get('/api/v1/cats').subscribe(response => cats = response.json());
}
}
The following question may reflect the same issue: Angular 2 render template after the PROMISE with data is loaded . Note that question has no code or accepted answer in it.
Try {{model?.person.name}}
this should wait for model to not be undefined
and then render.
Angular 2 refers to this ?.
syntax as the Elvis operator. Reference to it in the documentation is hard to find so here is a copy of it in case they change/move it:
The Elvis Operator ( ?. ) and null property paths
The Angular “Elvis” operator ( ?. ) is a fluent and convenient way to guard against null and undefined values in property paths. Here it is, protecting against a view render failure if the currentHero is null.
The current hero's name is {{currentHero?.firstName}}
Let’s elaborate on the problem and this particular solution.
What happens when the following data bound title property is null?
The title is {{ title }}
The view still renders but the displayed value is blank; we see only "The title is" with nothing after it. That is reasonable behavior. At least the app doesn't crash.
Suppose the template expression involves a property path as in this next example where we’re displaying the firstName of a null hero.
The null hero's name is {{nullHero.firstName}}
JavaScript throws a null reference error and so does Angular:
TypeError: Cannot read property 'firstName' of null in [null]
Worse, the entire view disappears.
We could claim that this is reasonable behavior if we believed that the hero property must never be null. If it must never be null and yet it is null, we've made a programming error that should be caught and fixed. Throwing an exception is the right thing to do.
On the other hand, null values in the property path may be OK from time to time, especially when we know the data will arrive eventually.
While we wait for data, the view should render without complaint and the null property path should display as blank just as the title property does.
Unfortunately, our app crashes when the currentHero is null.
We could code around that problem with NgIf
<!--No hero, div not displayed, no error -->
<div *ngIf="nullHero">The null hero's name is {{nullHero.firstName}}</div>
Or we could try to chain parts of the property path with &&, knowing that the expression bails out when it encounters the first null.
The null hero's name is {{nullHero && nullHero.firstName}}
These approaches have merit but they can be cumbersome, especially if the property path is long. Imagine guarding against a null somewhere in a long property path such as a.b.c.d.
The Angular “Elvis” operator ( ?. ) is a more fluent and convenient way to guard against nulls in property paths. The expression bails out when it hits the first null value. The display is blank but the app keeps rolling and there are no errors.
<!-- No hero, no problem! -->
The null hero's name is {{nullHero?.firstName}}
It works perfectly with long property paths too:
a?.b?.c?.d
The package @angular/router
has the Resolve
property for routes. So you can easily resolve data before rendering a route view.
See: https://angular.io/docs/ts/latest/api/router/index/Resolve-interface.html
Example from docs as of today, August 28, 2017:
class Backend {
fetchTeam(id: string) {
return 'someTeam';
}
}
@Injectable()
class TeamResolver implements Resolve<Team> {
constructor(private backend: Backend) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<any>|Promise<any>|any {
return this.backend.fetchTeam(route.params.id);
}
}
@NgModule({
imports: [
RouterModule.forRoot([
{
path: 'team/:id',
component: TeamCmp,
resolve: {
team: TeamResolver
}
}
])
],
providers: [TeamResolver]
})
class AppModule {}
Now your route will not be activated until the data has been resolved and returned.
Accessing Resolved Data In Your Component
To access the resolved data from within your component at runtime, there are two methods. So depending on your needs, you can use either:
route.snapshot.paramMap
which returns a string, or the route.paramMap
which returns an Observable you can .subscribe()
to. Example:
// the no-observable method
this.dataYouResolved= this.route.snapshot.paramMap.get('id');
// console.debug(this.licenseNumber);
// or the observable method
this.route.paramMap
.subscribe((params: ParamMap) => {
// console.log(params);
this.dataYouResolved= params.get('id');
return params.get('dataYouResolved');
// return null
});
console.debug(this.dataYouResolved);
I hope that helps.
EDIT: The angular team has released the @Resolve decorator. It still needs some clarification, in how it works, but until then I'll take someone else's related answer here, and provide links to other sources:
EDIT: This answer works for Angular 2 BETA only. Router is not released for Angular 2 RC as of this edit. Instead, when using Angular 2 RC, replace references to router
with router-deprecated
to continue using the beta router.
The Angular2-future way to implement this will be via the @Resolve decorator. Until then, the closest facsimile is CanActivate
Component decorator, per Brandon Roberts. see https://github.com/angular/angular/issues/6611
Although beta 0 doesn't support providing resolved values to the Component, it's planned, and there is also a workaround described here: Using Resolve In Angular2 Routes
A beta 1 example can be found here: http://run.plnkr.co/BAqA98lphi4rQZAd/#/resolved . It uses a very similar workaround, but slightly more accurately uses the RouteData
object rather than RouteParams
.
@CanActivate((to) => {
return new Promise((resolve) => {
to.routeData.data.user = { name: 'John' }
Also, note that there is also an example workaround for accessing nested/parent route "resolved" values as well, and other features you expect if you've used 1.x UI-Router.
Note you'll also need to manually inject any services you need to accomplish this, since the Angular Injector hierarchy is not currently available in the CanActivate decorator. Simply importing an Injector will create a new injector instance, without access to the providers from bootstrap()
, so you'll probably want to store an application-wide copy of the bootstrapped injector. Brandon's second Plunk link on this page is a good starting point: https://github.com/angular/angular/issues/4112
Set a local value with the observer
...also, don't forget to initialize the value with dummy data to avoid uninitialized
errors.
export class ModelService {
constructor() {
this.mode = new Model();
this._http.get('/api/v1/cats')
.map(res => res.json())
.subscribe(
json => {
this.model = new Model(json);
},
error => console.log(error);
);
}
}
This assumes Model, is a data model representing the structure of your data.
Model with no parameters should create a new instance with all values initialized (but empty). That way, if the template renders before the data is received it won't throw an error.
Ideally, if you want to persist the data to avoid unnecessary http requests you should put this in an object that has its own observer that you can subscribe to.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With