Wait for Angular 2 to load/resolve model before rendering view/template
Asked Answered
C

6

75

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.

Cufic answered 11/1, 2016 at 21:55 Comment(7)
You can read lazy loading article from @TGHNonmoral
@Cufic the whole router just got deprecated and re-written. Hopefully they talk about this today at NgConf.Erleneerlewine
@Erleneerlewine you mean they will completely re-write the angular 2 router?Darees
@Darees it seems that way, take a look at this: angular.io/docs/ts/latest/guide/router-deprecated.htmlErleneerlewine
well... than they could also write the 'deprected beta angular 2' replaced by the 'RC angular 2' Lets see wether the RC router can handle infinite child routers like aureliajs ;-)Darees
I would be happy with the capabilities of UI-Router :D. Although, it may not be renamed because it's completely getting re-specced. It may just be that they plan some smaller degree of breaking changes that they weren't comfortable calling "RC". One we know they planned to include but is not complete yet is this Resolve feature.Cufic
FYI, in case you haven't seen it yet, here is the most current and authoritative information on this. github.com/angular/angular/issues/4015 Note it is committed for RC.Cufic
M
38

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:

  1. route.snapshot.paramMap which returns a string, or the
  2. 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.

Melloney answered 5/7, 2016 at 21:1 Comment(4)
It works, but in your component, how can you access the data that is returned by the resolve?Mastectomy
looks like you can get it from the snapshot https://mcmap.net/q/270581/-using-resolve-in-angular2-routesSkillern
Resolver doesn't work for the very first route in the application;Crosseyed
Your Code is a little bit outdated. Take a look at angular.io/api/router/Resolve for the newer Syntax.Redeem
Z
86

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

Zincate answered 12/1, 2016 at 7:48 Comment(7)
Thank you. This works well as a workaround, but ultimately I think it makes more sense to treat the object and all of its children as mandatory and just wait for it to be available to render, as suggested by other answers (if I can get them to work). Otherwise this decoration will be on every value in all of my pages, and I'm back to Angular 1.x functionality in this regard. It probably has performance issues (extra dirty checks) as well.Cufic
@Cufic I don't believe that to be the case. Please see my update answer and the ng2 docs.Zincate
The reason for my suspicion regarding performance is that the Observable (same would hold for a Promise) has already been unwrapped. Now the only way to determine when it has changed is with our trusty Angular 1.x mechanism, the dirty check. Compare this to the use of an Observable, which is supported by the async pipe in the template itself. Angular internally can (I suspect it does) wire up the pipe to so that no watchers are necessary, and the node is rendered only when an observable arrives. It is effectively a message bus with event-driven actions.Cufic
Another minor issue is that putting Elvis operators everywhere defeats to some degree the goal of the Angular team's change of heart, regarding silently swallowing errors. You've probably experienced issues with this yourself, finding that templates (and the application as a whole) are harder to debug when functions are expected to handle null values. Further, if I have to make a choice of what to display, for example in an A:B scenario, now I also have to implement additional ngIf logic. I would really like to get the "wait to render component" approach working.Cufic
This operator is now called "safe navigation operator".Piscatelli
This operator doesn't work for two way binding, which severely limits its utility for this purpose.Carriole
Sure. I agree that if you want to display the empty data, then this is the right thing to do. But as the "safe navigation operator" link you provided mentions, if the content should never be viewed uninitialized, then this approach doesn't make sense. In keeping with my original question, which specifies that I wish to wait to display the interaction until after the model is resolved, I've selected the other answer. Thank you for this one, it is not quite what I asked for, but obviously very useful to many other people here.Cufic
M
38

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:

  1. route.snapshot.paramMap which returns a string, or the
  2. 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.

