Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

v-bind Many Props

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

The checkout form will have six fields... and now, thanks to our shiny form-input component, we can add those with very little duplication. Copy the form-input element and paste it five times: One, two, three, four, five.

<template>
<div class="row p-3">
<div class="col-12">
<form>
<form-input
id="customerName"
v-model="form.customerName"
label="Name:"
:error-message="validationErrors.customerName"
/>
<form-input
id="customerName"
v-model="form.customerName"
label="Name:"
:error-message="validationErrors.customerName"
/>
<form-input
id="customerName"
v-model="form.customerName"
label="Name:"
:error-message="validationErrors.customerName"
/>
<form-input
id="customerName"
v-model="form.customerName"
label="Name:"
:error-message="validationErrors.customerName"
/>
<form-input
id="customerName"
v-model="form.customerName"
label="Name:"
:error-message="validationErrors.customerName"
/>
<form-input
id="customerName"
v-model="form.customerName"
label="Name:"
:error-message="validationErrors.customerName"
/>
</form>
</div>
</div>
</template>
... lines 50 - 74

Now... I'll super quickly update each component to pass in the right props. Zoom! Each input will point to a different key on our form data and will use a different key on validationErrors.

<template>
<div class="row p-3">
<div class="col-12">
<form>
<form-input
id="customerName"
v-model="form.customerName"
label="Name:"
:error-message="validationErrors.customerName"
/>
<form-input
id="customerEmail"
v-model="form.customerEmail"
label="Email:"
:error-message="validationErrors.customerEmail"
/>
<form-input
id="customerAddress"
v-model="form.customerAddress"
label="Address:"
:error-message="validationErrors.customerAddress"
/>
<form-input
id="customerZip"
v-model="form.customerZip"
label="Zip Code:"
:error-message="validationErrors.customerZip"
/>
<form-input
id="customerCity"
v-model="form.customerCity"
label="City:"
:error-message="validationErrors.customerCity"
/>
<form-input
id="customerPhone"
v-model="form.customerPhone"
label="Phone Number:"
:error-message="validationErrors.customerPhone"
/>
</form>
</div>
</div>
</template>
... lines 50 - 74

Let's go see how it looks! Click to check out and... nice! I mean, it's doesn't look great, but we'll improve that soon.

Using v-bind with an Object

Before we do, that still felt like a lot of repetition. Like, on this field, we're repeating customerCity 3 different times.

Fortunately, we can clean this up with a clever use of v-bind. At the bottom, add a methods key and create a new method called getFieldProps(): This will return a map of all the props needed for a specific input. To generate that, this needs id and label arguments.

... lines 1 - 38
<script>
... lines 40 - 41
export default {
name: 'CheckoutForm',
... lines 44 - 59
methods: {
... lines 61 - 67
getFieldProps(id, label) {
... lines 69 - 73
},
},
};
</script>

Inside, return an object with the props the field needs... which as a reminder, are id, label and error-message. So, set id: id... or better, shorten to just id, label and error-message. But, this needs to be errorMessage in camel-case: Vue will handle normalizing that to error-message. Anyways, errorMessage set to this.validationErrors[id].

... lines 1 - 38
<script>
... lines 40 - 41
export default {
name: 'CheckoutForm',
... lines 44 - 59
methods: {
... lines 61 - 67
getFieldProps(id, label) {
return {
id,
label,
errorMessage: this.validationErrors[id],
};
},
},
};
</script>

Up in the template, we can use this to shorten things. Remember, :error-message is short for v-bind:error-message which binds a single prop. But we can also use v-bind to bind a bunch of props at once. Remove error-message, label and id and instead say v-bind - with no colon - then getFieldProps() passing the id and label.

<template>
<div class="row p-3">
<div class="col-12">
<form>
<form-input
v-model="form.customerName"
v-bind="getFieldProps('customerName', 'Name:')"
/>
<form-input
v-model="form.customerEmail"
v-bind="getFieldProps('customerEmail', 'Email:')"
/>
<form-input
v-model="form.customerAddress"
v-bind="getFieldProps('customerAddress', 'Address:')"
/>
<form-input
v-model="form.customerZip"
v-bind="getFieldProps('customerZip', 'Zip Code:')"
/>
<form-input
v-model="form.customerCity"
v-bind="getFieldProps('customerCity', 'City:')"
/>
<form-input
v-model="form.customerPhone"
v-bind="getFieldProps('customerPhone', 'Phone Number:')"
/>
</form>
</div>
</div>
</template>
... lines 38 - 78

