Building Related Selects with Vue.js

My buddy Nic Raboy has been posting some great "how to do X" style posts on Vue.js lately and it's inspired me to do the same. With that in mind, I decided to work on what I thought would be a simple demo of "related selects" - this is a common UI interface where one drop down drives the content of another. While working on the demos though I ran into some interesting edge cases that helped me learn, so I hope what follows is useful. As always, remember that I'm learning and there are probably better ways of doing what I'm showing here. (In fact, once again my friend Ted Patrick shared an update to my code that I'll be including in the post.)

For my demo, I decided to try two basic examples. The first example would be static data. The second example would be Ajax driven such that every change of the initial drop down would require a quick Ajax call to load the contents of the second drop down. Let's look at the first example.

I'll begin with the markup:

<div id="app">

  <select v-model="selectedDrink" @change="selectDrink">
    <option v-for="(drink,index) in drinks" :value="index">{{ drink.label }}</option>
  </select>

  <select v-model="selectedOption" v-if="selectedDrink != -1">
    <option v-for="option in drinks[selectedDrink].options">{{ option }}</option>
  </select>

  <p v-if="selectedOption">
    You selected a {{ drinks[selectedDrink].label }} and specifically {{ selectedOption }}.
  </p>
</div>

The first drop down is bound to a variable called selectedDrink and is driven by a list of data called drinks. You'll see all of this in a moment when we switch to the JavaScript. Note that my for loop is getting both the drink value as well as the numeric index. You'll see how that's used soon too.

The second drop down is looping over what will be a list of options for a specific drink. Note the use of v-if to only show up when we've selected a drink.

Finally - there is a simple paragraph which states what you've selected. It is hidden as well until you've selected a particular value.

Now let's look at the code.

const app = new Vue({
  el:'#app',
  data:{
    drinks:[
      {
        label:"Beer",
        options:["Sam Adams","Anchor Steam","St. Arnold"]
      },
      {
        label:"Soda",
        options:["Pepsi","Coke","RC"]
      },
      {
        label:"Coffee",
        options:["Starbucks","Dunkin Donuts","Gross Hotel Room"]
      }
    ],
    
    selectedDrink:-1,
    selectedOption:''
  },
  methods:{
    selectDrink:function() {
      this.selectedOption = '';
    }
  }
});

As I mentioned, this first example is static, hard coded data. In this case, an array of simple objects where each drink has a label and an array of options for the values. The only real logic in play here is selectDrink. I'm resetting selectedOption back to blank. I do this to hide that final paragraph. All in all rather trivial. The issues I ran into were in v-if. Specifically the fact that when generating a set of options for a drop down, Vue will set the default value to undefined, and my conditionals were tripping up on that. This is expected but it tripped me up. You can play with the first version below:

See the Pen Related DDs by Raymond Camden (@cfjedimaster) on CodePen.

So I liked this, but I was kinda bugged by the amount of logic I had in the view (HTML) area. It isn't a lot, but it just felt like a bit too much. I worked on a second version to try to correct this. Here is the new HTML:

<div id="app">

  <select v-model="selectedDrink" @change="selectDrink">
    <option v-for="(drink,index) in drinks" :value="index">{{ drink.label }}</option>
  </select>

  <select v-model="selectedOption" v-if="options.length">
    <option v-for="option in options">{{ option }}</option>
  </select>

  <p v-if="selectedOption">
    You selected a {{ selectedDrinkLabel }} and specifically {{ selectedOption }}.
  </p>
</div>

The main changes are in the second two blocks of layout and mainly in that I don't assume as much knowledge about the 'form' of drinks. In fact, I'd like to change options.length in the second block as well. The JavaScript is just a bit different:

const app = new Vue({
  el:'#app',
  data:{
    drinks:[
      {
        label:"Beer",
        options:["Sam Adams","Anchor Steam","St. Arnold"]
      },
      {
        label:"Soda",
        options:["Pepsi","Coke","RC"]
      },
      {
        label:"Coffee",
        options:["Starbucks","Dunkin Donuts","Gross Hotel Room"]
      }
    ],
    
    selectedDrink:-1,
    selectedOption:'',
    options:[],
    selectedDrinkLabel:''
  },
  methods:{
    selectDrink:function() {
      this.selectedOption = '';
      this.options = this.drinks[this.selectedDrink].options;
      this.selectedDrinkLabel = this.drinks[this.selectedDrink].label;
    }
  }
});

Note how the selectDrink method now does a bit more work. Again, I like this as I feel like more of the logic should be here versus the layout. You can view this below:

See the Pen Related DDs (p2) by Raymond Camden (@cfjedimaster) on CodePen.

Finally, Ted Patrick shared a third version with me. Note that his is missing some of the logic of the second version. But check out the change:

<div id="app">

  <select v-model="selectedDrink">
    <option v-for="drink in drinks" :value="drink">{{ drink.label }}</option>
  </select>

  <select v-model="selectedOption" v-if="selectedDrink != -1">
    <option v-for="option in selectedDrink.options">{{ option }}</option>
  </select>

  <p v-if="selectedDrink&&selectedOption">
    You selected a {{ selectedDrink.label }} and specifically {{ selectedOption }}.
  </p>
  
</div>

Specifically note that the value of the drop down is the drink object itself. That's cool! I'm basically making a drop down where the value is some random JavaScript object of any shape. I really, really dig that! You can find his complete version below.

See the Pen Related DDs by Ted Patrick (@ted) on CodePen.

Ok, now for round two! For the second version, I wanted related drop downs where the related content was driven via a web service. In this case, I decided to build something using the Star Wars API. The Star Wars API has simple GET endpoints for different types of data, like films, people, etc. So I built a related select where the first drop down was the kind of data and the second was the actual data. (To keep it simple, I didn't worry about paging.) Here is the markup.

<div id="app">

  <h2>SWAPI Data</h2>
  
  <select v-model="selectedOption" @change="loadData">
    <option v-for="option in options">{{ option }}</option>
  </select>

  <div v-if="selectedOption && !items.length"><i>Loading</i></div>
  <select v-if="items.length">
    <option v-for="item in items">{{ item.label }}</option>
  </select>
  
</div>

For the most part this isn't too much different from the initial version, except for the removal of the third paragraph. I added a "loading" widget as well. Now let's look at the JavaScript.

const app = new Vue({
  el:'#app',
  data:{
    options:["films","people","starships","vehicles","species","planets"],
    items:[],
    selectedOption:''
  },
  methods:{
    loadData:function() {
      this.items = [];
      let key = 'name';
      if(this.selectedOption === 'films') key = 'title';
      
      fetch('https://swapi.co/api/'+this.selectedOption)
      .then(res=>res.json())
      .then(res => {
        // "fix" the data to set a label for all types
        this.items = res.results.map((item) =>{
              item.label = item[key];
              return item;
        });
       
      });
    }
  }
});

I begin by defining a hard coded list of data types. I initialize items to an empty array. This will get populated when you select a type. You can see that logic in loadData. I run a fetch call to the end point and that's basically it. I have a little bit of logic to help keep my view simple, in this case creating a label property that is based on the best "name" for the data. As you can see, only films is a bit weird, using title instead of name. That's basically it. Here it is in action:

See the Pen Related DDs (p3) by Raymond Camden (@cfjedimaster) on CodePen.

Note that I really should add a simple caching layer so that I don't refetch data I don't need to. Also, Ted again shared an updated version with some changes:

Anyway, let me know what you think. I'm also open to requests for what to cover next. My current plan is to show simple form validation with Vue.

Like This?

If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can also subscribe to the email feed to get notified of new posts.

See Also