Bootstrap Vue prevent / cancel event

Viewed 1789

I have a page with a b-form-select control from Bootstrap Vue. When I change the value of this control I want to execute a function that can possibly cancel this event, and not change the value.

To make things more complicated, my select is in a child component and the function I execute is in the parent.

The child component

public values: any[] = [
  { name: 'default'},
  { name: 'forbidden'},
  { name: 'other option' },
]

<b-form-select :value="property" @change="$emit('onPropertyChange', arguments[0])">
   <option v-for="(val, key) in values" :value="val" :key="key">{{val.Name}}</option>
</b-form-select>

The parent component:

this.property = { name: 'default' }
public onPropertyChange(newValue) {
  if (newValue.name === 'forbidden') {
    // Not changing this.property
  } else {
    // Changing it
    this.property = newValue
  }     
}

<child :property="property" @onPropertyChange="onPropertyChange"></child>

When I select 'forbidden' in the select. I see that the select box has changed to this value, but the property in the child and also the property in the parent still have the old value, which is what I want. How can I also have the select to have the old value?

Bootstrap Vue does not seem to have a prevent modifier on the change event. I have also tried using the native event but that has the same issue.

Am I doing something wrong?

2 Answers

It would probably be a better user experience to disable forbidden options than to mysteriously have them not select the selected value, but you can get that behavior in your change event handler by saving the old value and restoring it on nextTick.

Note that your options structure isn't suited for use with b-form-select. I have made a computed that makes the right structure and handles setting disabled, and used that in the b-form-select.

new Vue({
  el: '#app',
  data: {
    selected: 'default',
    options: [{
        name: 'default',
      },
      {
        name: 'forbidden'
      },
      {
        name: 'other option'
      },
      {
        name: 'what I had before'
      },
    ]
  },
  computed: {
    selectOptions() {
      return this.options.map((opt) => ({
        text: opt.name,
        value: opt.name,
        disabled: opt.name === 'forbidden'
      }));
    }
  },
  methods: {
    onChange(newValue) {
      const oldValue = this.selected;

      if (newValue === 'what I had before') {
        this.$nextTick(() => {
          this.selected = oldValue;
        });
      }
    }
  }
});
<script src="https://unpkg.com/vue@latest/dist/vue.js"></script>
<script src="https://unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.js"></script>
<div id="app">

  <b-form-select v-model="selected" :options="selectOptions" @change="onChange">
  </b-form-select>

  <div>Selected: <strong>{{selected}}</strong></div>

</div>

I'd like to propose a solution based on $refs and computed getter/setter (but it can be achieved also with v-bind/v-on). There are valid use cases for such functionality, eg asking user for confirmation.

<template>
    <div>
        <b-form-select
            ref="mySelect"
            v-model="choiceHandler"
            :options="selectOptions"
        >
        </b-form-select>
    </div>
</template>
<script>
export default {
    data() {
        return {
            selectOptions: [
                { value: 1, text: "Choice 1" },
                { value: 2, text: "Choice 2" },
                { value: 3, text: "Choice 3" },
            ],
            storedChoice: 3,
        };
    },
    computed: {
        choiceHandler: {
            get() {
                return this.storedChoice;
            },
            set(newValue) {
                this.$refs.typeSelect.localValue = this.storedChoice; // this reverts displayed value to the one resulting from stored value
                let confirmDialog = new Promise((resolve) => {
                    // this promise is a mockup for an asycnc dialog returning true / false
                    setTimeout(() => {
                        resolve(true);
                    }, 1500);
                });
                confirmDialog.then((res) => {
                    if (res) {
                        this.storedChoice = newValue;
                    } else console.log("No changes");
                });
            },
        },
    },
};
</script>

We need to use a setter to prevent the user change from getting passed to data model/store. Prior to validation we set the localValue of the b-form-select to the value prior to user's interaction. If validation is positive, we update the value in the data model which makes Vue to refresh the b-form-select. If not, we do nothing.

This operation might trigger Vue warning about direct manipulation of the DOM (depending on what exactly you do here), however I think this is a justified use, because we are not trying to display data overriding our data model - on the contrary - we are preventing the browser from displaying data that are not yet stored in the model.

You can also check here my solution to the same problem using vanilla <select> tag (not BootstrapVue - it works slightly different) and v-bind/v-on instead of the getter/setter approach.

Related