There is still some repetition between v-model and v-bind, but it's an improvement. I'll type as fast as Fabien normally types to quickly repeat this for the other 5 fields.

Phew! Unless I messed something up (very possible), that should... not break anything. Move over, hit checkout and... awesome! Everything seems to be working!

Adding Markup to make the Form Look nicer

Now let's make this look a bit nicer by organizing the fields into a few columns. This doesn't have much to do with Vue... I just don't like ugly forms.

Above the first field, add <div class="form-row">, wrap the first two fields inside and indent them. Both elements now need an extra class. Pass class="col" two times.

<template>
<div class="row p-3">
<div class="col-12">
<form>
<div class="form-row">
<form-input
v-model="form.customerName"
class="col"
v-bind="getFieldProps('customerName', 'Name:')"
/>
<form-input
v-model="form.customerEmail"
class="col"
v-bind="getFieldProps('customerEmail', 'Email:')"
/>
</div>
... lines 18 - 42
</form>
</div>
</div>
</template>
... lines 47 - 87

But I want to point something out. We do not have a class prop inside our <form-input> custom component. And so, when we pass class to this, Vue will automatically add this as an attribute to the top level element of form-input. We can see this in the browser. If we inspect the element, yep! Both outer elements - which have a form-group - now also have a col class. That's exactly what we want.

Back in index.vue, leave the customerAddress on its own row, but wrap the last 3 fields inside of another <div class="form-row">. Add the ending div, indent, and give all 3 of these class="col". And... I think I have some extra whitespace my editor is mad about. Much better.

<template>
<div class="row p-3">
<div class="col-12">
<form>
... lines 5 - 23
<div class="form-row">
<form-input
v-model="form.customerZip"
class="col"
v-bind="getFieldProps('customerZip', 'Zip Code:')"
/>
<form-input
v-model="form.customerCity"
class="col"
v-bind="getFieldProps('customerCity', 'City:')"
/>
<form-input
v-model="form.customerPhone"
class="col"
v-bind="getFieldProps('customerPhone', 'Phone Number:')"
/>
</div>
</form>
</div>
</div>
</template>
... lines 47 - 87

Go check it out. That looks great!

Customizing the input type Attribute

Let's make one last improvement. All of these fields are <input type="text">. If we wanted to handle other field types like select elements or checkboxes, we would need to do more work in <form-input> to make it more flexible or even create some new components.

I'm not going to do that now, but I at least want to be able to render different input types, like <input type="email"> and <input type="tel"> for the phone number.

No problem!. Our <form-input> now needs to be more flexible. So let's add a new prop. Copy the value prop, call this one type... and change the default to text so that we don't have to pass this in.

... lines 1 - 28
<script>
export default {
name: 'FormInput',
... lines 32 - 44
type: {
type: String,
default: 'text',
},
... lines 49 - 52
},
... lines 54 - 58
};
</script>

Use this up in the template: replace type="text" with :type="type".

<template>
<div class="form-group">
... lines 3 - 8
<input
... lines 10 - 11
:type="type"
... lines 13 - 18
>
... lines 20 - 25
</div>
</template>
... lines 28 - 61

Thanks to the default value, we only need to pass this for two fields. Find customerEmail. What's cool is that we can mix the v-bind that's set to an entire object with other, specific props without any issues. What I mean is, when we pass in the type prop with type="email", that will merge nicely with whatever props getFieldProps() adds.

<template>
<div class="row p-3">
<div class="col-12">
<form>
<div class="form-row">
... lines 6 - 11
<form-input
... lines 13 - 14
type="email"
... line 16
/>
</div>
... lines 19 - 44
</form>
</div>
</div>
</template>
... lines 49 - 89

Repeat this on customerPhone: type="tel".

<template>
<div class="row p-3">
<div class="col-12">
<form>
... lines 5 - 24
<div class="form-row">
... lines 26 - 37
<form-input
... lines 39 - 40
type="tel"
... line 42
/>
</div>
</form>
</div>
</div>
</template>
... lines 49 - 89

Go check it! You probably won't notice any difference on a computer, but if you inspect the element... yep! It's <input type="email">.

Okay! We are ready to set up this form to submit via Ajax. When we do that, we're going to make handling and rendering form validation a main concern.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@fortawesome/fontawesome-free": "^5.15.1", // 5.15.1
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.3
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.2.0
        "eslint-plugin-import": "^2.19.1", // 2.22.1
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.12
        "vue-loader": "^15.9.1", // 15.9.4
        "vue-template-compiler": "^2.6.11", // 2.6.12
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}
userVoice