Search code examples
ember.jsember-dataember-cli

Drop-down menu which changes the content on an other drop-down menu


I'd like to create a drop-down menu from a hasMany relation which filters the values of an other drop-down menu. I have ships which belongsTo companies and which haveMany cruises. The user of the webpage should be able to select nothing (the table displays all ships) or a company (the table displays just the ships of that company) or a specific ship (the table just displays one ship). If the user selected a specific company only ships of that company should be displayed in the ship drop-down menu. The table of ships should be updated too.

Screenshot of the ships index table.

How can I create a set of two drop-down menus (above or below the table) which behave like this example and filter the content of the table?

Screenshot of an drop down example.

I know this question is a tall order. I tried to break it down as much as possible. Thank you for your time and effort.

The code of my example application

ember new travel_agency
cd travel_agency
ember install:addon ember-cli-scaffold
ember g scaffold ship name:string
ember g scaffold company name:string
ember g scaffold cruise starts_at:date
ember generate adapter application
ember g http-mock ships
ember g http-mock companies
ember g http-mock cruises
ember install:addon ember-moment

app/adapters/application.js

import DS from 'ember-data';

export default DS.RESTAdapter.extend({
  'namespace': 'api'
});

app/models/company.js

import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string'),
  ships: DS.hasMany('ship', { async: true })  
});

app/models/ship.js

import DS from 'ember-data';

export default DS.Model.extend({
  name: DS.attr('string'),
  company: DS.belongsTo('company', { async: true }),
  cruises: DS.hasMany('cruise', { async: true })  
});

app/models/cruise.js

import DS from 'ember-data';

export default DS.Model.extend({
  startsAt: DS.attr('date'),
  ship: DS.belongsTo('ship', { async: true })  
});

server/mocks/ships.js

[...]
var shipList = [
  {"id":1,"name":"Carnival Fantasy","company":1,"cruises":[1,2]},
  {"id":2,"name":"Carnival Triumph","company":1,"cruises":[3,4]},
  {"id":3,"name":"Queen Mary 2","company":2,"cruises":[5]},
  {"id":4,"name":"Queen Elizabeth","company":2,"cruises":[6]},
  {"id":5,"name":"Norwegian Jewel","company":3,"cruises":[7,8]}
]
[...]

server/mocks/companies.js

[...]
var companyList = [
  {"id":1,"name":"Carnival"},
  {"id":2,"name":"Cunard"},
  {"id":3,"name":"Norwegian Cruise Line"}
]
[...]

server/mocks/cruises.js

[...]
var cruiseList = [
  {"id":1,"startsAt":"2014-10-01","ship":1},
  {"id":2,"startsAt":"2014-10-15","ship":1},
  {"id":3,"startsAt":"2014-10-30","ship":2},
  {"id":4,"startsAt":"2014-11-10","ship":2},
  {"id":5,"startsAt":"2014-11-20","ship":3},
  {"id":6,"startsAt":"2014-11-20","ship":4},
  {"id":7,"startsAt":"2014-10-20","ship":5},
  {"id":8,"startsAt":"2014-11-20","ship":5}
]
[...]

app/templates/ships/index.hbs

[...]
<tbody>
  {{#each ship in model}}
    <tr>
      <td>
        {{ship.name}}
      </td>
      <td>
        {{ship.company.name}}
      </td>
      <td>
        {{#each cruise in ship.cruises}}
          {{moment date "L" cruise.startsAt}}, 
        {{/each}}
      </td>
      <td>
        {{link-to "Edit" "ships.edit" ship}}
      </td>
      <td>
        {{link-to "Show" "ships.show" ship}}
      </td>
      <td>
        <a href="#" {{action "remove" ship}}>Remove</a>
      </td>
    </tr>
  {{/each}}
</tbody>
[...]

Solution

  • I'm not going to dive into your whole application. Because it doesn't have much to do with the functionality you want. If I understand correctly you want to filter the second option-list based on the value of the first.

    You'd need a template containing two option-lists

    {{view "select" content=model.colors value=selectedItem}}
    color: {{selectedItem}}
    {{view "select" content=filteredBoats optionLabelPath="content.boat"}}
    

    You'd need a route providing a model:

    App.IndexRoute = Ember.Route.extend({
      model: function() {
        return { 
          colors: [],
          boats: []
        };
      }
    });
    

    And then you need a controller wiring things up:

    App.IndexController = Ember.Controller.extend({
      filteredBoats: function(){
          var selI = this.get('selectedItem');
    
          return this.get('model.boats').filter(
            function(boat) { 
               return boat.color == selI; 
            });
      }.property('selectedItem')
    });
    

    Lets recap and see what happens.

    1. In the view we bind the result of the first option-list to selectedItem in the controller
    2. In the controller we create a computed property and filter model.boats based on it's value
    3. In the view we bind the second option-list to the computed property.

    See this jsbin.