Melloney answered 5/7, 2016 at 21:1 Comment(4)
It works, but in your component, how can you access the data that is returned by the resolve?Mastectomy
looks like you can get it from the snapshot https://mcmap.net/q/270581/-using-resolve-in-angular2-routesSkillern
Resolver doesn't work for the very first route in the application;Crosseyed
Your Code is a little bit outdated. Take a look at angular.io/api/router/Resolve for the newer Syntax.Redeem
C
7

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

Cufic answered 21/1, 2016 at 14:59 Comment(5)
Your reffered plnker: run.plnkr.co/BAqA98lphi4rQZAd/#/resolved isn't available anymore. Thank you for pushing this feature. Maybe you can add the current feature which is scheduled for the RC1 release issue to your answer: github.com/angular/angular/issues/4015Crosscountry
RouteData is immutable.Uptake
@HristoVenev: Does your comment (and associated downvote) relate to the applicability of the solution, while the Angular 2 Beta router is still current?Cufic
@Cufic RouteData is immutable even for the beta router (router-deprecated).Uptake
Yes, I'm aware that RouteData is documented as "immutable". Let's ignore for a moment what that means to a JavaScript property set. Let's get right to the heart of your point. Are saying the solution does not work, or that there is a better solution available? Last I checked, my application shows no defects in this area, this solution was suggested by the Angular team, no competing solution has been offered by anyone else. Although it would be nice to follow the documentation's guidance, Resolve is not yet implemented, so that guidance doesn't merit a downvote.Cufic
A
4

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.

Azalea answered 13/1, 2016 at 21:27 Comment(13)
This doesn't make sense to me, Evan. Doesn't this circumvent the better-performing Observable pattern of Angular 2, by flattening the observable sequence into a plain object that needs dirty checks? Also I'd really prefer not to have to maintain a bunch of empty initializers, one for each model in my client. I'm hoping there is another way.Cufic
I totally agree than an ngIf is also a functional workaround - and it requires less duplicated code than your other suggestion in cases where result objects are expected to be complex. However, it has the same weakness in that it unwraps the Observable and requires the template to perform dirty checks (or re-render unnecessarily). I know you disagreed with this in your last comment, but if you think further about it, I suspect you'll agree. See also my comment to @tehaaron, whose answer had a similar effect.Cufic
@Cufic Dirty checking in Angular2 is cheap but that's irrelevant because it doesn't answer the question. What you want is a prewarmed cache of data prior to page load so there's no re-render flash. To do that you'll need to have your data loaded as a separate service and you'll need a way to trigger the data to load prior to the page loading.Azalea
(cont) Services aren't constructed until the first time they're injected. So some other component has to trigger the construction. One way would be to inject the service into a parent component. Another would be to lazy load the route and somehow trigger the service to start prior to loading the component.Azalea
Dirty checking is always cheap at small volumes, and I have nothing against the usage of it in Angular 1.x. I agree the benefit of simplicity outweighs the cost. However, a fundamental architectural decision was made to support an Observable-driven mechanism in Angular 2 and the performance improvement on pages with large numbers of items was one reason. It is higher performance and still very easy to get "right" as a consumer. So, I'd like to use it.Cufic
A pre-warmed cache is a good idea, but is not actually what I need in my case. Users are selecting to navigate to a leaf on a potentially large tree. I do not plan to preload the entire object graph over time before allowing the first navigation click.Cufic
From Angular 2 code: ObservableStrategy.prototype.createSubscription = function(async, updateLatestValue) { return async_1.ObservableWrapper.subscribe(async, updateLatestValue, function(e) { throw e; }); };Cufic
@Cufic Here is actual working code incl a service for caching. github.com/evanplaice/evanplaice.com/blob/master/app/vitae/…. github.com/evanplaice/evanplaice.com/blob/master/app/vitae/….Azalea
@Cufic Dirty checking in AngularJS is -- by no means -- comparable to dirty checking in Angular2. I'm not sure of a better way to describe it. To prevent dirty checking of deeply nested data structures, immutability is the best option. Create a new object using the data from the old so the comparator only has to check references. egghead.io/lessons/….Azalea
Ultimately, though, dirty-checking is still dirty-checking. Using the node-specific notification offered by Observables makes sense to me if it's available. Another example of where it plays nicer, is in error handling. What happens when the resource doesn't load? Now you have to "un-navigate".Cufic
Re: your code example, I hear what you are saying. As a side note, I think the code you shared exposes some likely misuse scenarios resulting in defects. I'd be happy to discuss if you are interested. You could reduce your workload in addressing misuse by simply initializing an Observable as a concatenated value: http.get("etc").map(res => new FRESHModel(res.json())).startWith(new FRESHModel()), if I understand your intent.Cufic
@Cufic I'm all ears. Observables is a new concept to me so in many cases I don't know what I don't know. Would you mind filing an issue on my project so we can continue the discussion there. I'd prefer not to flood the comments here any more than we have already.Azalea
Let us continue this discussion in chat.Cufic
K
3

