0
votes

I'm trying to build a triple state checkbox component. Initially the checkbox value is null and is represented by an indeterminate checkbox. When user clicks an indeterminate checkbox the value becomes true, next click false and then again null.

So far this is what I have:

Vue.component('checkbox', {
        template: '<input type="checkbox" @change="change" class="checkbox-input">',
        props: ['currentSate'],
        mounted: function () {
            this.currentSate = null;
            this.$el.indeterminate = true
        },
        methods: {
            change: function () {
                if (this.currentSate == null) {
                    this.currentSate = true;
                    this.$el.checked = true;
                    this.$el.indeterminate = false;
                    return;
                }

                if (this.currentSate == true) {
                    this.currentSate = false
                    this.$el.checked = false;
                    this.$el.indeterminate = false;
                    return;
                }

                if (this.currentSate == false) {
                    this.currentSate = null;
                    this.$el.indeterminate = true;
                    return;
                }
                this.$emit('input', this.currentSate);
            }
        }
    });
        <checkbox v-bind:current-sate="chState"></checkbox>
                chState = {{chState}}

the error I'm getting is:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "currentSate"

How can this be accomplished?

3

3 Answers

1
votes

When you get this error, in a nutshell you need to emit the value you want, but let the parent set it so it flows back down through the property.

More Info

You send an event up to the parent using @emit, the parent changes the property bound to :value or :currentSate in your case, and that in turn updates the child.

The way v-model works is that an input in changes in the child, it fires an @input event, the parent receives it and modified the bound :value, and that goes back down to the child.

It seem a bit tedious, but in the end it eliminates a whole series of design issues.

Solution

So in your case, you should not modify currentSate. Just @emit the new value, and not modify currentSate. Let the parent modify whatever data is bound to :currentSate and that will modify the property currentSate.

I advise you to use value instead of currentSate to follow the Vue conventions. If you emit input and receive value, v-model will work out of the box.

For Tri State Box

You can listen to 'click', and then switch on the value of the property 'currentSate'. Simply emit the next value based on the current value. You still need to let the parent update the bound value.

onClick(value){
  switch(this.currentSate){
    case null: this.$emit('input', true); break;
    case true: this.$emit('input', false); break;
    case false: this.$emit('input', null); break;
  }
}

Here is an example of listening to the event and updating the bound property:

<checkbox v-bind:current-sate="chState" @input='chState=$event'></checkbox>              

Now, if you emit input and bind to :value instead of currentSate, you can do this:

<checkbox v-model="chState"></checkbox>

v-model works when you use the convention of emitting your value as input and accepting your property as value.

0
votes

The warning comes up because you are mutating the prop directly from the child component.

v-model is definitely the way to go, however the state management can be handled in a far more elegant way.

I would recommend using a custom directive to enforce the indeterminate state of the checkbox when its inserted into the DOM. Here you can find a code pen with a working example.

In a nutshell, it will look like this:

Vue.directive('indeterminate', (el, binding) => {
  if (el.type && el.type === "checkbox" && binding.value === null) {
    el.indeterminate = true;
  }
});

new Vue({
  el: '#app',
  data: () => ({
    checked: null,
  }),
  methods: {
    onInput($event) {
      this.checked = this.checked === false ? null : this.checked === true ? false : true;
    }
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  I am {{ checked === null ? 'NULL' : checked }}
  <input type="checkbox" v-indeterminate="checked" :checked="checked" @input="onInput">
</div>

Update:

Updated to handle three states in a cycle. A simple solution is to handle the sequence of state in the onInput and to update the directive on every change.

0
votes

None of the answers were complete So here it is.

Vue.component('checkbox', {
        template: '<input type="checkbox" @change="change" class="checkbox-input">',
        props: ['value'],
        mounted: function () {
            this.value = null;
            this.$el.indeterminate = true
        },
        methods: {
            change: function () {
                if (this.value == null) {
                    this.$emit('input', true);
                    return;
                }

                if (this.value == true) {
                    this.$emit('input', false);
                    return;
                }

                if (this.value == false) {
                    this.$el.indeterminate = true;
                    this.$emit('input', null);

                    return;
                }
            }
        }
    });

    var myApp = new Vue(
        {
            el: '#app',
            data: {
                chState: null,
            }
        });
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
 <checkbox v-model="chState"></checkbox>
        chState = {{chState}}
</div>