11

Any idea how to resolve this problem:

in this example, the author uses vue 2.3.2 which works perfect,

new Vue({
  el: '#app',
  data: {
    users: [{
        "id": "Shad",
        "name": "Shad"
      },
      {
        "id": "Duane",
        "name": "Duane"
      },
      {
        "id": "Myah",
        "name": "Myah"
      },
      {
        "id": "Kamron",
        "name": "Kamron"
      },
      {
        "id": "Brendon",
        "name": "Brendon"
      }
    ],
    selected: [],
    allSelected: false,
    userIds: []
  },
  methods: {
    selectAll: function() {
      this.userIds = [];

      if (this.allSelected) {
        for (user in this.users) {
          this.userIds.push(this.users[user].id.toString());
        }
      }
    },
    select: function() {
      this.allSelected = false;
    }
  }
})
<script src="https://cdn.jsdelivr.net/vue/latest/vue.js"></script>

<div id="app">
  <h4>User</h4>
  <div>
    <table>
      <tr>
        <th>Name</th>
        <th>Select All<input type="checkbox" @click="selectAll" v-model="allSelected"></th>
      </tr>
      <tr v-for="user in users">
        <td>{{ user.name }}</td>
        <td><input type="checkbox" v-model="userIds" @click="select" :value="user.id"></td>
      </tr>
    </table>
  </div>

  <span>Selected Ids: {{ userIds }}</span>
</div>

when I switch it to 2.5.16 ( <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script> ) , the behavior is wierd:

When click the selectAll checkbox, only that checkbox checked, but when I toggle it to uncheck, all the checkboses below get checked

enter image description here

enter image description here

enter image description here

Kuan
  • 11,149
  • 23
  • 93
  • 201
  • 3
    It seems to be working here; is it behaving differently in your project? – Daniel Beck Jul 19 '18 at 19:10
  • 1
    Example you linked work perfectly fine if change vue link to https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js . Same as in original. – Aldarund Jul 19 '18 at 19:39
  • @Aldarund I updated my question to show screenshot, only vue script tag changed – Kuan Jul 19 '18 at 20:14
  • @DanielBeck I updated my question to show screenshot, only vue script tag changed – Kuan Jul 19 '18 at 20:14
  • 2
    it seems you should use `@change` instead of `@click` – Sphinx Jul 19 '18 at 20:29
  • I'm not seeing the behavior shown in your screenshots with either version of Vue; as Aldarund said it works the same either way. Are you certain there is no other difference in your code other than the vue version? – Daniel Beck Jul 19 '18 at 20:31
  • check [this fiddle, remember to open the console](https://codepen.io/anon/pen/djORXE?editors=1011) – Sphinx Jul 19 '18 at 20:35
  • or another approach is change `this.allSelected = !this.allSelected` when @click is fired. check [Stack Over flow: events of checkbox](https://stackoverflow.com/questions/5575338/what-the-difference-between-click-and-change-on-a-checkbox) may help you understand it. (when click, it does means the value of checkbox already changed, for onchange, some browser will be fired after lose focus, but some doesn't) – Sphinx Jul 19 '18 at 20:44

2 Answers2

12

For consistent browser functionality, I can recommended to not use click/change on checkboxes. Instead, bind the checkbox to a value (which you've already done), and then use a watcher on the value. This way, the real value of the checkbox will always accurately represent it's state. So you'd have something like this:

<input type="checkbox" v-model="allSelected">


Vue.component({..., {
    data: function() {
             return {
                allSelected: false,
             }
          }
    },
    watch: {
        allSelected: function(val){
            //Use your source of truth to trigger events!
            this.doThingWithRealValue(val); 
        }
    }
});

You're already using your component data value of allSelected as the source of truth, so you should use this source of truth as the real triggering element value, not a click. Whenever the value of allSelected changes, your code will get ran. This solves the problem without the rendering order weirdness.

rob
  • 2,119
  • 1
  • 22
  • 41
  • How do you handle cases when either user input or something else could update the data field? For example a user click should update the field and propagate a change the backend but a sync from the backend could also update the checkbox to its latest value? – Matt Sanders Apr 06 '20 at 18:49
  • Perhaps I misunderstand, but simply set the data attribute you're modeling to whatever you want. It doesn't matter what changes it, the data attribute itself is what is important. That is the source of truth here- the watcher reacts to a change in your attribute- it does not manage it. – rob Oct 26 '20 at 15:03
4

As pointed out by rob in the comments and in his answer you cannot rely on @click / @input / @change to have the same behaviour in all browsers in regards to their execution order relative to the actual model change. There is an issue at the VueJS repository with a bit more context: https://github.com/vuejs/vue/issues/6709

The better solution is to watch the model for changes and then react accordingly.

new Vue({
  el: '#app',
  data: {
    users: [{
        "id": "Shad",
        "name": "Shad"
      },
      {
        "id": "Duane",
        "name": "Duane"
      },
      {
        "id": "Myah",
        "name": "Myah"
      },
      {
        "id": "Kamron",
        "name": "Kamron"
      },
      {
        "id": "Brendon",
        "name": "Brendon"
      }
    ],
    selected: [],
    allSelected: false,
    userIds: []
  },
  methods: {
    selectAll: function() {
      this.userIds = [];

      if (this.allSelected) {
        for (user in this.users) {
          this.userIds.push(this.users[user].id.toString());
        }
      }
    },
    select: function() {
      this.allSelected = false;
    }
  },
  watch: {
      allSelected: function () {
          this.selectAll()
      }
  }
})
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>

<div id="app">
  <h4>User</h4>
  <div>
    <table>
      <tr>
        <th>Name</th>
        <th>Select All<input type="checkbox" v-model="allSelected"></th>
      </tr>
      <tr v-for="user in users">
        <td>{{ user.name }}</td>
        <td><input type="checkbox" v-model="userIds" @click="select" :value="user.id"></td>
      </tr>
    </table>
  </div>

  <span>Selected Ids: {{ userIds }}</span>
</div>
puelo
  • 5,464
  • 2
  • 34
  • 62
  • Thanks, but when I change vue version(only that script tag), same code works differently – Kuan Jul 19 '18 at 20:27
  • Edited my answer. VueJS seem to have changed the event order, or how the model is updated. – puelo Jul 19 '18 at 20:35
  • Are we seeing different results? The code in the question behaves identically to that in this updated answer, as far as I can tell? (With either version of Vue) – Daniel Beck Jul 19 '18 at 20:40
  • @DanielBeck For me the linked codepen shows the described behaviour: Works for 2.3.2 and is backwards for 2.5.16- Backwards (for me): https://codepen.io/anon/pen/bjBRWg?editors=1010 – puelo Jul 19 '18 at 20:41
  • 1
    Ah! OK, this is browser-specific; Chrome and Firefox work as you described (behavior reversed in 2.5.16); in Safari both versions behave identically. – Daniel Beck Jul 19 '18 at 20:43
  • 2
    Found a closed issue in VueJS for that matter. Added it. – puelo Jul 19 '18 at 20:50
  • This isn't bulletproof. Check out my answer- it will work across browsers as it accurately uses the vue component as the source of truth, without guessing about when code gets called by the browser. – rob Jul 19 '18 at 21:57
  • 1
    You are right. Your solution is better. Will also change my answer – puelo Jul 19 '18 at 22:18
  • @puelo Thanks still. In my case, I am not sure what did other scripts do, it always gives me error like can not set read only property. But your Onchange event handler works still – Kuan Jul 19 '18 at 23:12