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