1
votes

I'm having an issue where I'm unable to get nested outlets to appear properly in my Ember CLI app. The view tree I want is as follows:

  • application (list of all resources, of which client_availability is one)
  • - client_availabilities.index (list of client_availabilities)
  • -- client_availability (individual client_availability)

This is very similar to the "application > posts.index > post" hierarchy in the Ember Starter Kit. My desired behavior is for a list of client_availabilities to appear in "mainoutlet" when I navigate to client_availabilities.index, then persist when I bring up an individual client_availability in "suboutlet".

Easy, right? This is the default behavior & why we all love Ember. However, I can't seem to get it working. When I explicitly target my named suboutlet in client_availabilities.index and click on an individual client_availability, nothing shows up in either outlet:

Scenario 1: Render suboutlet inside client_availabilities

/app/template/application.hbs:

    {{link-to 'Client Availabilities' 'client_availabilities'}}

    {{outlet 'mainoutlet'}}

/app/template/client-availabilities/index.hbs:

    {{outlet 'suboutlet'}}

/app/routes/client-availabilities/index.js:

    import Ember from 'ember';

    export default Ember.Route.extend({

      renderTemplate: function(){
          this.render({
            into: "application",
            outlet: "mainoutlet"
          });
      },

      model: function() {
        return this.store.find('client_availability');
      }

    });

/app/routes/client-availability.js:

    import Ember from 'ember';

    export default Ember.Route.extend({

      renderTemplate: function(){
          this.render('client_availability', {
            into: "client_availabilities",
            outlet: "suboutlet"
          });
      },

      model: function(params) {
        return this.store.find('client_availability', params.client_availability_id);
      }

    });

Alternately, when I target my mainoutlet in application, client_availability appears in "suboutlet" client_availabilities.index disappears from "mainoutlet":

Scenario 2: Render suboutlet inside application

/app/template/application.hbs:

    {{link-to 'Client Availabilities' 'client_availabilities'}}

    {{outlet 'mainoutlet'}}

    {{outlet 'suboutlet'}}

/app/template/client-availabilities/index.hbs: (empty)

/app/routes/client-availabilities/index.js:

    import Ember from 'ember';

    export default Ember.Route.extend({

      renderTemplate: function(){
          this.render({
            into: "application",
            outlet: "mainoutlet"
          });
      },

      model: function() {
        return this.store.find('client_availability');
      }

    });

/app/routes/client-availability.js:

    import Ember from 'ember';

    export default Ember.Route.extend({

      renderTemplate: function(){
          this.render('client_availability', {
            into: "application",
            outlet: "suboutlet"
          });
      },

      model: function(params) {
        return this.store.find('client_availability', params.client_availability_id);
      }

    });

And here's my router, the same in both cases:

/app/router.js:

    import Ember from 'ember';

    var Router = Ember.Router.extend({
      location: 'auto'
    });

    Router.map(function() {
      this.resource('client_availabilities', function() {
        this.resource('client_availability', { path: ':client_availability_id' });
      });
    });

    export default Router;

I'm happy to share more code, but the application is split into several files and unfortunately not something I can post in its entirety. Can anyone see what I'm doing wrong? The rest of the app is working fine, I just can't seem to get this basic behavior to work.

1

1 Answers

8
votes

Do you have an /app/templates/client-availibilities.hbs template with only {{outlet}} inside of it? Without this, the app is going to lose its place in the outlet tree. Ember-CLI and the Ember Starter Kit are very, very different from each other in structure, so I can see where the confusion comes from.

How I like to think of Ember's rendering style is that each handlebars file inside the templates folder (i.e. /templates/users.hbs) represents a change the overall state of the application from one subject to another (example: from newsfeed to users). The corresponding subfolders inside the templates folder change the state of the subject itself.

For example:

  • Required Templates
    • Users container OR the only users page you need app-wide is at /templates/users.hbs
  • Optional Templates
    • Users Index would be at /templates/users/index.hbs
    • Users Show would be at /templates/users/show.hbs
    • Users New would be at /templates/users/new.hbs