A nice solution that I've found is to do on UI something like:

<div *ngIf="vendorServicePricing && quantityPricing && service">
 ...Your page...
</div

Only when: vendorServicePricing, quantityPricing and service are loaded the page is rendered.

Kunstlied answered 22/6, 2017 at 18:31 Comment(2)
This should be on first place because it is the simplest solution. No need of Resolve or ?. "notnull-Operator"Redeem
What if you have a really big template? Store everything in a big IF clause? It'd be more nice to have a means of intercepting pre-render like in React and just return null until you have some data.Platinocyanide
E
2

Implement the routerOnActivate in your @Component and return your promise:

https://angular.io/docs/ts/latest/api/router/OnActivate-interface.html

EDIT: This explicitly does NOT work, although the current documentation can be a little hard to interpret on this topic. See Brandon's first comment here for more information: https://github.com/angular/angular/issues/6611

EDIT: The related information on the otherwise-usually-accurate Auth0 site is not correct: https://auth0.com/blog/2016/01/25/angular-2-series-part-4-component-router-in-depth/

EDIT: The angular team is planning a @Resolve decorator for this purpose.

Erleneerlewine answered 11/1, 2016 at 22:13 Comment(11)
Does returning a promise in ngOnInit have the same effect?Permission
I don't think so, but it wouldn't help if it does because that function gets called until after. From: angular.io/docs/ts/latest/api/core/… , "ngOnInit is called right after the directive's data-bound properties have been checked for the first time"Erleneerlewine
Interesting: It seems silly that they're using it in all of there demos if you can't initialize data with it.Permission
you can, to empty objects, the idea is that you show the view as quickly as possible, and render the details later, but I prefer to load the model first too.Erleneerlewine
This didn't fly on our first attempt. The object was not resolved when the page loads. I didn't inspect carefully yet, will try again in the morning.Cufic
If the request doesn't complete prior to the template rendering phase, it'll still crash. I don't think routerOnActivate is intended as a workaround for setting template values.Azalea
The doc says > * If routerOnActivate returns a promise, the route change will wait until the promise settles to * instantiate and activate child components.Shaveling
@Erleneerlewine I believe this is unnecessarily complicated compared to the Elvis operator for this specific use case. Do you know if there is an advantage to routerOnActivate for simply waiting for data to become available?Zincate
The elvis operator is not allowed on two way bindings.Erleneerlewine
@tehaaron: Also, if you consider how the Elvis operator must determine when an object of unknown type is resolved, vs. how an Observer is notified of updates, you'll see an inherent design intent and performance difference.Cufic
@Langley: I understand I must resolve it. We've done some more digging, and the exception while attempting to access the property is raised before the promise is resolved. I've returned the promise from onRouterActivate, and verified the hook is being called. The promise is returned, and is still unresolved after the exception. I see in Angular RouterOutlet code comments where the render is supposed to be deferred until the promise is fulfilled, but this is not the case.Cufic

© 2022 - 2024 — McMap. All rights reserved.