How to prevent @change event when changing v-model value

12,897

Solution 1

@acdcjunior, thanks for your answer.

Of course just removing the reference to searcher() just means no action is taken upon field value change so the field won’t work at all.

Moving the searcher() function into methods: {} instead of computed: {} means that it will be called on an input event and not a change even (another mystery but not one for today). A subtle difference that takes away the typeahead feature I’m aiming at.

However, it did make me remember that the result of computed: {} functions are cached and will be re-computed when any parameters change. In this case I realised that the searcher() function is dependent upon the this.selectedTo variable. So when the selectToUser() function sets this.selectedTo it triggers another call to searcher().

Fixed now. In case anyone has a similar problem in the future, I resolved this by turning to old fashioned semaphore by adding another variable.

var userMadeSelection: false

Now, searcher() begins with a check for this scenario:

  computed: {
      searcher() {
          if(this.userMadeSelection) {
            this.userMadeSelection = false;
            return this.selectedTo;
          }
          …

and then in selectToUser():

this.userMadeSelection = true;

Solution 2

On:

<b-form-input id="toUser"
    type="text"
    v-model="selectedTo"
    @change="searcher">

The "searcher" should be a method. A method that will be called whenever that b-component issues a change event.

But looking at your code, it is not a method, but a computed:

computed: {
  searcher() {
    ...
  },

  showSearcherDropdown() {
    ...
  }
},

methods: {
  selectToUser: function( user ) {
   ...
  }
}

So when the change event happens, it tries to call something that is not a method (or, in other words, it tries to call a method that doesn't exist). That's why you get the error.

Now, since what you actually want is to update searcher whenever this.selectedTo changes, to get that, it is actually not needed to have that @change handler. This is due to the code of computed: { searcher() { already depending on this.selectedTo. Whenever this.selectedTo changes, Vue will calculate searcher again.

Solution: simply remove @change="searcher" from b-form. Everything else will work.

Share:
12,897
Capn Sparrow
Author by

Capn Sparrow

Trading systems and capital markets technology.

Updated on July 23, 2022

Comments

  • Capn Sparrow
    Capn Sparrow almost 2 years

    I'm building an auto-complete menu in Vue.js backed by Firebase (using vue-fire). The aim is to start typing a user's display name and having match records show up in the list of divs below.

    The template looks like this:

    <b-form-input id="toUser"
        type="text"
        v-model="selectedTo"
        @change="searcher">
    </b-form-input>
    
    <div v-on:click="selectToUser(user)" class="userSearchDropDownResult" v-for="user in searchResult" v-if="showSearcherDropdown">{{ user.name }}</div>
    

    Upon clicking a potential match the intention is to set the value of the field and clear away the list of matches.

    Here is the code portion of the component:

    computed: {
      /* method borrowed from Reddit user imGnarly: https://www.reddit.com/r/vuejs/comments/63w65c/client_side_autocomplete_search_with_vuejs/ */
      searcher() {
        let self = this;
        let holder = [];
        let rx = new RegExp(this.selectedTo, 'i');
        this.users.forEach(function (val, key) {
          if (rx.test(val.name) || rx.test(val.email)) {
            let obj = {}
            obj = val;
            holder.push(obj);
          } else {
            self.searchResult = 'No matches found';
          }
        })
        this.searchResult = holder;
        return this.selectedTo;
      },
    
      showSearcherDropdown() {
        if(this.searchResult == null) return false;
        if(this.selectedTo === '') return false;
        return true;
      }
    },
    
    methods: {
      selectToUser: function( user ) {
        this.newMessage.to = user['.key'];
        this.selectedTo = user.name;
        this.searchResult = null;
      }
    }
    

    Typeahead works well, on each change to the input field the searcher() function is called and populates the searchResult with the correct values. The v-for works and a list of divs is shown.

    Upon clicking a div, I call selectToUser( user ). This correctly reports details from the user object to the console.

    However, on first click I get an exception in the console and the divs don't clear away (I expect them to disappear because I'm setting searchResults to null).

    [Vue warn]: Error in event handler for "change": "TypeError: fns.apply is not a function"
    
    found in
    
    ---> <BFormInput>
           <BFormGroup>
             <BTab>
    
    TypeError: fns.apply is not a function
        at VueComponent.invoker (vue.esm.js?efeb:2004)
        at VueComponent.Vue.$emit (vue.esm.js?efeb:2515)
        at VueComponent.onChange (form-input.js?1465:138)
        at boundFn (vue.esm.js?efeb:190)
        at invoker (vue.esm.js?efeb:2004)
        at HTMLInputElement.fn._withTask.fn._withTask (vue.esm.js?efeb:1802)
    

    If I click the div a second time then there's no error, the input value is set and the divs disappear.

    So I suspect that writing a value to this.selectedTo (which is also the v-model object for the element is triggering a @change event. On the second click the value of doesn't actually change because it's already set, so no call to searcher() and no error.

    I've noticed this also happens if the element loses focus.

    Question: how to prevent an @change event when changing v-model value via a method?

    (other info: according to package.json I'm on vue 2.5.2)