Image

vue-3-forms

8 min read
Last update: December 19, 2021

Base Input

Our Demo Form

Throughout this course, we’re building a set of reusable base form components and exploring best practices along the way. In order to build these, we’ll want a demo form to use as a starting point, which we’ll break apart into these base component. I’ve prepared one so that we can jump right into it.

The form already includes v-model bindings into a reactive state and a categories array to power the select element.

📃SimpleForm.vue

<template>
    <div>
        <h1>Create an event</h1>
        <form>

            <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>

            <h3>Name & describe your event</h3>

            <label>Title</label>
            <input v-model="event.title" type="text" placeholder="Title" class="field">

            <label>Description</label>
            <input v-model="event.description" type="text" placeholder="Description" class="field" />

            <h3>Where is your event?</h3>

            <label>Location</label>
            <input v-model="event.location" type="text" placeholder="Location" class="field" />

            <h3>Are pets allowed?</h3>
            <div>
                <input type="radio" v-model="event.pets" :value="1" name="pets" />
                <label>Yes</label>
            </div>

            <div>
                <input type="radio" v-model="event.pets" :value="0" name="pets" />
                <label>No</label>
            </div>

            <h3>Extras</h3>
            <div>
                <input type="checkbox" v-model="event.extras.catering" class="field" />
                <label>Catering</label>
            </div>

            <div>
                <input type="checkbox" v-model="event.extras.music" class="field" />
                <label>Live music</label>
            </div>

            <button type="submit">Submit</button>
        </form>
    </div>
</template>

<script>
    export default {
        data() {
            return {
                categories: [
                    'sustainability',
                    'nature',
                    'animal welfare',
                    'housing',
                    'education',
                    'food',
                    'community'
                ],
                event: {
                    category: '',
                    title: '',
                    description: '',
                    location: '',
                    pets: 1,
                    extras: {
                        catering: false,
                        music: false
                    }
                }
            }
        }
    }
</script>

The Base Input

The goal of this lesson is to create a BaseInput component. Whenever we are building forms in Vue, creating reusable components for each specific input type will allow us to easily replicate them, modify them and also extend them. This also ensures that throughout our application forms will be consistent.

Let’s get started by creating the file BaseInput.vue in our components folder.

We’ll start by copying the input element with its label, just as they currently are in the demo form, and paste that into the new component’s template - we’re going to transform this static code into something more reusable and flexible. After all, that’s the benefit of making components!

📃BaseInput.vue

<template>
    <label>Title</label>
    <input v-model="event.title" type="text" placeholder="Title" class="field">
</template>

In Vue 3 we can have multiple root nodes. This means we can have the label and input at root level without needing to wrap them in a single root element like a div, like we had to in Vue 2.

The first thing we need to do is allow our component to receive a label from the parent. To do this, we are going to create a label prop. This prop will be used not only for our label, but also as a placeholder - so it is very convenient that we only have to define it once in the parent.

Let’s go ahead and add the label property in the script section of our component.

📃BaseInput.vue

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

Now we can use our new label property through interpolation inside our template’s label element.

📃BaseInput.vue

<template>
    <label>{{ label }}</label>
    <input v-model="event.title" type="text" placeholder="Title" class="field">
</template>

While we’re at it, we’re going to delete the v-model directive since we’re no longer going to be using it inside the component. We’ll come back to using v-model later on.

We will also delete type, because it will be provided as part of the attributes by the parent — remember that we want to keep the component as flexible as possible.

The user of this component may want to make it of type email or password, and the default for input is already of type text, if not declared.

Finally, let’s bind the placeholder attribute to our label property as well. This will make sure that both the “hint” text inside the input and the actual label are coordinated and reactive.

📃BaseInput.vue

<template>
    <label v-if="label">{{ label }}</label>
    <input class="field" :placeholder="label">
</template>

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

v-model: Binding to the value

Now that our component has its basic structure, we can move on to adding the capability for our component to be v-model ready.

By default in Vue 3, v-model expects a property named modelValue to be on your v-model-capable component. Let’s go ahead and add this new property, and then bind it to the value attribute of our input.

We will default it to an empty string, but will specify String and Number as the allowed types.

