1
votes

I am creating a custom checkbox component in Vue, and it is working fine with the data being stored in the root instance. I plan to reuse this component (along with many other components I'm building) in a wide variety of cases. I do not want to have to update or edit the root Vue instance every time I use the component, and want to store the data ONLY in the component itself. The boolean value of checked/unchecked needs to be reactive.

I played around with using a computed value, but could not get that to work either. I am open to using this though if I need to.

(THIS VERSION DOES NOT WORK)

<body>
  <script src="https://unpkg.com/vue@2.6.10"></script>
  <div id="app">
    <checkbox-item v-model="checkData">Active</checkbox-item>
    {{ checkData }}
  </div>
</body>

</html>

<script>
  Vue.component('checkbox-item', {
    template: `
          <label class="checkbox-item">
            <input type="checkbox" :checked="value"
                   @change="$emit('input', $event.target.checked)"
                   class="checkbox-input">
            <span class="checkbox-label">
              <slot></slot>
            </span>
          </label>
        `,
    data: function() {
      return {
        checkData: null
      }
    },
    props: ['value']
  })
  new Vue({
    el: '#app',
  })
</script>

(THIS VERSION WORKS, BUT AGAIN I NEED THE DATA TO NOT BE IN THE ROOT INSTANCE)

<body>
  <script src="https://unpkg.com/vue@2.6.10"></script>
  <div id="app">
    <checkbox-item v-model="checkData">Active</checkbox-item>
    {{ checkData }}
  </div>
</body>

<script>
  Vue.component('checkbox-item', {
    template: `
          <label class="checkbox-item">
            <input type="checkbox" :checked="value"
                   @change="$emit('input', $event.target.checked)"
                   class="checkbox-input">
            <span class="checkbox-label">
              <slot></slot>
            </span>
          </label>
        `,
    props: ['value']
  })
  new Vue({
    el: '#app',
    data: {
      checkData: null
    }
  })
</script>

The error I get is:

[Vue warn]: Property or method "checkData" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.

And checkData is not reactive like it was in the working example.

EDIT: Okay, here's what works! I am definitely going to look into using SFC's and other code organization methods, but for now it's still in one html file. Does anyone see a reason this wouldn't work in the long run?

  <body>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <div id="app">
        <checkbox-item></checkbox-item>
    </div>
  </body>
</html>

<script>
  Vue.component('checkbox-item', {
    template: `
      <label class="checkbox-item">
        <input type="checkbox" v-model="checkData"
               class="checkbox-input">
        <span class="checkbox-label">
          <slot>Active: {{checkData}}</slot>
        </span>
      </label>
    `,
    data: function(){
      return {
        checkData: this.checked
      }
    },
  })
  new Vue({
    el: '#app',
  })
</script>
1
I highly suggest taking a look at VueMastery's intro and advanced components course. Learning about v-on="$listeners" and v-bind="$attrs" will help you a lot. They also go into depth about composition using slots and scoped-slots. Lastly, the VueLand Discord is an amazing, free resource to get help at.Jess

1 Answers

1
votes

The reason the first example one doesn't work is because your root Vue component doesn't have access to the checkbox's data.

<html>
<!-- Your existing nonfunctional example -->
<body>
  <script src="https://unpkg.com/vue@2.6.10"></script>
  <div id="app">
    <!-- Both v-model and {{ checkData }}
      are asking about the root vue's checkData,
      but you took that off. This is why you have an error -->
    <checkbox-item v-model="checkData">Active</checkbox-item>
    {{ checkData }}
  </div>
</body>

</html>

What's going on in the non-functional example?

When you say v-model="checkData" in the nonfunctional example, that's telling the Parent (Root) Vue component to find checkData in its local scope and v-bind:value="checkData" and v-on:input="checkData = $event.target.value"

Making your checkbox-item reusable

Your component is wonderfully reusable as-is.

The only change I would make to your checkbox-item is to pass v-on="$listeners" and v-bind="$attrs" through on the input element, and remove the explicit "value" in the prop.

For any low-level, reusable UI component, you'll want to get the data in and out. Using a v-model and storing the data in a parent component is actually what you want to do. Storing the data in the root component feels clumsy, so often times there are mid-level components.

To demonstrate this, I made a list of nerds that can either be happy or sad and use your checkbox to update their state.

<body>
  <script src="https://unpkg.com/vue@2.6.10"></script>
  <div id="app">
    <!-- If you wanted to take in data,
        you would pass in the nerds array as a Prop
        into nerd-list _from_ the root Vue instance -->
    <nerd-list/> 
  </div>
</body>

<script>
  Vue.component('nerd-list', {
   template: '<div>
     <h1>List of nerds and if they are happy</h1>
     <div v-for="nerd in nerds" :key="nerd.id">
       {{ nerd.name }}
       <checkbox-item v-model="nerd.happy">Happy</checkbox-item>
     </div>
   </div>',
  data() {
    return {
      nerds: [
        { name: 'Jess', happy: true, id: 1 },
        { name: 'Tiffany', happy: true, id: 2 },
      ]
    };
  },
});

  Vue.component('checkbox-item', {
    template: `
          <label class="checkbox-item">
            <input type="checkbox" :checked="value"
                   @change="$emit('input', $event.target.checked)"
                   class="checkbox-input">
            <span class="checkbox-label">
              <slot></slot>
            </span>
          </label>
        `,
    props: ['value']
  })
  new Vue({
    el: '#app',
    data: {}, // any data in here could be used to pass props into the nerd-list.
  })
</script>

Lastly, as you can tell, writing all of this in Javascript files might seem a bit cumbersome. I highly recommend using Vue CLI and Vue SFC components for a nicer experience.