2

Using: ember 1.7.0

I have some server-side data that I want to load up in my ember app before any routes are transitioned to. Multiple (but not all) of my other routes / controllers need this data. I was thinking that I could just load up this data in the ApplicationRoute model method. It works fine, but does not display a loading state.

Is it possible to have the ApplicationRoute display a loading state until it's model promise gets resolved.

Here's a jsbin illustrating the problem: http://jsbin.com/soqivo/1/

Thanks for the help!

steakchaser
  • 5,198
  • 1
  • 26
  • 34
  • I was playing around with [this](http://jsbin.com/reyuti/5/edit?html,js,output) the other day, might help you out. This sample is kind of buggy because it was written by me lol, but written properly this should work for you. – MilkyWayJoe Oct 23 '14 at 20:57
  • Thanks for the sample. Same issue though - when I add a wait to the ApplicationRoute `model` it just shows a white screen. http://jsbin.com/titoz/1/ – steakchaser Oct 23 '14 at 21:21
  • Have a look at it http://emberjs.com/guides/routing/loading-and-error-substates/ – Susai Oct 24 '14 at 04:22

2 Answers2

1

Update:

As of 1.11.0 release, it should be possible to define a loading substate for the application route.

TL;DR;

I think this is by design, but not a design flaw. This particular issue is happening because that long model request is taking place in ApplicationRoute#model when it should be in IndexRoute#model. Move that promise/request into the index route and it should be fine. If you must add stuff to the application controller, consider this, combined with something that says "loading" in your index.html file while the app is waiting.

Why

Ember.Route has a number of hooks that we often override so it does what we want instead of a default implementation. The most obvious hook being model and setupController. But sometimes we simply don't write the setuptController method because it does what we want it to do already (given one just wants to set the model into the controller). But regardless of these methods being overridden, they will run as part of an internal workflow anyway. This workflow has a number of steps that are not often discussed because they already do what we want and we tend to forget about them and their importance, and, as for this particular issue, the order in which these methods get called in the route life cycle.

App = Ember.Application.create();

App.logs = Ember.ArrayProxy.create({
  content: []
});

App.Router.map(function() {
  this.resource('posts', function() {});
});

function loggingAlias(property) {
  return function() {
    App.logs.pushObject(this._debugContainerKey + ' ' + property);
    return this._super.apply(this, arguments);
  };
}

App.LoggingRoute = Ember.Route.extend({
  enter: loggingAlias('enter (private)'),
  exit: loggingAlias('exit (private)'),
  activate: loggingAlias('activate'),
  deactivate: loggingAlias('deactivate'),
  serialize: loggingAlias('serialize'),
  deserialize: loggingAlias('deserialize (private)'),
  model: loggingAlias('model'),
  setupController: loggingAlias('setupController'),
  afterModel: loggingAlias('afterModel'),
  beforeModel: loggingAlias('beforeModel'),
  renderTemplate: loggingAlias('renderTemplate'),
  redirect: loggingAlias('redirect')
});

App.LogsController = Ember.ArrayController.extend({
  content: App.logs,
  actions: {
    clearLogs: function() {
      App.logs.clear();
    }
  }
});

App.ApplicationRoute = App.LoggingRoute.extend();
App.PostsRoute = App.LoggingRoute.extend();
App.PostsIndexRoute = App.LoggingRoute.extend();
/* Put your CSS here */

html,
body {
  margin: 20px;
}
<!DOCTYPE html>
<html>

<head>
  <meta name="description" content="Ember Route Hook Order" />
  <meta charset="utf-8">
  <title>Ember Route Hook Order</title>
  <link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/normalize/2.1.0/normalize.css">
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
  <script src="http://builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v1.2.1.js"></script>
  <script src="http://builds.emberjs.com/beta/ember.js"></script>
</head>

<body>

  <script type="text/x-handlebars">

    <strong>Note:</strong>  <em>
    MilkyWayJoe says: I didn't write this. I have found in jsbin at: <a href="http://jsbin.com/rolo/2/edit?output">http://jsbin.com/rolo/2/edit?output</a>
    <br /> 
    Added here in case the link goes kaput just to point out the order of Ember.Route internal workflow steps 
    </em>
    <br />
    <br />{{link-to 'Index' 'index'}} {{link-to 'Posts' 'posts'}} {{outlet}} {{render 'logs'}}
  </script>

  <script type="text/x-handlebars" id='logs'>
    <h3>Logged Method Calls</h3>

    <a href="#" {{action 'clearLogs'}}>Clear Logs</a>
    <ul>
      {{#each}}
      <li>{{this}}</li>
      {{/each}}
    </ul>
  </script>
</body>

</html>

Since renderTemplate is the last to get called so it only makes sense that it doesn't render anything until the promise(s) within a given route get resolved.

For child routes, that's totally fine because their loading substate will have some type of canvas to draw onto, since a parent-already-loaded-living-and-breathing-route has been loaded prior this route even being instantiated. But that's not true for the ApplicationRoute since it has no parent route or template to rely upon, thus rendering a blank page until all promises get resolved.

Working snippet

The next best thing is moving any long running requests to a child route. As the proposed solution, I've moved your 3 sec promise to IndexRoute#model since this route will run anyway and is the direct child of ApplicationRoute by default. I would say reserve the application route or controller for handling error events instead.

App = Em.Application.create({
  displayName: 'Test.App'
});

App.Router.map(function() {
  this.resource('files', function() {
    this.route('index', {path: '/'});
    this.resource('file', { path: ':file_id' }, function() {
      this.route('index', {path: '/'});
      this.route('detail');
    });
  });
});

App.FilesController = Em.ArrayController.extend();
App.FilesFileController = Em.ObjectController.extend();

App.Person = Ember.Object.extend({}); 

App.IndexRoute = Ember.Route.extend({
    model: function(params, transition){
      return new Ember.RSVP.Promise(function(resolve){
        Ember.run.later(function(){ 
          var model =  App.Person.create();
          resolve(model);
        }, 3000); 
      });
    } 
});

App.LoadingRoute = Em.Route.extend({
  renderTemplate: function() {
    this.render('loading', {
      into: 'application',
      outlet: 'loading'
    });
  }
});

App.FileLoadingRoute = App.LoadingRoute.extend();
 
App.FilesRoute = Em.Route.extend({
  model: function() {
    var selfie = this;
    return new Ember.RSVP.Promise(function(resolve){
        Ember.run.later(function() {  
            var model = selfie.store.find('file');
            resolve(model);
        }, 800);  
    });
  }
});

App.FilesIndexRoute = Em.Route.extend({
  model: function(){
    return this.store.all('file');
  }
});

App.FileRoute = Em.Route.extend({
  model: function(params) { 
    return this.store.find('file', params.file_id);
  } 
});

App.FileIndexRoute = Em.Route.extend({
  model: function() {  
    return this.modelFor('file');  
  },
  renderTemplate: function() {
    
    this.render('files/index', {
      into: 'application'
    });
    
    this.render('file/index', {
      into: 'files/index',
      outlet: 'file'
    });
    
  }
});

App.FileDetailRoute = Em.Route.extend({
  model: function() {
    var selfie = this;
    return new Ember.RSVP.Promise(function(resolve){
        Ember.run.later(function(){ 
            var file = selfie.modelFor('file');
            var model = selfie.store.find('fileDetail', file.id);
            resolve(model);
        }, 800);  
    });
  },
  renderTemplate: function() {
    
    this.render('files/index', {
      into: 'application'
    });
    
    this.render('file/index', {
      into: 'files/index',
      outlet: 'file'
    });
    
    this.render('file/detail', {
      into: 'file/index',
      outlet: 'detail'
    });
  },
  actions: {
    loading: function() {
      return true;
    }
  }
});

App.RlLoadIndicatorComponent = Em.Component.extend({
  
  classNames: ['rl-load-indicator'],
  classNameBindings: ['isLoading:rl-overlay:rl-silent'],
  overlay: true,
  spinner: true,
  message: 'Loading...',
  loading: false,
  
  isLoading: function() {
    return this.get('loading');
  }.property('loading'),
  
  spinnerClass: function() {
    if (this.get('loading')) {
      if (this.get('spinner')) {
        return 'rl-spinner';
      }
    }
    return "";
  }.property(),
  
  actions: {
    setLoading: function() {
      this.set('loading', true);
    },
    setDone: function() {
      this.set('loading', false);
    }
  }
  
});

App.ApplicationAdapter = DS.FixtureAdapter.extend();

App.File = DS.Model.extend({
  name: DS.attr('string'), 
  text: DS.attr('string'),
  detail: DS.belongsTo('fileDetail', {async: true})
});

App.FileDetail = DS.Model.extend({
  owner: DS.attr('string'),
  canEdit: DS.attr('bool'),
  file: DS.belongsTo('file'),
  property1: DS.attr('string'),
  property2: DS.attr('string'),
  property3: DS.attr('string'),
  property4: DS.attr('string'),
  property5: DS.attr('string')
});

App.File.FIXTURES = [
  {id: 1, name: 'File 1', text: 'Blah 1', detail: 1},
  {id: 2, name: 'File 2', text: 'Blah 2', detail: 2},
  {id: 3, name: 'File 3', text: 'Blah 3', detail: 3},
  {id: 4, name: 'File 4', text: 'Blah 4', detail: 4},
  {id: 5, name: 'File 5', text: 'Blah 5', detail: 5},
  {id: 6, name: 'File 6', text: 'Blah 6', detail: 6},
  {id: 7, name: 'File 7', text: 'Blah 7', detail: 7},
  {id: 8, name: 'File 8', text: 'Blah 8', detail: 8},
  {id: 9, name: 'File 9', text: 'Blah 9', detail: 9},
  {id: 10, name: 'File 10', text: 'Blah 10', detail: 10}
];

App.FileDetail.FIXTURES = [
  {
    id: 1, 
    owner: 'Spiderman', 
    canEdit: true, 
    file_id: 1, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'With great values, comes great bindings'
  },
  {
    id: 2, 
    owner: 'Iron Man', 
    canEdit: true, 
    file_id: 2, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Another Value'
  },
  {
    id: 3, 
    owner: 'Thor', 
    canEdit: false, 
    file_id: 3, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Another Value'
  },
  {
    id: 4, 
    owner: 'Captain America', 
    canEdit: false, 
    file_id: 4, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Another Value'
  },
  {
    id: 5, 
    owner: 'Neil DeGrasse Tyson', 
    canEdit: true, 
    file_id: 5, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Another Value'
  },
  {
    id: 6, 
    owner: 'Dr. Doom', 
    canEdit: false, 
    file_id: 6, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Another Value'
  },
  {
    id: 7, 
    owner: 'Reed Richards', 
    canEdit: true, 
    file_id: 7, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Another Value'
  },
  {
    id: 8, 
    owner: 'Walter White', 
    canEdit: true, 
    file_id: 8, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Say My Name!' 
  },
  {
    id: 9, 
    owner: 'Jesse Pinkmann', 
    canEdit: true, 
    file_id: 9, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Bitch'
  },
  {
    id: 10, 
    owner: 'Hawk Barton', 
    canEdit: false, 
    file_id: 10, 
    property1: 'Value 1', 
    property2: 'Value 2', 
    property3: 'Value 3', 
    property4: 'Value 4', 
    property5: 'Another Value' 
  }
];
/* Put your CSS here */
html, body {
  margin: 20px;
}

.rl-load-indicator {
  text-align: center;
}

.rl-overlay {
    position:fixed;
    top:0;
    left:0;
    right:0;
    bottom:0;
    background-color:rgba(0, 0, 0, 0.85);
    background: url(data:;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABl0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuNUmK/OAAAAATSURBVBhXY2RgYNgHxGAAYuwDAA78AjwwRoQYAAAAAElFTkSuQmCC) repeat scroll transparent\9; /* ie fallback png background image */
    z-index:9999;
    color:white;
}

.rl-silent {
  display: none;
  visibility: hidden;
}

.rl-spinner {
  width: 30px;
  height: 30px;
  background-color: #27ae60;
  margin: 100px auto;
  margin-bottom: 8px;
  -webkit-animation: rotateplane 1.2s infinite ease-in-out;
  animation: rotateplane 1.2s infinite ease-in-out;
}

.arrow-right {
 width: 0; 
 height: 0; 
 border-top: 5px solid transparent;
 border-bottom: 5px solid transparent;
 border-left: 5px solid green;
}

@-webkit-keyframes rotateplane {
  0% { -webkit-transform: perspective(120px) }
  50% { -webkit-transform: perspective(120px) rotateY(180deg) }
  100% { -webkit-transform: perspective(120px) rotateY(180deg)  rotateX(180deg) }
}

@keyframes rotateplane {
  0% { 
    transform: perspective(120px) rotateX(0deg) rotateY(0deg);
    -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg) 
  } 50% { 
    transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg);
    -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg) 
  } 100% { 
    transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
    -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg);
  }
}
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="Loading Thingy" />
  <meta charset="utf-8">
  <title>Ember Starter Kit</title>
  <link href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css" rel="stylesheet">
  <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
  <script src="http://builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v1.3.0.js"></script>
  <script src="http://builds.emberjs.com/tags/v1.7.0/ember.js"></script>
  <script src="http://builds.emberjs.com/beta/ember-data.js"></script>
