Image

vue-3-forms

7 min read
Last update: December 19, 2021

Base Select

In our previous lesson we learned how to create our first reusable form component, the BaseInput.

In this lesson, we are going to learn how to build our second component, the BaseSelect.

Let’s dive right in!


Currently in our sample form, we have the following select element. Our objective for this lesson is to turn this code into a reusable component, just like we did for BaseInput in the previous lesson.

📃SimpleForm.vue

<label>Select a category</label>
<select v-model="event.category">
    <option v-for="option in categories" :value="option" :key="option" :selected="option === event.category">{{ option }}</option>
</select>

As we did with the BaseInput element, we are going to create a new component file called BaseSelect.vue and start by copying the select element, along with its corresponding label, into the new file inside a template tag.

📃BaseSelect.vue

<template>
    <label>Select a category</label>
    <select v-model="event.category">
        <option v-for="option in categories" :value="option" :key="option" :selected="option === event.category">{{ option }}</option>
    </select>
</template>

In order for our component to be flexible, we need to allow the parent to be able to modify and provide content details, such as the label.

Just like we did in BaseInput, our first step is going to be to add a prop named label, and apply the contents into our label element by using interpolation.

📃BaseSelect.vue

<template>
    <label v-if="label">{{ label }}</label>
    <select v-model="event.category">
        <option v-for="option in categories" :value="option" :key="option" :selected="option === event.category">{{ option }}</option>
    </select>
</template>

<script>
    export default {
        props: {
            label: {
                type: String,
                default: ''
            }
        }
    }
</script>

Making it v-model ready

Just like we did in BaseInput, we want to make sure that our BaseSelect component is v-model ready, that way whenever it is used by our parent form, or any other form, it can easily be used with a double binding directly into our parent’s state.

Select elements do have a couple of differences and gotchas, so let’s take a look at those.

The first thing we need to do is remove the old v-model declaration from the code that we pasted from the original form.

📃BaseSelect.vue

<template>
    <label v-if="label">{{ label }}</label>
    <select>
        <option v-for="option in categories" :value="option" :key="option" :selected="option === event.category">{{ option }}</option>
    </select>
</template>

Next, we are going to add the modelValue prop to the component and bind the value attribute of the select element to it. Remember that modelValue is the default name of the property that Vue will look for when double binding through v-model to custom components.

While we’re at it, we’ll also add a class of field to the select element so that it looks nicer.

📃BaseSelect.vue

<template>
    <label v-if="label">{{ label }}</label>
    <select :value="modelValue" class="field">
        <option v-for="option in categories" :value="option" :key="option" :selected="option === event.category">{{ option }}</option>
    </select>
</template>

<script>
    export default {
        props: {
            label: {
                type: String,
                default: ''
            },
            modelValue: {
                type: [String, Number],
                default: ''
            }
        }
    }
</script>

Now that the value binding of the select is set, we have to add the second part of the v-model contract: the emit . This part of the binding will allow our component to communicate to its parent that the data has changed, and needs to be updated on the parent’s state.

Select elements fire a change event whenever the user makes a new selection, so unlike our BaseInput component, we are going to listen to the select element’s change event.

Since we are also going to need to bind the $attrs to our select element like we did on the input element for BaseInput, we’ll tackle them together and bind them using the v-bind syntax.

Since $attrs is an object, we can use the JavaScript spread operator to combine our binds into a single object. We will first spread the $attrs into our v-bind, and then bind the change event into our v-bind.

📃BaseSelect.vue

<template>
    <label v-if="label">{{ label }}</label>
    <select class="field" :value="modelValue" v-bind="{
      ...$attrs,
      onChange: ($event) => { $emit('update:modelValue', $event.target.value) }
    }">
        <option v-for="option in categories" :value="option" :key="option" :selected="option === event.category">{{ option }}</option>
    </select>
</template>

<script>
    export default {
        props: {
            label: {
                type: String,
                default: ''
            },
            modelValue: {
                type: [String, Number],
                default: ''
            }
        }
    }
</script>

Notice that this time we are not using a direct binding by setting up our change event listener with the @change keyword like we did on our BaseInput component for the @input event. This time we are setting up our event binding directly into the v-bind object, after our $attrs binding.

In Vue 3, its important to remember that if we choose not to use the @ sign syntax, the event will be prefixed by the keyword on, in this case onChange since were listening to the change event.

All event listeners that are received in $attrs from the parent are prefixed with the on keyword, and the first letter is capitalized.

On our event listener for onChange we capture the $event as the function’s parameter, and $emit our event update:modelValue with the payload of $event.target.value to notify the parent of any changes.


Selecting the correct option

I want to draw your attention to the v-for loop that we currently have on the option element in our select. Notice that we are looping over the categories array that does not exist in our component. This is very tightly coupled to what the parent is trying to do with this particular input.

In order to make our BaseSelect component work with any array of options, we are going to first rename categories to options in our v-for loop. Then, we’ll create an options prop so that our component can receive this information from the parent.

📃BaseSelect.vue

<template>
    <label v-if="label">{{ label }}</label>
    <select class="field" :value="modelValue" v-bind="{
      ...$attrs,
      onChange: ($event) => { $emit('update:modelValue', $event.target.value) }
    }">
        <option v-for="option in options" :value="option" :key="option" :selected="option === event.category">{{ option }}</option>
    </select>
</template>

<script>
    export default {
        props: {
            label: {
                type: String,
                default: ''
            },
            modelValue: {
                type: [String, Number],
                default: ''
            },
            options: {
                type: Array,
                required: true
            }
        }
    }
</script>

The second thing that we need to fix is the :selected binding for the option element loop. Currently we are trying to check if the current option that we’re looping over is equal to event.category, which is also tightly coupled to the parent.

In this case, we can safely change this to check to see if option is equal to modelValue, and that will satisfy the HTML requirement so that the option updates whenever the modelValue changes.

📃BaseSelect.vue

<template>
    <label v-if="label">{{ label }}</label>
    <select class="field" :value="modelValue" v-bind="{
      ...$attrs,
      onChange: ($event) => { $emit('update:modelValue', $event.target.value) }
    }">
        <option v-for="option in options" :value="option" :key="option" :selected="option === modelValue">{{ option }}</option>
    </select>
</template>

With these changes we can now go back to our form and switch out the select element for our brand new BaseSelect, and check it out in the browser.


Updating the form

Back in SimpleForm.vue we have a select element at the very top of the form, labeled “Select a category”. Let’s replace the label and select elements for our BaseSelect component.

📃SimpleForm.vue

<BaseSelect :options="categories" v-model="event.category" label="Select a category" />

If we check out our form in the browser, we can see that our dropdown still works as expected — now powered behind the scenes by our reusable form component.

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fformwithselect.png?alt=media&token=0922defe-6f1b-4a2b-810c-62024a2a5660


Wrapping up

In this lesson you learned how to create a reusable BaseSelect component.

Have you noticed we haven’t had to import either BaseInput or BaseSelect into our SimpleForm.vue component before we used them? In our next lesson we’ll take a look at a cool trick I’m using that allows us to automatically import any components prefixed with the word “Base”. After all, this is why we’ve been naming them as such.

See you there!