Multiple select inputs in table header with unique models

<div id='app'>
<table class="table table-bordered" v-for="(file, index) in fileData" :key="index">
    <thead>
  <tr>
    <th scope="col" v-for="(col, index2) in file[index]" :key="index2+index">
      <b-form-select v-model="selectedValue[index+index2]" :options="options">
        <template v-slot:first>
          <b-form-select-option :value="null" disabled>Ignore</b-form-select-option>
        </template>
      </b-form-select>
    </th>
  </tr>
</thead>
<tbody>
  <tr v-for="(row, index2) in file" :key="index2">
    <td v-for="(data, index3) in row" :key="index3">
      {{ data }}
    </td>
  </tr>
</tbody>

data: {
selectedValue: [],
mappedColumns: [],
selected: null,
options: [{
    value: '1',
    text: 'Option 1'
  },
  {
    value: '2',
    text: 'Option 2'
  },
  {
    value: '3',
    text: 'Option 3'
  }
],

You are using a single value in v-model for all the dropdowns. so, once you change a single dropdown. All of them gets changed.

Try the above solution, where I declared a new array in data which is selectedValue You can keep the data of which dropdown is selected in this array


Use v-on:change and a function instead of v-model Here is the solution for individual selection

new Vue({
  el: "#app",
  data: {
    mappedColumns: [],
    selected: null,
    options: [{
        value: '1',
        text: 'Option 1'
      },
      {
        value: '2',
        text: 'Option 2'
      },
      {
        value: '3',
        text: 'Option 3'
      }
    ],
    fileData: [
      [
        ["123", "21/11/2013", "Data", "Data"],
        ["234", "22/11/2013", "Data", "Data"],
        ["345", "12/09/2018", "Data", "Data"],
      ],
      [
        ["123", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data",
          "Data"
        ],
        ["234", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data",
          "Data"
        ],
        ["345", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data", "Data",
          "Data"
        ]
      ]
    ]
  },
  methods: {
    getSelectedItem(a, b, c) {
      console.log(a, b, c);
    }
  }
})
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/tether.min.css">
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/bootstrap-vue.css">
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css">
  <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/js/tether.min.js"></script>
  <script src="https://unpkg.com/[email protected]/dist/bootstrap-vue.js"></script>
  <title>Document</title>
  <style>
    #app {
      padding: 20px;
      height: 500px;
    }
  </style>
</head>

<body>
  <div id='app'>
    <table class="table table-bordered" v-for="(file, index) in fileData" :key="index">
      <thead>
        <tr>
          <th scope="col" v-for="(col, index2) in file[index]" :key="index2">
            <b-form-select v-on:change="getSelectedItem($event,index,index2)" :options="options">
              <template v-slot:first>
                                <b-form-select-option :value="null" disabled>Ignore</b-form-select-option>
                            </template>
            </b-form-select>
          </th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(row, index2) in file" :key="index2">
          <td v-for="(data, index3) in row" :key="index3">
            {{ data }}
          </td>
        </tr>
      </tbody>
    </table>
  </div>
  <script src="table.js"></script>

</body>

</html>


I guess you can make this a component

The header:

Possible usage:

<thead
  is="THeadSelect"
  :options="header_row"
  :length="length /*defaults to options.length*/"
  :headers.sync="headers"
></thead>

The ComponentOptions

//*.js
const THeadSelect = {
  template: "#theadselect",
  mounted(){
    // make sure headers is populated
    this.$emit("update:headers",
     this.headers
     .concat(Array.from({length:this.length_}, _=>""))
     .slice()
    )
  },
  props: {
    options: {
      type: Array,
      required: true,
    },
    length: Number,
    headers: {
      type: Array,
      required: true
    }
  },
  computed:{
    length_:{
      get(){
        return this.length || this.options.length
      },
      set(l){
        this.$emit("update:length",l)
      }
    },
    filteredOptions(){
      return this.options.filter(
        option => !this.headers.includes(option)
      )
    }
  }
}

The template

// *.html 
<template id="theadselect">
  <thead>
    <tr>
      <th
        v-for="(i,index) in length_"
        :key="index"
      >
        <select 
          v-model="headers[index]">
          <option disabled value="">
            Please select one
          </option>

          <option
            v-if="headers[index]"
            selected
          >
            {{headers[index]}}
          </option>
          <option
           v-for="option in filteredOptions"
           :key="option"
          >
            {{option}}
          </option>
        </select>
      </th>
    </tr>
  </thead>
</template>

Example

const THeadSelect = {
  template: "#theadselect",
  mounted(){
    // make sure headers is populated
    this.$emit("update:headers",
     this.headers
     .concat(Array.from({length:this.length_}, _=>""))
     .slice()
    )
  },
  props: {
    options: {
      type: Array,
      required: true,
    },
    length: Number,
    headers: {
      type: Array,
      required: true
    }
  },
  computed:{
    length_:{
      get(){
        return this.length || this.options.length
      },
      set(l){
        this.$emit("update:length",l)
      }
    },
    filteredOptions(){
      return this.options.filter(
        option => !this.headers.includes(option)
      )
    }
  }
}

new Vue({

  components: {THeadSelect},
  data(){
    return {
      headers: [],
      length: 10
    }
  },
  template: "#root"
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template id="theadselect">
  <thead>
    <tr>
      <th
        v-for="(i,index) in length_"
        :key="index"
      >
        <select 
          v-model="headers[index]">
          <option value="">
            Please select one
          </option>
          <option
            v-if="headers[index]"
            selected
          >
            {{headers[index]}}
          </option>
          <option
           v-for="option in filteredOptions"
           :key="option"
          >
            {{option}}
          </option>
        </select>
      </th>
    </tr>
  </thead>
</template>

<template id="root">
  <div>
  <table>
    <caption>Sample usage with select</caption>
    <thead 
      is="THeadSelect"
      :options="['option1', 'option2', 'option3']"
      :headers.sync="headers"
    ></thead>
    <tbody>
      <tr>
        <td
          v-for="(prop, index) in headers"
          :key="prop+index"
        >
          {{ prop || '?'}}
        </td>
      </tr>
    </tbody>
  </table>
  </div>
</template>
<table id="app"></table>

For the body one can think about the value of the headers array. Maybe put array indeces or object properties instead of currently option values

So for multiple tables one can think about:

<template id="table">
  <table

    v-for="(table, index) in tables"
    :key="'table-'+index"
    is="TableSelect"
    :headers="table[0]"
    :rows="table.slice(1)"
  >
  </table>
</template>

And for TableSelect:

const TableSelect = {
  props: ["headers", "rows"],
  template: "#table-select",
  data(){
    return {
      selectedHeaders: []
    }
  },
  computed(){
    mappedRows(){
      return this.rows
      .map(row=> row.map(
        (cell, index) => ({[headers[index]]: cell})
      ).reduce((obj, val) => Object.assign(obj, val))
    )}
  }
}
<template id="table-select">
  <table>
    <thead
      is="THeadSelect"
      :options="headers"
      :headers.sync="selectedHeaders"
    ></thead>
    <tbody>
      <tr
        v-for="(row, index) in mappedRows"
        :key="'row-'+index"
      >
        <td
          v-for="cell in selectedHeaders"
          :key="cell+index"
        >
          {{cell && row[cell || ""]}}
        </td>
      </tr>
    </tbody>
  </table>
</template>

there are errors in above code but due to lazyness and missing linter on so i will let it be - as it provides the basic idea.


And a running example on codesandbox:

https://lbm8l.csb.app/

Tags:

Vue.Js