Building a list keyboard control component with Vue.js and scoped slots
There are great packages which let you create a dropdown list and use keyboard to control it. But what if you can't use any package for that and need to implement a custom component with that kind of functionality from scratch? Fortunately, with Vue it is very easy to implement that and in this tutorial we will use Vue's scoped slot feature.
First let's get a simple project setup. We will use Vue-Cli 3 which now has a stable version. You can find more information about it at https://cli.vuejs.org/guide/creating-a-project.html#vue-create. Open the terminal and install vue-cli 3 if you do not have it yet:
npm install -g @vue/cli
or
yarn global add @vue/cli
After installation create a new project by running this command in the terminal
vue create vue-list-keyboard-control
You can use the default setup or select any additional features manually if you want, but we won't need them. After the project has been created move into the project directory and start the server:
cd vue-list-keyboard-control && npm run serve
Now modify the App.vue file so it looks like this:
<template>
<div id="app">
</div>
</template>
<script>
export default {
name: "app",
components: {}
};
</script>
<style lang="scss">
</style>
We want to have a clear template without any initial addition. Now of course we need to have a list which we will control with a keyboard. Add created hook in the App.vue file and create a new list there. Besides that we need to display the list and we will also add a little bit of styling. That's how your file should look like
<template>
<div id="app">
<!-- Here we loop through the list and display the item -->
<div
v-for="(item, index) in $options.list"
class="list-item"
:key="index">
<p>{{item}}</p>
</div>
</div>
</template>
<script>
export default {
name: "app",
components: {},
created() {
// Array with some items to create a list
this.$options.list = ["hello", "world", "this", "is", "a", "list"];
}
};
</script>
<style lang="scss">
#app {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
height: 100vh;
}
.list-item {
width: 150px;
text-align: center;
border: 1px solid #999;
cursor: pointer;
}
.list-item {
width: 150px;
text-align: center;
border: 1px solid #999;
cursor: pointer;
}
.list-item:hover,
.list-item.selected {
background-color: yellowgreen;
}
</style>
If you save the App.vue file you should see a screen like this:
So we have a list in place, but now we have to add functionality for controlling it with a keyboard. Create a new file in 'components' folder called 'WithKeyboardControl.vue'. We will need to use here data, render, methods, created, and destroyed properties of the Vue instance. We will be using 'created' hook to create event listeners and 'destroyed' hook to remove all event listeners. However, let's just add 'selectedIndex' property to the 'data' as well as the render function which will render our list, and 'listLength' prop as we will need it later in the handlers.
<script>
export default {
props: {
listLength: Number
},
data() {
return {
// Index of the list item to display
// -1 means that no item is selected
selectedIndex: -1
};
},
render(h) {
// Create a div and render content in the scoped slot as well as pass the selectedIndex
return h(
"div",
this.$scopedSlots.default({ selectedIndex: this.selectedIndex })
);
},
methods: {},
created() {},
destroyed() {}
};
</script>
Now import and register this component in App.vue file. We also will wrap our list in the 'with-keyboard-control' component and with '<template slot-scope="props"></template>' We also need to pass the length of our list to the <with-keyboard-control> component.
<template>
<div id="app">
<with-keyboard-control>
<template slot-scope="props">
<div
v-for="(item, index) in $options.list"
class="list-item"
:key="index">
<p>{{item}}</p>
</div>
</template>
</with-keyboard-control>
</div>
</template>
<script>
import WithKeyboardControl from "./components/WithKeyboardControl";
export default {
name: "app",
components: {
WithKeyboardControl
},
created() {
this.$options.list = ["hello", "world", "this", "is", "a", "list"];
}
};
</script>
If you save and refresh the page you should see that nothing has changed. However, now our list has an access to the 'selectedIndex' from the 'with-keyboard-control' component through 'slot-scope="props"'. As we only have one property which we want to pass we can use destructuring the get the 'selectedIndex' directly.
Before:
<template slot-scope="props">
After:
<template slot-scope="{selectedIndex}">
One more thing which we have to do in App.vue is to add the 'selected' class to a div which will be selected.
<div
v-for="(item, index) in $options.list"
class="list-item"
:class="{'selected': index === selectedIndex}"
:key="index">
<p>{{item}}</p>
</div>
If you save now and change the 'selectedIndex' in 'WithKeyboardControl.vue' file to 2 you should see that the second item in the list will now have a background color. Let's head back to the 'WithKeyboardControl.vue' file and add the event listeners. Create 'addKeyHandler' and 'removeKeyHandler' methods. In one of them we will add the event listener and in the other remove it.
methods: {
addKeyHandler(e) {
window.addEventListener("keydown", this.keyHandler);
},
removeKeyHandler() {
window.removeEventListener("keydown", this.keyHandler);
}
},
As you can see when someone will press the key down, the 'keyHandler' method will be called. We also have to create it as well as methods to handle 'up', 'down' and 'enter' keys. In the keyHandler we will initialise appropriate handler depending on which key has been pressed. We can get the key code from the event which is passed as the first argument to the key handler. In addition, to handling 'up', 'down', and 'enter' key we will also handle the 'tab'. If a user will press the Tab only it will fire the 'down' handler and if user will be holding 'shift' as well, 'up' handler will be called.
keyHandler(e) {
/**
38 - up
40 - down
9 - tab
13 - enter
*/
const key = e.which || e.keyCode;
if (key === 38 || (e.shiftKey && key === 9)) {
this.handleKeyUp(e);
} else if (key === 40 || key === 9) {
this.handleKeyDown(e);
} else if (key === 13) {
this.handleEnter(e);
}
},
handleKeyUp() {},
handleKeyDown() {},
handleEnter() {},
We also need to initialise the listener and remove it when the component is destroyed.
created() {
this.addKeyHandler()
},
destroyed() {
this.removeKeyHandler()
}
If you would add a console.log() to the keyHandler and start pressing keys, you would see that the handler is being fired correctly. However, we have nothing in our key handlers so let's add something into them.
handleEnter(e) {
e.preventDefault();
this.$emit("selected", this.selectedIndex);
},
handleKeyUp(e) {
e.preventDefault();
if (this.selectedIndex <= 0) {
// If index is less than or equal to zero then set it to the last item index
this.selectedIndex = this.listLength - 1;
} else if (
this.selectedIndex > 0 &&
this.selectedIndex <= this.listLength - 1
) {
// If index is larger than zero and smaller or equal to last index then decrement
this.selectedIndex--;
}
},
handleKeyDown(e) {
e.preventDefault();
// Check if index is below 0
// This means that we did not start yet
if (
this.selectedIndex < 0 ||
this.selectedIndex === this.listLength - 1
) {
// Set the index to the first item
this.selectedIndex = 0;
} else if (
this.selectedIndex >= 0 &&
this.selectedIndex < this.listLength - 1
) {
this.selectedIndex++;
}
},
After saving the file you can go to the browser and use the keyboard. You should now see that when pressing 'up', 'down' or 'tab' key, the background colour will move to the next or previous item. To see that 'enter' also work, go to the App.vue file and add an event listener to the <with-keyboard-control> component and a method to display an alert.
<with-keyboard-control :listLength="$options.list.length" @selected="selectedHandler">
...content
</with-keyboard-control>
methods: {
selectedHandler(selectedIndex) {
alert(
`The item '${
this.$options.list[selectedIndex]
}' with index ${selectedIndex} has been selected!`
);
}
},
Now if you press enter you should see an alert with information about which item has been selected.
That's it for this tutorial. You can find the source code at https://github.com/DeraIeS/vue-list-keyboard-control. Scoped-slots is an extremely powerful feature and even though it might be a little bit hard to get your head around it at the beginning, it is certainly worth to learn and use it in your projects.
Other readings:
A quick intro to new React Context API and why it won't replace state management libraries
How to create a staggered animation for paginated list in Vue.js
This geometry dash meltdown is a useful resource for anyone wanting to build complex UI components in Vue.js