[Vuejs]-How to collect selected BootstrapVue (b-form-checkbox) check boxes?

0👍

I advise you to think through your code, and have the logic steps ready:

  1. You put out a list of UI items, based on a list in the Vuex store
  2. Each UI item has a toggle (checkbox) that sets some part of the state of the item it displays (should it be removed or not)
  3. By clicking the toggle (checkbox), the state of the item should change
  4. By clicking the button (remove), the Vuex state should change: remove every item whose state fulfills a condition (it’s toRemove state is true)

That’s all. More simply put: don’t work by selecting some UI element. Work by updating the state of the data that the UI element is based upon. (And let Vue’s reactive capability do the rest.)

The first snippet is an easy one, not using Vuex:

Vue.component('RowWithCb', {
  props: ["id", "title", "checked"],
  computed: {
    isChecked: {
      get() {
        return this.checked
      },
      set(val) {
        this.$emit("update:checked", val)
      }
    },
  },
  template: `
    <b-row>
      <b-col
        class="d-flex"
      >
        <div>
          <b-checkbox
            v-model="isChecked"
          />
        </div>
        <div>
          {{ title }}
        </div>
      </b-col>
    </b-row>
  `
})
new Vue({
  el: "#app",
  computed: {
    toRemove() {
      return this.rows.filter(({
        checked
      }) => checked)
    },
    toKeep() {
      return this.rows.filter(({
        checked
      }) => !checked)
    },
  },
  data() {
    return {
      rows: [{
          id: 1,
          title: "Row 1",
          checked: false,
        },
        {
          id: 2,
          title: "Row 2",
          checked: false,
        }
      ]
    }
  },
  template: `
    <b-container>
      <b-row>
        <b-col>
          <h3>Items:</h3>
          <row-with-cb
            v-for="row in rows"
            :key="row.id"
            v-bind="{
             ...row
            }"
            :checked.sync="row.checked"
          />
        </b-col>
      </b-row>
      <hr />
      <b-row>
        <b-col>
          <h4>To keep:</h4>
          {{ toKeep }}
        </b-col>
        <b-col>
          <h4>To remove:</h4>
          {{ toRemove }}
        </b-col>
      </b-row>
    </b-container>
  `
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app"></div>

NOW WITH VUEX

const initialState = () => ({
  rows: [{
      id: 1,
      title: "Row 1",
      checked: false,
    },
    {
      id: 2,
      title: "Row 2",
      checked: false,
    }
  ]
})

const store = new Vuex.Store({
  state: initialState(),
  mutations: {
    RESET(state) {
      const init = initialState()
      for (key in init) {
        state[key] = init[key]
      }
    },
    SET_TO_REMOVE(state, {
      id,
      checked
    }) {
      state.rows = state.rows.map(row => {
        if (row.id === id) {
          return {
            ...row,
            checked,
          }
        } else {
          return row
        }
      })
    },
    REMOVE_SELECTED(state) {
      state.rows = state.rows.filter(({
        checked
      }) => !checked)
    },
  },
  actions: {
    reset({
      commit
    }) {
      commit("RESET")
    },
    setToRemove({
      commit
    }, data) {
      commit("SET_TO_REMOVE", data)
    },
    removeSelected({
      commit
    }) {
      commit("REMOVE_SELECTED")
    },
  },
  getters: {
    getRows: state => state.rows,
    getRowsToRemove: state => state.rows.filter(({
      checked
    }) => checked),
    getRowsToKeep: state => state.rows.filter(({
      checked
    }) => !checked),
  }
})

Vue.component('RowWithCb', {
  props: ["id", "title", "checked"],
  computed: {
    isChecked: {
      get() {
        return this.checked
      },
      set(val) {
        this.$emit("update:checked", {
          id: this.id,
          checked: val,
        })
      }
    },
  },
  template: `
    <b-row>
      <b-col
        class="d-flex"
      >
        <div>
          <b-checkbox
            v-model="isChecked"
          />
        </div>
        <div>
          {{ title }}
        </div>
      </b-col>
    </b-row>
  `
})
new Vue({
  el: "#app",
  store,
  computed: {
    rows() {
      return this.$store.getters.getRows
    },
    toRemove() {
      return this.$store.getters.getRowsToRemove
    },
    toKeep() {
      return this.$store.getters.getRowsToKeep
    },
  },
  methods: {
    handleSetChecked(data) {
      this.$store.dispatch("setToRemove", data)
    },
    handleRemoveItems() {
      this.$store.dispatch("removeSelected")
    },
    handleResetStore() {
      this.$store.dispatch("reset")
    },
  },
  template: `
    <b-container>
      <b-row>
        <b-col>
          <h3>Items:</h3>
          <row-with-cb
            v-for="row in rows"
            :key="row.id"
            v-bind="{
             ...row
            }"
            v-on="{
              'update:checked': (data) => handleSetChecked(data)
            }"
          />
        </b-col>
      </b-row>
      <hr />
      <b-row>
        <b-col>
          <h4>To keep:</h4>
          {{ toKeep }}
        </b-col>
        <b-col>
          <h4>To remove:</h4>
          {{ toRemove }}
        </b-col>
      </b-row>
      <hr />
      <b-row>
        <b-col>
          <b-button
            @click="handleRemoveItems"
          >
            REMOVE
          </b-button>
          <b-button
            @click="handleResetStore"
          >
            RESET
          </b-button>
        </b-col>
      </b-row>
    </b-container>
  `
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app"></div>

You can see that I did not mess with any checkboxes or UI elements (the only difference is that the id is also emitted from the UI component). All the functions were moved to the Vuex store & that’s all. (A bit more verbose than the first snippet, as I added the remove & the reset functionality.)


LET’S BREAK IT DOWN

1. You need a component that lists items. In this case, let’s use the root component for this:

new Vue({
  el: "#app",
  data() {
    return {
      baseItems: [{
          id: "item_1",
          name: "Item 1",
        },
        {
          id: "item_2",
          name: "Item 2",
        },
      ]
    }
  },
  computed: {
    items() {
      return this.baseItems
    },
  },
  template: `
    <ul>
      <li
        v-for="item in items"
        :key="item.id"
      >
        {{ item.name }}
      </li>
    </ul>
  `
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app"></div>

I deliberately put items as computed – it’s going to be ease to switch to Vuex.

2. You know that your listed items are going to be a bit more complex than this, so let’s create a component that can handle their upcoming complexity:

Vue.component('ListItem', {
  props: ['id', 'name'],
  template: `
    <li>
      {{ name }}
    </li>
  `
})

new Vue({
  el: "#app",
  data() {
    return {
      baseItems: [{
          id: "item_1",
          name: "Item 1",
        },
        {
          id: "item_2",
          name: "Item 2",
        },
      ]
    }
  },
  computed: {
    items() {
      return this.baseItems
    },
  },
  template: `
    <ul>
      <list-item
        v-for="item in items"
        :key="item.id"
        v-bind="{
          ...item,
        }"
      />
    </ul>
  `
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app"></div>

3. Let’s make them removable – one-by-one or in a bunch:

Vue.component('ListItem', {
  props: ['id', 'name', 'toRemove'],
  computed: {
    selected: {
      get() {
        return this.toRemove
      },
      set(val) {
        this.$emit('update:to-remove', val)
      }
    },
  },
  template: `
    <li>
      <input
        type="checkbox"
        v-model="selected"
      />
      {{ name }}
    </li>
  `
})

new Vue({
  el: "#app",
  data() {
    return {
      baseItems: [{
          id: "item_1",
          name: "Item 1",
          toRemove: false,
        },
        {
          id: "item_2",
          name: "Item 2",
          toRemove: false,
        },
      ]
    }
  },
  computed: {
    items() {
      return this.baseItems
    },
  },
  methods: {
    handleRemoveSelected() {
      this.baseItems = this.baseItems.filter(item => !item.toRemove)
    },
  },
  template: `
    <div>
      So, it's easier to follow:
      {{ items }}
      <hr />
      <ul>
        <list-item
          v-for="item in items"
          :key="item.id"
          v-bind="{
            ...item,
          }"
          :toRemove.sync="item.toRemove"
        />
      </ul>
      <b-button
        @click="handleRemoveSelected"
      >
        REMOVE SELECTED
      </b-button>
    </div>
  `
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app"></div>

OK, now there is the function you were after – without Vuex

4. Let’s mix in Vuex:

  • move the list to a Vuex state
  • move the selection to a Vuex action
  • move the removal to a Vuex action
// initial state is a function per Vuex best practices
const initialState = () => ({
  items: [{
      id: "item_1",
      name: "Item 1",
      toRemove: false,
    },
    {
      id: "item_2",
      name: "Item 2",
      toRemove: false,
    }
  ]
})

const store = new Vuex.Store({
  state: initialState(),
  mutations: {
    // so you are able to reset the store to
    // initial state
    RESET(state) {
      const init = initialState()
      for (key in init) {
        state[key] = init[key]
      }
    },
    // this handles the "checked" state if each item
    SET_TO_REMOVE(state, {
      id,
      toRemove,
    }) {
      // returning a new, modified array
      // per FLUX process
      state.items = state.items.map(item => {
        if (item.id === id) {
          return {
            ...item,
            toRemove,
          }
        } else {
          return item
        }
      })
    },
    // the same function that was in the app before
    REMOVE_SELECTED(state) {
      state.items = state.items.filter(({
        toRemove
      }) => !toRemove)
    },
  },
  actions: {
    reset({
      commit
    }) {
      commit("RESET")
    },
    // action to commit the mutation: select item for removal
    setToRemove({
      commit
    }, data) {
      commit("SET_TO_REMOVE", data)
    },
    // action to commit the mutation: remove items
    removeSelected({
      commit
    }) {
      commit("REMOVE_SELECTED")
    },
  },
  getters: {
    getItems: state => state.items,
  }
})

Vue.component('ListItem', {
  props: ['id', 'name', 'toRemove'],
  computed: {
    selected: {
      get() {
        return this.toRemove
      },
      set(val) {
        this.$emit('update:to-remove', val)
      }
    },
  },
  template: `
    <li>
      <input
        type="checkbox"
        v-model="selected"
      />
      {{ name }}
    </li>
  `
})

new Vue({
  el: "#app",
  store,
  data() {
    return {
      baseItems: [{
          id: "item_1",
          name: "Item 1",
          toRemove: false,
        },
        {
          id: "item_2",
          name: "Item 2",
          toRemove: false,
        },
      ]
    }
  },
  computed: {
    items() {
      return this.$store.getters.getItems
    },
  },
  methods: {
    handleSetToRemove(data) {
      // data should be: { id, toRemove }
      this.$store.dispatch("setToRemove", data)
    },
    handleRemoveSelected() {
      this.$store.dispatch("removeSelected")
    },
    handleResetStore() {
      this.$store.dispatch("reset")
    },
  },
  template: `
    <div>
      So, it's easier to follow:
      {{ items }}
      <hr />
      <ul>
        <list-item
          v-for="item in items"
          :key="item.id"
          v-bind="{
            ...item,
          }"
          @update:to-remove="(toRemove) => handleSetToRemove({
            id: item.id,
            toRemove,
          })"
        />
      </ul>
      <b-button
        @click="handleRemoveSelected"
      >
        REMOVE SELECTED
      </b-button>
      <b-button
        @click="handleResetStore"
      >
        RESET
      </b-button>
    </div>
  `
})
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<script src="//unpkg.com/vue@latest/dist/vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app"></div>

This is all there’s to it.

  1. You handle the selection in the small component – but the change of the selected state is NOT HANDLED inside this component, but emitted to the parent, which in turn handles this by calling a Vuex action. This changes the state in the Vuex store & that "flows" back through the props.

  2. If you click on REMOVE SELECTED, that doesn’t look at the components (if they are selected), but calls another Vuex action, that removes all selected items (by filtering) from the state of the Vuex store. As all items are put out from a getter of the Vuex store (and getters update if the underlying state changes), the new state appears in the list.

So, in this case, using Vuex might seem a bit overkill, but if you plan on a more complex state for the listed items, then it may be reasonable.

Leave a comment