Building Related Selects with Vue.js

This post is more than 2 years old.

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.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Faisal Mohammed posted on 1/12/2019 at 1:52 AM

thanks. exactly what i was looking for

Comment 2 by MMMuhd posted on 4/10/2019 at 1:26 PM

Good

Comment 3 by Wasiq posted on 7/25/2019 at 8:27 AM

how we can do that in loop like i have a rows of country and city and i need to select city base on country but i have 10 rows of both dropdowns

Comment 4 (In reply to #3) by Raymond Camden posted on 7/25/2019 at 5:23 PM

What did you try? My example shows how to associate "sub options" with a top level option, so you could modify it to be countries and cities.

Comment 5 by xilem89 posted on 2/2/2020 at 12:39 PM

Thank you very much. This explanation really helpful for a beginner like me.

Comment 6 by xilem89 posted on 2/3/2020 at 7:37 AM

I found 1 problem for this method https://codepen.io/__ted__/... the output of selectedOption did not change accordingly. It still maintain the old value until other option in other drinks are set. is it because of vue reactivity?

Comment 7 (In reply to #6) by Raymond Camden posted on 2/3/2020 at 8:12 PM

I'm not exactly sure what you mean. It sounds like you are saying that when you pick the first option... it isn't really selected? But that doesn't make sense as then the second dropdown wouldn't work. Can you reword your question?

Comment 8 (In reply to #7) by xilem89 posted on 2/4/2020 at 2:10 PM

sorry for my bad english.
This is the example.

https://codepen.io/dreamtan... .

The second dropdown did change accordingly, but for the third dropdown the state remains the same even after we change the first dropdown.

Comment 9 (In reply to #8) by Raymond Camden posted on 2/4/2020 at 2:43 PM

Your code works for me. Some of the cars do NOT have models, and some of the models do NOT have options, so you won't get the related select for them, which is *expected*. When I pick Perodua, Alza, I get the final select with options.

Comment 10 (In reply to #9) by xilem89 posted on 2/6/2020 at 4:05 PM

I have updated my code. It works fine now. Feel free to test it. Thanks to your article I managed to solve it. https://codepen.io/dreamtan...

Comment 11 (In reply to #10) by Raymond Camden posted on 2/6/2020 at 4:09 PM

Glad to hear it!

Comment 12 by Jimbox_surf posted on 4/25/2020 at 10:24 AM

That's what I was looking for.

Just one question. In the third expample how can I set a default drink in the two selects?

Cheers,