Build a simple and reusable Vue JS radio button component
Tags: vue, components, javascript
Published: 2020-12-14 21:02:23

In this article, I want to show you how to DRY up a piece of repeated code into pieces of reusable components. 

#### App.vue
```vue
<template>
 <div>
  <UserDecision v-model="decision" />
 </div>
</template>

<script>
 import Vue from 'vue';
 
 import UserDecision from './UserDecision.vue';
 
 export default {
  components: { UserDecision },
  data: () => ({
   decision: '',
  })
 }

</script>
```

#### UserDecision.vue
```vue

 <template>
    <div class="user-decision">
        <label for="Yes">
            <input type="radio" name="radio" value="Yes" id="Yes" 
             @change="$emit('input', 'Yes')" />
            Yes
        </label>
        <label for="No">
            <input type="radio" name="radio" value="No" id="No" 
            @change="$emit('input', 'No')" />
            No</label
        >
        <label for="Undecided">
            <input
                type="radio"is
                name="radio"
                value="Undecided"
                id="Undecided"
                @change="$emit('input', 'Undecided')"
            />Undecided
        </label>
    </div>
 </template>

<script>
 import Vue from 'vue';

 export default {} 
</script>

<style>
 .user-decision {
  width: 60%;
  margin: 20px auto;
  padding: 15px 20px;
  border: 1px solid black;
  border-radius: 8px;

  display: flex;
  justify-content: space-between;
}
</style>


```
If you look at this code, this works fine if you don't think there would ever be the need for more implementations of such. 

So for the purpose of reusability, I will show you how we can refactor this, to make it easy and simple to reuse.

The first level of refactoring I see we can do here is inside the `UserDecison.vue` component file. A careful look at that file will reveal that the input, label elements are repeated. Let's DRY it up.


#### BaseRadioButtonGroup.vue
```vue
 <template>
  <div class="radio-button-group">
   <label :for="option" v-for="option in options">
    <input type="radio" name="radio-input" :value="option" 
 :id="option" 
    @change="$emit('input', option)" />
    {{ option }}
   </label>
  </div>
 </template>

<script>
 import Vue from 'vue';

 export default {
  name: 'BaseRadioButtonGroup',
  props: {
    options: {
     required: true,
     type: Array
    }
  }
 }
</script>

<style>
 .radio-button-group {
  width: 60%;
  margin: 20px auto;
  padding: 15px 20px;
  border: 1px solid black;
  border-radius: 8px;

  display: flex;
  justify-content: space-between;
}
</style>
```

Now to use our new, usage-agnostic component we have renamed 'BaseRadioButtonGroup', let's go back to the `App.vue` file.

#### App.vue
```vue
<template>
 <div>
  <BaseRadioButtonGroup v-model="decision" :options="options" />
 </div>
</template>

<script>
 import Vue from 'vue';
 
 import BaseRadioButtonGroup from './BaseRadioButtonGroup.vue';

 export default {
  components: { UserDecision },
  data: () => ({
   decision: '',
   options: ['Yes', 'No', 'Undecided']
  })
 }

</script>
```

What we have done so far is fine, but if you look at the `BaseRadioButtonGroup` component, we can even further break down its content into a smaller component that would give us more flexibility. Let me show you what I mean.


#### BaseRadioButtonGroup.vue
```vue
 <template>
 <div class="radio-button-group">
  <label :for="option" v-for="option in options" :key="option">
   <input type="radio" name="radio-input" :value="option" :id="option" 
    @change="$emit('input', option)" />
    {{ option }}
  </label>
 </div>
 </template>

<script>
 import Vue from 'vue';

 export default {
  name: 'BaseRadioButtonGroup',
  props: {
    options: {
     required: true,
     type: Array
    }
  }
 }
</script>

<style>
 .radio-button-group {
  width: 60%;
  margin: 20px auto;
  padding: 15px 20px;
  border: 1px solid black;
  border-radius: 8px;

  display: flex;
  justify-content: space-between;
}
</style>
```

#### BaseRadioButton.vue
```vue
 <template>
  <label :for="option">
   <input type="radio" name="radio-input" :value="option" :id="option" 
    @change="$emit('input', option)" />
    {{ option }}
  </label>
 </div>
 </template>

<script>
 import Vue from 'vue';

 export default {
  name: 'BaseRadioButton',
  props: {
    option: {
     required: true,
     type: string
    }
  }
 }
</script>
```
We have extracted this into a component that is decoupled, can be re-used, easily styled and customized, any time in the future.

Now, let's update the `BaseRadioButtonGroup` component to reflect the change we made.

#### BaseRadioButtonGroup.vue
```vue
 <template>
 <div class="radio-button-group">
  <BaseRadioButton v-for="option in options" :option="option" :key="option" @input="inputEventHandler" />
 </div>
 </template>

<script>
 import Vue from 'vue';

 import BaseRadioButton from './BaseRadioButton.vue';

 export default {
  name: 'BaseRadioButtonGroup',
  components: { BaseRadioButton },
  props: {
    options: {
     required: true,
     type: Array
    }
  },
  methods: {
   inputEventHandler(value) {
     this.$emit('input', value);
   }
  }
 }
</script>

<style>
 .radio-button-group {
  width: 60%;
  margin: 20px auto;
  padding: 15px 20px;
  border: 1px solid black;
  border-radius: 8px;

  display: flex;
  justify-content: space-between;
}
</style>
```

Now, because the event is two levels down, we had to handle it at each level of the component to ensure it is emitted to the parent component to ensure the v-model is kept in sync. i.e. the `BaseRadioButton` emits an `input` event, that the `BaseRadioButtonGroup` component listens to and finally emits to the `v-model` inside the `App.vue` file. 

For the purpose of writing less code and getting the same behaviour, we can get rid of the method by emitting the `input` event straight to the `App.vue` file from down inside the `BaseRadioButton` component.

Let me quickly show you what I mean.

#### BaseRadioButton.vue
```vue
 <template>
  <label :for="option">
   <input type="radio" name="radio-input" :value="option" :id="option" 
    @change="$parent.$emit('input', option)" />
    {{ option }}
  </label>
 </div>
 </template>
```

With the `$parent.$emit` property, the event will keep bubbling up the component tree until it gets inside the `App.vue` where the `v-model` property will listen to it.

Now, we can get rid of the method we created to emit the event inside the `BaseRadioButtonGroup` component.

#### BaseRadioButtonGroup.vue
```vue
 <template>
 <div class="radio-button-group">
  <BaseRadioButton v-for="option in options" :option="option" :key="option" />
 </div>
 </template>

<script>
 import Vue from 'vue';

 import BaseRadioButton from './BaseRadioButton.vue';

 export default {
  name: 'BaseRadioButtonGroup',
  components: { BaseRadioButton },
  props: {
    options: {
     required: true,
     type: Array
    }
  },
 }
</script>
```

Now, that we have two components that can be used together or singly. To render N-number of radio buttons, all we need to do is to pass an array of the options to the `BaseRadioButtonGroup` component and it would work fine. And if we need more control over every single radio button, we can equally use the `BaseRadioButton` component.

You can play with the final code [here](https://codesandbox.io/s/vue-radio-button-group-ls9hk)