</head>
<body>
 
  <script type="text/x-handlebars">
    <h1>{{unbound App.displayName}}</h1>
    {{partial "menu"}}
    <hr />
    {{outlet}} 
    {{outlet "loading"}} 
  </script>
 
  <script type="text/x-handlebars" data-template-name="loading">
  {{rl-load-indicator loading=true}}
  </script>
  
  <script type="text/x-handlebars" data-template-name="_menu">
{{#link-to 'index'}}Home{{/link-to}} |    {{#link-to 'files.index'}}Files{{/link-to}}
  </script>
  
  <script type="text/x-handlebars" data-template-name="index">
    <h3>Index</h3>
    Content goes here
  </script>
  
  <script type="text/x-handlebars" data-template-name="files/index">
    <h3>Files</h3>
    <table class="table table-hover">
      <thead>
        <tr>
          <th>Id</th>
          <th>Name</th>
          <th>&nbsp;</th>
        </tr>
      </thead>
      <tbody>
      {{#each file in model}}
        <tr>
          <td>{{file.id}}</td>
          <td>{{file.name}}</td>
          <td>
            {{#link-to 'file.index' file}}
            <p class="arrow-right"></p>
            {{/link-to}}
          </td>
        </tr>
      {{/each}}
      </tbody>
    </table>
    {{outlet "file"}} 
  </script>
  
  <script type="text/x-handlebars" data-template-name="file/index">
    <h3>{{name}}</h3>
    {{text}}
    <hr />{{#link-to 'file.detail'}}Detail{{/link-to}}
    {{outlet "detail"}}
    
  </script>
  
    <script type="text/x-handlebars" data-template-name="file/detail">
    <h5>Details</h5>
    <hr />
    <ul>
    <li>owner: {{owner}}</li>
    <li>can edit: {{canEdit}}</li>
    <li>property 1: {{property1}}</li>
    <li>property 2: {{property3}}</li>
    <li>property 3: {{property3}}</li>
    <li>property 4: {{property4}}</li>
    <li>property 5: {{property5}}</li>
    </script>
  
  
  <script type="text/x-handlebars" data-template-name="components/rl-load-indicator">
  <div {{bind-attr class=spinnerClass}}></div>
  {{unbound message}}
  </script>

</body>
</html>
Community
  • 1
  • 1
MilkyWayJoe
  • 9,082
  • 2
  • 38
  • 53
  • Thanks for the background on "why" this is by design when it comes to the ApplicationRoute. I think I'll probably take your suggestion of having some base html (outside of ember) which has the initial loading state. The reason I don't want to stuff this into the IndexRoute (or any other route), is because I want to make sure it always happens. If someone were to deep link into my app for a specific route they would then bypass the IndexRoute. – steakchaser Oct 24 '14 at 22:12
1

Just create a different resource below the application route, and do all of your loading there. Don't use the index route though, it is only hit when you hit the root of a particular resource (App.IndexRoute would be when you just hit the root of your application).

App = Ember.Application.create();

App.Router.map(function() {
  this.resource("top",function(){ // a place you can grab things for the app and block
    this.resource('home');  // a place you want to get when everything is ready
  });
});

App.ApplicationRoute = Ember.Route.extend();

App.IndexRoute = Ember.Route.extend({
  redirect: function() {
    this.transitionTo("home");
  }
});
App.TopRoute = Ember.Route.extend({
//   This does trigger a loading state
  model: function(params){
    return new Ember.RSVP.Promise(function(resolve){
        setTimeout(function(){ 
            resolve();
        }, 3000); // 3 second delay, wooh, your server is slow!!!
    });
  }
});

http://jsbin.com/mivul/edit?html,js,output

Kingpin2k
  • 47,277
  • 10
  • 78
  • 96
  • Thanks for your suggestion. Correct me if I'm wrong, but if I use any route other than the ApplicationRoute, the pre-loading of data would be bypassed if a user deep linked directly into some other route in my app? Maybe what I need is to just have all of my routes that need this pre-loaded data, inherit from some base class. – steakchaser Oct 24 '14 at 22:15
  • Incorrect :) if they hit a resource anywhere deeper in the top resource it will always resolve the top model before moving on to the next deeper resource. – Kingpin2k Oct 25 '14 at 04:35