PageRenderTime 45ms CodeModel.GetById 6ms app.highlight 34ms RepoModel.GetById 1ms app.codeStats 0ms

/source/guides/routing/loading-and-error-substates.md

https://github.com/mattmarcum/website
Markdown | 320 lines | 256 code | 64 blank | 0 comment | 0 complexity | b4de83b693ed3f46cb06ff32cfffb27f MD5 | raw file
  1In addition to the techniques described in the
  2[Asynchronous Routing Guide](/guides/routing/asynchronous-routing/),
  3the Ember Router provides powerful yet overridable
  4conventions for customizing asynchronous transitions
  5between routes by making use of `error` and `loading`
  6substates.
  7
  8## `loading` substates
  9
 10The Ember Router allows you to return promises from the various
 11`beforeModel`/`model`/`afterModel` hooks in the course of a transition
 12(described [here](/guides/routing/asynchronous-routing/)).
 13These promises pause the transition until they fulfill, at which point
 14the transition will resume.
 15
 16Consider the following:
 17
 18```js
 19App.Router.map(function() {
 20  this.resource('foo', function() { // -> FooRoute
 21    this.route('slowModel');        // -> FooSlowModelRoute
 22  });
 23});
 24
 25App.FooSlowModelRoute = Ember.Route.extend({
 26  model: function() {
 27    return somePromiseThatTakesAWhileToResolve();
 28  }
 29});
 30```
 31
 32If you navigate to `foo/slow_model`, and in `FooSlowModelRoute#model`,
 33you return an AJAX query promise that takes a long time to complete.
 34During this time, your UI isn't really giving you any feedback as to
 35what's happening; if you're entering this route after a full page
 36refresh, your UI will be entirely blank, as you have not actually
 37finished fully entering any route and haven't yet displayed any
 38templates; if you're navigating to `foo/slow_model` from another
 39route, you'll continue to see the templates from the previous route
 40until the model finish loading, and then, boom, suddenly all the
 41templates for `foo/slow_model` load.
 42
 43So, how can we provide some visual feedback during the transition?
 44
 45Ember provides a default implementation of the `loading` process that implements
 46the following loading substate behavior.
 47
 48```js
 49App.Router.map(function() {
 50  this.resource('foo', function() {       // -> FooRoute
 51    this.resource('foo.bar', function() { // -> FooBarRoute
 52      this.route('baz');                  // -> FooBarBazRoute
 53    });
 54  });
 55});
 56```
 57
 58If a route with the path `foo.bar.baz` returns a promise that doesn't immediately
 59resolve, Ember will try to find a `loading` route in the hierarchy
 60above `foo.bar.baz` that it can transition into, starting with
 61`foo.bar.baz`'s sibling:
 62
 631. `foo.bar.loading`
 642. `foo.loading`
 653. `loading`
 66
 67Ember will find a loading route at the above location if either a) a
 68Route subclass has been defined for such a route, e.g.
 69
 701. `App.FooBarLoadingRoute`
 712. `App.FooLoadingRoute`
 723. `App.LoadingRoute`
 73
 74or b) a properly-named loading template has been found, e.g.
 75
 761. `foo/bar/loading`
 772. `foo/loading`
 783. `loading`
 79
 80During a slow asynchronous transition, Ember will transition into the
 81first loading sub-state/route that it finds, if one exists. The
 82intermediate transition into the loading substate happens immediately
 83(synchronously), the URL won't be updated, and, unlike other transitions
 84that happen while another asynchronous transition is active, the
 85currently active async transition won't be aborted.
 86
 87After transitioning into a loading substate, the corresponding template
 88for that substate, if present, will be rendered into the main outlet of
 89the parent route, e.g. `foo.bar.loading`'s template would render into
 90`foo.bar`'s outlet. (This isn't particular to loading routes; all
 91routes behave this way by default.)
 92
 93Once the main async transition into `foo.bar.baz` completes, the loading
 94substate will be exited, its template torn down, `foo.bar.baz` will be
 95entered, and its templates rendered.
 96
 97### Eager vs. Lazy Async Transitions
 98
 99Loading substates are optional, but if you provide one,
