In this article, I want to show you how to DRY up a piece of repeated code into pieces of reusable components.
App.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
<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
<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
<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
<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
<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
<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
<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
<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
Top comments (2)
Thank you so much for this wonderful tutorial. So clear and easy to follow.
You are welcome. Glad you found it useful