Front-End A Straightforward Vue.js Typeahead Component

Dyploma
Dyploma is a system for managing containerized applications and services on top of Kubernetes in Outbrain. Dyploma includes the concepts of:
- artifacts
- builds
- deployments
- services
Dyploma includes Java Spring backend and a Python command-line tool (CLI). The command-line tool operates through API calls to the backend.
The Dyploma Web Application
To facilitate broader adoption of containers within Outbrain, we set up to develop a web application that will have the capabilities of the Dyploma CLI.
The web application will operate by fetching data from the backend and sending operations for execution at the backend. This will be done through the same REST API used by the CLI.
Why Typeahead?
For advanced search, and for filling details in forms, we need a typeahead functionality for:
- artifacts
- builds
- deployments
- services
- environments
- kube clusters
In each case, typeahead is relative to a different field in the data.
Why Another Typeahead Component?
Googling we found many Vue.js typeahead components. But we found them too sophisticated to our needs.
We decided to base our component on Let's build Type Ahead Component with VueJS 2 and Fetch API. However, its usage of transition groups not good for us. And it did not provide for customisation of the typeahead field.
Our Typeahead
Our typeahead is a Vue.js single file component (SFC). We use ES6 (ES2015).
One can customise the field on which it operates. It interacts with your backend through promises.
It gets two props:
startAt
- how many characters to wait before trying to fetch the list of corresponding options from the serverfield
- a structure describing the typeahead field
The Field
{
key: 'serviceId',
autocomplete: 'name',
placeholder: 'Select a service',
}
The Result
Emitted with a custom event selected
:
this.$emit('selected', { field: this.field.key, value: item.id });
Where:
field
: the key of the fieldvalue
: theid
of the selected option
The Backend
We have encapsulated the call to the backend to fetch the data into a backend
module which has a getAutocomplete
obtaining the query and the key of the field and returns a promise.
You will need to write it for your case.
We assume the data contains an id
field identifying the selected item.
The Template
The Top Level
<div class="typeahead">
</div>
The Input Box
<input
v-model="query"
@focus="reset"
type="text"
class="search-input"
v-bind:placeholder="field.placeholder"
>
List of Possible Options
<ul class="results" v-if="!selected">
<li
v-for="item in items"
:key="item.id"
v-on:click="selectedItem(item)"
>
<span>
<strong>{{ item.value }}</strong>
</span>
</li>
</ul>
The Code
We use Lodash for processing arrays.
import _ from 'lodash';
import backand from `@/backend`;
export default {
name: 'typeahead',
props: ['startAt', 'field'],
data() {
return {
items: [],
query: '',
selected: false,
};
},
computed: {
isEmpty() {
if (!this.query) {
return false;
}
return this.items.length < 1;
},
},
methods: {
fetchItems() {
const q = this.query.trim();
backand.getAutocomplete(q, this.field.key)
.then((results) => {
this.items = this.extractItems(results, this.field);
});
},
extractItems(results, field) {
return _.concat([{ id: null, value: 'none selected' }], _.map(results, r => ({ id: r.id, value: r[this.field.autocomplete] })));
},
reset() {
this.items = [];
this.selected = false;
},
selectedItem(item) {
this.selected = true;
if (item.id) {
this.query = item.value;
this.$emit('selected', { field: this.field.key, value: item.id });
} else {
this.query = '';
}
},
},
watch: {
query(to, from) {
if (this.query.trim().length >= this.startAt) {
this.fetchItems();
}
},
},
};
The Styling
Using SASS:
$height: 18.95vh;
.typeahead {
flex: 2;
height: $ * 0.2;
.search-input {
position: relative;
width: 100%;
height: $height * 0.2;
font-family: AvenirNext;
font-size: 14px;
font-weight: bold;
text-align: left;
color: #a7aaac;
margin: 0;
padding: 0;
font-size: 1em;
outline: 0;
position: relative;
color: #2e3d42;
}
.search-input::placeholder {
font-family: AvenirNext;
font-size: 14px;
font-weight: bold;
text-align: left;
color: #2e3d42;
padding-left: 5%;
}
.search-input:hover {
border: 1px solid #0097ce;
}
.search-input:focus {
border: 1px solid #0097ce;
}
.search-input:active {
border: 1px solid #0097ce;
}
.results {
margin: 0;
padding: 0;
text-align: left;
position: relative;
opacity: 1;
z-index: 1000;
li {
font-family: AvenirNext;
font-size: 14px;
font-weight: bold;
text-align: left;
color: #2e3d42;
background-color: #fdfefe;
margin: 0;
padding: 1em;
list-style: none;
width: 100%;
}
}
}
The Complete Code
The Gist
Yoram Kornatzky