bekwam courses

Vue.js Related Selects

July 11, 2020

This article shows how to implement a pair of related HTML <select> controls. Selecting a value in one select determines the list of items available to the second.

This CodePen demonstrates the implementation. The select "Select a state" is filled with a list of US states. Selecting a US state will display a list of counties belonging to that state. If a state selection changes, the county selection is cleared.

See the Pen Vue Related Select Controls by Carl Walker (@walkerca) on CodePen.

Data

The source of the data is a de-normalized list. This is similar to a list that might be returned from a RESTful web service. The list is "flat" or "de-normalized" because the hierarchical state / county relationship is removed. Instead, each item in the data is a county and the US state is added as a category. The following is an excerpt from the data section listed fully in the CodePen.

  data() {
    return {
      selectedState: null,
      selectedCounty: null,
      counties: [
        {
           state: "Maryland",
           county: "Baltimore"
        },
        {
          state: "Maryland",
          county: "Frederick"
        },

Rather than maintain a separate list of US states for its select, the select is based on a computed property "states". This transforms the list of counties into a list of states using map(). Then, a Set constructor is used to remove the duplicates.

  computed: {
    states() {
      return new Set(this.counties.map( c => c.state));
    },

As presented earlier, the data section includes a pair of variables for recording the selections made by the user. Initially, these values are null. These are selectedState and selectedCounty which are v-modeled to their respective selects.

Browser Screenshot
Related HTML Selects

With selectedState, the following computed will produce the list of items for the dependent county select.

  selectedCounties() {
      if( this.selectedState == null ) {
        return [];
      }
      return this.counties
        .filter( 
          c => c.state == this.selectedState )
        .map( c => c.county );
  }

This computed filters on the selectedState. The result of the filter is the set of matching counties. (Set is not needed here since I know that each county is unique within a state.) With the states filtered, a map is used to replace the county object with just the county name.

Because these are v-modeled, the counties will change dynamically. However, a watcher is needed to reset the selectedCounty variable.

  watch: {
    selectedState() {
      this.selectedCounty = null;
    }
  }

These <select> blocks are from the template. Both are coded similarly which is an important point in this article. The presentation doesn't have to use special handlers or events. Rather, each one follows a familiar v-model / v-for pattern. This means that adding a third or fourth dependent select would not clutter any of the template.

<select v-model="selectedState">
	<option :value="null">Select a state</option>
	<option v-for="s in states" :key="s" :value="s">
	  {{ s }}
	</option>     
</select>

<select v-model="selectedCounty">
	<option :value="null">Select a county</option>
	<option v-for="c in selectedCounties" :key="c" :value="c">
	  {{ c }}
	</option>
</select>

I'm also using a Filter on the presentation of the null state selection. Two paragraphs show Selected State and Selected County. The Filter is applied using the bar syntax within a handlebars expression.

<p class="content">
  <span class="has-text-weight-bold">Selected State:</span> 
  {{ selectedState | noSelection }}</p>
<p class="content">
  <span class="has-text-weight-bold">Selected County:</span> 
  {{ selectedCounty | noSelection }}</p>

The code for the Filter checks for null. If null, then a special string is returned. Otherwise, the value passed into the Filter is returned.

  filters: {
    noSelection(val) {
      if( val == null ) {
        return "No Selection";
      }
      return val;
    }
  }

There are third-party component libraries to create related selects. I haven't seen the need to use any since this code is minimal and I can create my own component without much effort. While it's sometimes better to use something off-the-shelf, there is a learning curve and new dependencies needed by your project. This post shows that v-model and some computeds can implement the requirement without extensive DOM manipulations


Headshot of Carl Walker

By Carl Walker

President and Principal Consultant of Bekwam, Inc