You can have [ /templates/users.hbs ] without having [ /templates/users/*.hbs ] and still keep track of your data; however, you cannot have [ templates/users/index.hbs ] without [ /templates/users.hbs ] and still keep track of your data. Why? Imagine if you navigate to somesite.com/users. There is currently no top-level template with an outlet into which Ember can render the [ users/index.hbs ] template. The [ /templates/users.hbs ] template bridges that gap and also serves as a container for all other pages inside the /templates/users folder as well.

For example, in the terms of your app, in order to render [ /app/templates/client-availibilities/index.hbs ] when a user visits http://www.yourwebsite.com/client-availibilities, your app will need these templates defined so that ember can drill down into them.

application.hbs                     // and in its outlet, it will render...
--client-availibilities.hbs         // and in its outlet, it will render by default...
----client-availibilities/index.hbs // then, for the client-availability (singular), you can have ember render it in
----client-availibilities/show.hbs  // will render also in the client-availabilites as it is a separate state of the subject. Can also be nested inside the index route within the router so that it renders inside the index template.

As it is, I would structure your app as such...

/app/router.js

... // previous code

  Router.map(function() {
    this.resource('client_availabilities', function() {
      this.route('show', { path: '/:client_availability_id' });
      // this.route('new');  ! if needed !
      // this.route('edit', { path: '/:client_availability_id/edit' ); ! if needed !
    });
  });

... // code

/app/templates/application.hbs

{{link-to 'Client Availabilities' 'client_availabilities'}}

{{outlet}}

/app/templates/client-availabilities.hbs

{{outlet}}

/app/templates/client-availabilities/index.hbs

<ul>
  {{#each}}
    {{#if available}}       
      <li>
        {{#link-to #link-to 'client-availabilities.show' this}}
          {{firstName}} {{lastName}}
        {{/link-to}}
      </li>
    {{/if}}
  {{else}}  <!-- we want this to only render if the each loop returns nothing, which is why it's outside the if statement -->
    <li>Nobody is available</li>
  {{/each}}
</ul>

<!-- Note: you don't need to put an outlet here because you're at the end of the tree -->

/app/templates/client-availabilities/show.hbs

<!-- Everything you want to show about each availability -->>
<!-- Note: you don't need to put an outlet here because you're at the end of the tree -->

/app/routes/client-availabilities/index.js

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return this.store.findAll('client_availability');
  }
});

/app/routes/client-availabilities/show.js

import Ember from 'ember';

export default Ember.Route.extend({
  model: function(params) {
    return this.store.find('client-availability', params.client_availability_id);
  }
});

/app/models/client-availability.js

import DS from 'ember-data';

var client-availability = DS.Model.extend({
  firstName: DS.attr('string'),
  lastname:  DS.attr('string'), 
  available: DS.attr('boolean'),
  available_on: DS.attr('date')
});

export default client-availability;

However, are you sure you want to structure your app by the availability of each client? Wouldn't it make more sense to structure it by each client and then just filter each client to show if they were available or not? Resources are supposed to be nouns, and routes are supposed to be adjectives. Therefore, it would be best to use a client as your model instead of their availability and have a either an isAvailable property on the model (as used in the example above) or a one-to-many association with an additional availability model if you want to show clients who have several availabilities (as shown below).

For example,

/app/models/client.js

import DS from 'ember-data';

var Client = DS.Model.extend({
  firstName: DS.attr('string'),
  lastName:  DS.attr('string'), 
  availabilities: DS.hasMany('availability')
});

export default Client;

/app/models/availability.js

 import DS from 'ember-data';

 var Availability = DS.Model.extend({
   date:   DS.attr('date'),
   client: DS.belongsTo('client')
 });

 export default Availability;

In the long run, this latter approach would set up your app to show all availabilities at once and allow the user to filter by the client, plus it would allow the user to view a client and see all their availabilities. With the original approach (the isAvailable property on the client model), the user can only get the availabilities from the client model itself, but what if the user wants to see all clients who are available on, say, March 3rd at noon? Well, without an availability model associated with the client model, you are going to have to put a lot of code into your client controller that ember would give you by default if you go down the one-to-many path.

If you need more advice on where to go from here, let me know. I'm more than happy to add more examples of the templates, controllers, and routes that you'll need in order to pull this off.