Wednesday 27th February 2019
Uniquely labelled form elements in Vue Components
Introduction
It’s generally good practice to include a <label>
when you create a form element. Additionally, it’s helpful to associate these elements to allow the user to click the label and focus on that form element.
Labelled Controls
By referencing the form element’s id
attribute with the label’s for
attribute, you define the form element as a labelled control of that label. This should look familiar:
<label for="name">What is your name?</label>
<input id="name" type="text">
There’s a point to note however, according to MDN:
The first element in the document with an id matching the value of the for attribute is the labeled control for this label element, if it is a labelable element.
Each id
attribute must be unique in a given document, so if we create a reusable component in Vue that contains correctly labelled form elements, we need to make sure to generate unique ids.
Example
Let’s imagine our Vue app has a self-contained, reusable search-box
component that emits a start-search
event. We can easily reuse this component in various parts of our page by hooking into a parent method to perform a search request:
<search-box @start-search="performSearchRequest"/>
An implementation of a search-box
component might look something like:
<template>
<div class="form-group">
<label for="search-term">What are you looking for?</label>
<input id="search-term" type="text" v-model.trim="searchTerm">
<button class="btn btn-primary"
type="button"
@click="startSearch"
:disabled="isSearchButtonDisabled">
<i class="fa fa-search"></i> Search
</button>
</div>
</template>
<script>
export default {
computed: {
isSearchButtonDisabled: ({searchTerm}) => searchTerm === null,
},
data () {
return {
searchTerm: null
}
},
methods: {
startSearch () {
this.$emit('start-search', {searchTerm: this.searchTerm})
this.searchTerm = null
}
}
}
</script>
A user must enter a search term to enable the search button. When they click the search button the component emits a start-search
event containing the searchTerm
to be handled by its parent, and resets the searchTerm
.
But…
There’s a problem here—if we reuse this component in various parts of our document, the input#search-term
is no longer a unique id and is no longer an accurately identifiable labelled control. Clicking on any search-box
label will always focus the first input with that id. Not much good if you have more than one instance on the page.
Solution
Fortunately, a solution is very straightforward! We need to create a computed property with a unique id for each component.
_uid
to the rescue. It’s an internal property of each component—an integer id that Vue assigns to uniquely identify each component it creates.
We could choose a more robust method to create a unique identifier, and I would normally be shy about relying on the internal properties of a Vue component instance.
However, given this particular use-case it’s worth the trade-off and not have to worry about generating a unique value ourselves.
Let’s create a computed property:
computed: {
id: ({_uid}) => `search-box-${_uid}`,
// etc.
}
And use it like this:
<label :for="id">
<input :id="id">
Done! :)