There is a good chance that the parent may try to bind either a text or string like Hello to our input, but it may also try to bind a numeric value, like the user’s age or 30 - we need to be able to allow either to be set.

📃BaseInput.vue

<template>
    <label v-if="label">{{ label }}</label>
    <input :value="modelValue" :placeholder="label" class="field">
</template>

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

Now that we have our modelValue property set and bound to the input attribute of the input element, let’s look at the second part of the v-model two-way binding: emitting an event.


v-model: Emitting the update:modelValue event

All components that are capable of being v-modeled have to emit an event in order for the parent to be able to catch the updates to that component’s data.

In Vue 3, by default all v-model contracts expect for your component to emit an update:modelValue event, regardless of what type of input, or inputs, your component contains.

Let’s go ahead and add an input event listener to our <input /> element, and emit an update:modelValue event whenever an input event occurs.

📃BaseInput.vue

<template>
    <label v-if="label">{{ label }}</label>
    <input :value="modelValue" :placeholder="label" @input="$emit('update:modelValue', $event.target.value)" class="field">
</template>

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

Adding an @input listener to our input element allows us to fire-off the required event every time the user types something into the input field.

Notice that we are passing the event’s target value ($event.target.value)as the payload of the event. This is the value that the v-model will receive on the parent.

Speaking of the parent, let’s go back to our form and use our new BaseInput component instead of our native elements to test out our code. Let’s replace the Title, Description and Location inputs in our form with our new component.

📃App.vue

<form>
    <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>

    <h3>Name & describe your event</h3>

    <BaseInput v-model="event.title" label="Title" type="text" />

    <BaseInput v-model="event.description" label="Description" type="text" />

    <h3>Where is your event?</h3>

    <BaseInput v-model="event.location" label="Location" type="text" />

    <h3>Are pets allowed?</h3>
    <div>
        <input type="radio" v-model="event.pets" :value="1" name="pets" />
        <label>Yes</label>
    </div>

    <div>
        <input type="radio" v-model="event.pets" :value="0" name="pets" />
        <label>No</label>
    </div>

    <h3>Extras</h3>
    <div>
        <input type="checkbox" v-model="event.extras.catering" class="field" />
        <label>Catering</label>
    </div>

    <div>
        <input type="checkbox" v-model="event.extras.music" class="field" />
        <label>Live music</label>
    </div>

    <button type="submit">Submit</button>
</form>

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fbroken%20form.opt.jpg?alt=media&token=24917041-4103-49f2-92c0-15798a4b1e36

Our components seem to be “working”, but there seems to be a problem with the styles.

If we inspect the component further, it seems that our type attribute is nowhere to be found. We want to be able to assign attributes like type into the component’s input when we set them on the instance in the parent.

Let’s take a look at how to achieve this.


Assigning the $attrs to the input

In Vue, whenever you pass down attributes, classes and styles from a parent to a child like we are doing with the type in our BaseInput component, Vue will attempt to automatically figure out where inside your template these attributes should be injected.

In components with a single wrapping element, also known as single root components, this behavior is very straightforward. Vue will simply inject all the attributes, classes and styles into the root element.

In multi-root components, such as our BaseInput, Vue can’t figure out without our help which one of the nodes, or fragments, it should inject the attributes to — so Vue simply gives up and issues a warning.

In the case of our BaseInput component, we want to be able to inject attributes directly into the input, so we have to manually bind the $attrs object to it. Let’s go ahead and do that now by adding v-bind="$attrs” to our input element.

<input v-bind="$attrs" :value="modelValue" :placeholder="label" @input="$emit('update:modelValue', $event.target.value)" class="field">

With this small change, the input elements will now correctly receive the type binding from the parent, and our CSS classes will be applied.

https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Ffixed%20form.opt.jpg?alt=media&token=974d9e01-a3c1-4b49-9db1-3465fe70e39b


Coming up next

In this lesson we learned how to build our first form component, the BaseInput, and how to correctly create a component that is v-model ready.

Did you notice that we never actually imported the BaseInput component before we used it on our form? Don’t worry about that bit for now, in lesson 4 we’ll go through the bit of magic that goes on behind the scenes to accomplish thi

In our next lesson, we’re going to build our next component, the BaseSelect.

See you there!