100you are essentially telling Ember that you
101want this async transition to be "eager"; in the absence of destination
102route loading substates, the router will "lazily" remain on the pre-transition route
103while all of the destination routes' promises resolve, and only fully
104transition to the destination route (and renders its templates, etc.)
105once the transition is complete. But once you provide a destination
106route loading substate, you are opting into an "eager" transition, which
107is to say that, unlike the "lazy" default, you will eagerly exit the
108source routes (and tear down their templates, etc) in order to
109transition into this substate. URLs always update immediately unless the
110transition was aborted or redirected within the same run loop.
111
112This has implications on error handling, i.e. when a transition into
113another route fails, a lazy transition will (by default) just remain on the
114previous route, whereas an eager transition will have already left the
115pre-transition route to enter a loading substate.
116
117### The `loading` event
118
119If you return a promise from the various `beforeModel`/`model`/`afterModel` hooks,
120and it doesn't immediately resolve, a `loading` event will be fired on that route
121and bubble upward to `ApplicationRoute`.
122
123If the `loading` handler is not defined at the specific route,
124the event will continue to bubble above a transition's pivot
125route, providing the `ApplicationRoute` the opportunity to manage it.
126
127```js
128App.FooSlowModelRoute = Ember.Route.extend({
129  model: function() {
130    return somePromiseThatTakesAWhileToResolve();
131  },
132  actions: {
133    loading: function(transition, originRoute) {
134      //displayLoadingSpinner();
135
136      // Return true to bubble this event to `FooRoute`
137      // or `ApplicationRoute`.
138      return true;
139    }
140  }
141});
142```
143
144The `loading` handler provides the ability to decide what to do during
145the loading process. If the last loading handler is not defined
146or returns `true`, Ember will perform the loading substate behavior.
147
148```js
149App.ApplicationRoute = Ember.Route.extend({
150  actions: {
151    loading: function(transition, originRoute) {
152      displayLoadingSpinner();
153
154      // substate implementation when returning `true`
155      return true;
156    }
157  }
158});
159```
160
161## `error` substates
162
163Ember provides an analogous approach to `loading` substates in
164the case of errors encountered during a transition.
165
166Similar to how the default `loading` event handlers are implemented,
167the default `error` handlers will look for an appropriate error substate to
168enter, if one can be found.
169
170```js
171App.Router.map(function() {
172  this.resource('articles', function() { // -> ArticlesRoute
173    this.route('overview');              // -> ArticlesOverviewRoute
174  });
175});
176```
177
178For instance, an error thrown or rejecting promise returned from
179`ArticlesOverviewRoute#model` (or `beforeModel` or `afterModel`)
180will look for:
181
1821. Either `ArticlesErrorRoute` or `articles/error` template
1832. Either `ErrorRoute` or `error` template
184
185If one of the above is found, the router will immediately transition into
186that substate (without updating the URL). The "reason" for the error
187(i.e. the exception thrown or the promise reject value) will be passed
188to that error state as its `model`.
189
190If no viable error substates can be found, an error message will be
191logged.
192
193
194### `error` substates with dynamic segments
195
196Routes with dynamic segments are often mapped to a mental model of "two
197separate levels." Take for example:
198
199```js
200App.Router.map(function() {
201  this.resource('foo', {path: '/foo/:id'}, function() {
202    this.route('baz');
203  });
204});
205
206App.FooRoute = Ember.Route.extend({
207  model: function(params) {
208    return new Ember.RSVP.Promise(function(resolve, reject) {
209       reject("Error");
210    });
211  }
212});
213```
214
215In the URL hierarchy you would visit `/foo/12` which would result in rendering
216the `foo` template into the `application` template's `outlet`. In the event of
217an error while attempting to load the `foo` route you would also render the
218top-level `error` template into the `application` template's `outlet`. This is
219intentionally parallel behavior as the `foo` route is never successfully
220entered. In order to create a `foo` scope for errors and render `foo/error`
221into `foo`'s `outlet` you would need to split the dynamic segment:
222
223```js
224App.Router.map(function() {
225  this.resource('foo', {path: '/foo'}, function() {
226    this.resource('elem', {path: ':id'}, function() {
227      this.route('baz');
228    });
229  });
230});
231```
232
233[Example JSBin](http://jsbin.com/tujepi)
234
235
236### The `error` event
237
238If `ArticlesOverviewRoute#model` returns a promise that rejects (because, for
239instance, the server returned an error, or the user isn't logged in,
240etc.), an `error` event will fire on `ArticlesOverviewRoute` and bubble upward.
241This `error` event can be handled and used to display an error message,
242redirect to a login page, etc.
243
244
245```js
246App.ArticlesOverviewRoute = Ember.Route.extend({
247  model: function(params) {
248    return new Ember.RSVP.Promise(function(resolve, reject) {
249       reject("Error");
250    });
251  },
252  actions: {
253    error: function(error, transition) {
254
255      if (error && error.status === 400) {
256        // error substate and parent routes do not handle this error
257        return this.transitionTo('modelNotFound');
258      }
259
260      // Return true to bubble this event to any parent route.
261      return true;
262    }
263  }
264});
265```
266
267In analogy with the `loading` event, you could manage the `error` event
268at the Application level to perform any app logic and based on the
269result of the last `error` handler, Ember will decide if substate behavior
270must be performed or not.
271
272```js
273App.ApplicationRoute = Ember.Route.extend({
274  actions: {
275    error: function(error, transition) {
276
277      // Manage your errors
278      Ember.onerror(error);
279
280      // substate implementation when returning `true`
281      return true;
282
283    }
284  }
285});
286```
287
288
289## Legacy `LoadingRoute`
290
291Previous versions of Ember (somewhat inadvertently) allowed you to define a global `LoadingRoute`
292which would be activated whenever a slow promise was encountered during
293a transition and exited upon completion of the transition. Because the
294`loading` template rendered as a top-level view and not within an
295outlet, it could be used for little more than displaying a loading
296spinner during slow transitions. Loading events/substates give you far
297more control, but if you'd like to emulate something similar to the legacy
298`LoadingRoute` behavior, you could do as follows:
299
300```js
301App.LoadingView = Ember.View.extend({
302  templateName: 'global-loading',
303  elementId: 'global-loading'
304});
305
306App.ApplicationRoute = Ember.Route.extend({
307  actions: {
308    loading: function() {
309      var view = this.container.lookup('view:loading').append();
310      this.router.one('didTransition', view, 'destroy');
311    }
312  }
313});
314```
315
316[Example JSBin](http://jsbin.com/leruqa)
317
318This will, like the legacy `LoadingRoute`, append a top-level view when the
319router goes into a loading state, and tear down the view once the
320transition finishes.