App Initialization flow


#1

Just a heads up, this will require some effort to understand. I’ll try my best to explain the issue.

Let’s say the user goes directly to http://some-app.com/#/receipes

The initialisation happens like so:

  • ApplicationRoute
  • beforeModel
  • model
  • afterModel
  • RecipesRoute
  • beforeModel
  • model
  • afterModel
  • ApplicationRoute
  • activate
  • setupController
  • RecipesRoute
  • activate
  • setupController

The logic of the RecipesRoute does the following.

  1. It needs to know a selected ingredient which is stored on the server and is loaded by the ApplicationRoute.
  2. If it gets the selected ingredient it will load the recipes containing that ingredient.
  3. If it does not get the selected ingredient it redirects to a route where an ingredient is created.

The above logic executes in the model hook. If you refer to the initialisation order above, you will notice that the application’s controller has not been setup yet. So the only way to get the selected ingredient is to call modelFor(‘application’). This works great if the selected ingredient exists on the server and has been successfully retrieved by the ApplicationRoute’s model hook.

Now, when there’s no selected ingredient has been stored on the server the ApplicationRoute’s model hook returns null.

RecipesRoute’s model hook calls modelFor(‘application’) and gets null. Then it redirects the user to the a route where a user can create a selected ingredient. Once the ingredient is created and selected we store it on the server and then on the ApplicationController.model property;

Everything is hunky dory :smiley:

Then later, the user navigates to the RecipesRoute again, thus executing the model hook. Remember our logic? We call modelFor(‘application’) and we get null again. This is because modelFor method returns a value from the resolvedModels hash stored on the transition. Below is the original code from ember.js

modelFor: function(name) {

  var route = this.container.lookup('route:' + name),
      transition = this.router.router.activeTransition;

  // If we are mid-transition, we want to try and look up
  // resolved parent contexts on the current transitionEvent.
  if (transition) {
    var modelLookupName = (route && route.routeName) || name;
    if (transition.resolvedModels.hasOwnProperty(modelLookupName)) {
      return transition.resolvedModels[modelLookupName];
    }
  }

  return route && route.currentModel;
}

Below is an attempt to create logic with a bunch of IF statements in the RecipesRoute’s model hook to work around the issue.

model: function() {
  var model = this.modelFor('application');
  var controller = this.controllerFor('application').get('model');
	
  if (model === null)
  {
    if (controller === null)
    {
       this.transitionTo('ingredients.new');
    }
    else if (controller !== null)
    {
       this.getRecipes(controller.id);
    }
  }
  else if (model !== null)
  {
    if (controller !== null)
    {
      this.getRecipes(controller.id);
    }
    else if (controller === null)
    {
      // this particular case is very tricky
      // as it can have two scenarios

      // First, is when this is the first time model hook is triggered
      // and the controller has not been setup yet.
      // In this case we want to use the model.id
      
      // Second, is when the selected ingredient was deleted and
      // ApplicationController.model property was set 
      // to null deliberately.
      // In this case we don't want to use model.id as it is old

      // In my opinion, to make a distinction between the two, 
      // new variables would need to be added.
    }
  }
}

I feel like it is a relatively common scenario.

An object needs to be selected in order for the application to perform various tasks with it. And if it isn’t selected then make the user select/create it.

I’d like to see an Ember way solution.

The issue I see is that in the Route’s model hook there’s no way to store values on the Controller as it is simply not ready during the initialisation of the app. I would love to be proven wrong though.


#2

Although I am not entirely happy with it from a code future readability point of view, currently I push from the Application model hook to an attribute on the child route’s singleton controller (via a promise). For example:

var ApplicationRoute = Ember.Route.extend({

  model: function() {
    var recipesController = this.controllerFor('recipes');
    var promise = Ember.RSVP.hash({ // I use hash in my app because I am looking up more than 1 thing here.
      selectedIngredient: store.findQuery(...)
    }).then(function(data)) {
      recipesController.set('selectedIngredient', data.selectedIngredient);
      // or you can do recipesController.reopen(data) and set multiple things at once
    });
    return promise;
  }
})

#3

Interesting. Although, your solution isn’t neat it made me discover an interesting fact.

controllerFor(‘application’) will always return an instance of the controller. However, the controller property is only set somewhere later. I would assume in setupController hook.

This brings another question. Is this by design? What would be the reason for not setting the controller property on the router from the very beginning? Since the instance of it (controller) has been created.


#4

Apologies, I got it mixed up with View. There’s not ‘controller’ property on the Route. That’s is why router.get(‘controller’) returns undefined.