Writing Better Components in Vue
John Daly
3/1/2018
This article was originally published on the Mesh Blog
When starting a new project, you want to get things up and running as quickly as possible. While modern JavaScript advancements have made things a lot easier on developers, they have also increased the complexity of getting things going. Every developer who has worked with Webpack knows what I am talking about; Webpack is a great example of a very powerful tool that has an incredibly steep learning curve, even for doing basic things. Fortunately there is a solution for those who want to go fast: Vue and vue-cli.
I don’t think I have come across a solution that so easily gets you up and on your way to developing a modern JavaScript web application. The vue-cli
frees you from the tedious initial Webpack configuration, and allows you to start writing application code right away. While this is great, I have found there to be some shortcomings to Vue itself; mainly that Vue code can be very messy and cluttered. Here is what the standard format for a .vue
file looks like:
<template> <!-- HTML goes here --></template><script> // JavaScript goes here</script><style> /* CSS goes Here */</style>
The HTML, JavaScript, and CSS are all located in one single file. While this is okay for small components, and you could argue that it promotes keeping things simple and breaking big components into many smaller components, I find that it can easily result in monolithic files which are hard to read and difficult for others to maintain.
I also find that this style is the most commonly used way to write Vue code, being used in official examples and in most Vue projects I have come across. Honestly, this would prove to be a potential deal breaker for me if I was deciding between using React or Vue for a larger web app. Fortunately though, there is a way to separate the three pieces of the .vue file into their own files, leading to code that is easier to read and maintain.
The source code for this project can be found here.
Lets Build a Vue App
To see how easy vue-cli
makes things for us, lets start from scratch:
1. Download the vue-cli
npm install -g @vue/cli
2. Create your project with vue-cli
vue init webpack <project-name-here>
This command will lead you through a step-by-step process to configure your project. Here is what I selected for this project:
? Project name readable-vue-components? Project description A Vue.js project? Author John Daly <my email here>? Vue build standalone? Install vue-router? Yes? Use ESLint to lint your code? Yes? Pick an ESLint preset Standard? Set up unit tests Yes? Pick a test runner jest? Setup e2e tests with Nightwatch? No? Should we run `npm install` for you after the project has been created? (recommended) npm
Congratulations, you have successfully set up your project! One last thing before we start building components, we are going to install Buefy, a great UI package for Vue:
3. Install Buefy
npm install --save-dev buefy
4. Adding Buefy to the Project
Go into src/main.js
and add the following imports:
import Buefy from 'buefy';import 'buefy/lib/buefy.css';
Next configure Vue to use Buefy:
Vue.use(Buefy);
Your main.js
file should now look like this:
// The Vue build version to load with the `import` command// (runtime-only or standalone) has been set in webpack.base.conf with an alias.import Vue from 'vue';import Buefy from 'buefy';import App from './App';import router from './router';import 'buefy/lib/buefy.css';Vue.use(Buefy);Vue.config.productionTip = false;/* eslint-disable no-new */new Vue({ el: '#app', router, components: { App }, template: '<App/>',});
5. Update the Router
Go into src/router/index.js
and update it to look as follows:
import Vue from 'vue';import Router from 'vue-router';import CheckList from '@/components/Checklist/Checklist';Vue.use(Router);export default new Router({ routes: [ { path: '/', name: 'Checklist', component: Checklist, }, ],});
We are now ready to start building the Checklist
and ChecklistItem
components!
Building the Components
We will be building a simple todo list app, which will have the following functionality:
- Add items to the list
- Remove items from the list
- Edit items on the list
- Check off items on the list
Checklist Component
Let’s start by adding a new folder to hold our components. Create a folder called Checklist
in src/components
and put the following files inside of it:
- Checklist.vue
- template.html
- script.js
- style.css
Next, we will hook up the template
, script
, and style
files in the Checklist
file:
<template src="./template.html" /><script src="./script.js" /><style src="./style.css" />
Now we can focus on the individual pieces that make up the .vue
file, rather than having them all crammed in the same file!
Building the Template
Lets open up our src/components/Checklist/template.html
file and get started.
Here is what we are going to want our Checklist to look like:
<-- Header --><-- New Item Input --><-- --><-- Checklist Items -->
We can quickly tackle these piece by piece:
Header
This will be simple, just a standard <h1>
title:
<h1>Checklist</h1>
New Item Input
Next we want a text input, with a button to confirm the value:
<b-field> <b-input v-model="newItem" expanded @keyup.native.enter="addItem" /> <p class="control"> <button class="button is-primary" @click="addItem">Add</button> </p></b-field><!-- We want a divider to be rendered if we have any items in the Checklist --><hr v-show="checklistItems.length > 0" />
Checklist Items
Finally, we want the Checklist to be filled with ChecklistItem components. We will be building this component later, but we will add this here now.
<ChecklistItem v-for="item in checklistItems" :key="item.key" :itemTitle="item.title" :indexKey="item.key"/>
Finished Template
To wrap up this template, we will put each of those pieces in the following set of divs:
<div class="container is-fluid"> <div class="section"> <div class="checklist-box"> <!-- Header --> <!-- New Item Input --> <!-- Checklist Items --> </div> </div></div>
Your template file should now look like this:
<div class="container is-fluid"> <div class="section"> <div class="checklist-box"> <!-- Header --> <h1>Checklist</h1> <!-- New Item Input --> <b-field> <b-input v-model="newItem" expanded @keyup.native.enter="addItem" /> <p class="control"> <button class="button is-primary" @click="addItem">Add</button> </p> </b-field> <hr v-show="checklistItems.length > 0" /> <!-- Checklist Items --> <ChecklistItem v-for="item in checklistItems" :key="item.key" :itemTitle="item.title" :indexKey="item.key" /> </div> </div></div>
Writing the Script
Next, we will open up our JavaScript file at src/components/Checklist/script.js
.
Name
The name of the component will be Checklist
:
const name = 'Checklist';
Components
The Checklist
component is going to render many ChecklistItem
components as new items are added. We need to import the ChecklistItem
component and register it as a component of Checklist
:
// Put this at top of fileimport ChecklistItem from './ChecklistItem/ChecklistItem';const components = { ChecklistItem,};
Props
We won't need any props for this component
Data
According to our template, we will only need two properties in our data object: - The description of a new item to add (newItem) - A list, holding properties of the ChecklistItem
components that will be in the Checklist
const data = function () { return { checklistItems: [], newItem: '', };};
Methods
The methods we will need for this component are simple add/deleteItem operations
addItem
addItem will create a new object, which holds information that will be used by the ChecklistItem
component, and push it into the checklistItems
list.
const addItem = function () { let key = 0; const numItems = this.checklistItems.length; if (numItems !== 0) { // In an effort to keep the key of an item unique in the // set we will set the new item's key to be equal to the // latest item's key, plus 1 key = this.checklistItems[numItems - 1].key + 1; } // Add the newItem information to the checklistItems list this.checklistItems.push({ key, title: this.newItem }); // Reset the newItem value this.newItem = '';};
deleteItem
deleteItem will take a key
, and will remove the entry from checklistItems
that has that key.
const deleteItem = function (indexKey) { // Loop through the checklistItems list for (let i = 0; i < this.checklistItems.length; i++) { // Check the current element to see if we have a key match if (this.checklistItems[i].key === indexKey) { // Splice the matching element out of the // list and break from the loop this.checklistItems.splice(i, 1); break; } }};
Exporting the script
With the script logic written, we need to export it in the same way you export in a standard Vue file:
export default { name, components, props: {}, data, computed: {}, methods: { addItem, deleteItem, },};
Finishing the Script
Here is what the finished script file should look like:
import ChecklistItem from './ChecklistItem/ChecklistItem';// ==================// NAME// ==================const name = 'Checklist';// ==================// COMPONENTS// ==================const components = { ChecklistItem,};// ==================// PROPS// ==================const props = {};// ================// DATA// ================const data = function () { return { checklistItems: [], newItem: '', };};// ================// COMPUTED// ================const computed = {};// =================// METHODS// =================/** * Creates a new item from the value of newItem * and pushes it into the checklistItems list */const addItem = function () { let key = 0; if (this.checklistItems.length !== 0) { key = this.checklistItems[this.checklistItems.length - 1].key + 1; } this.checklistItems.push({ key, title: this.newItem }); this.newItem = '';};/** * Removes the item with the given indexKey from the checklistItems list * @param indexKey Number the key of the item to remove */const deleteItem = function (indexKey) { for (let i = 0; i < this.checklistItems.length; i++) { if (this.checklistItems[i].key === indexKey) { this.checklistItems.splice(i, 1); break; } }};export default { name, components, props, data, computed, methods: { addItem, deleteItem, },};
Writing the CSS
Finally we will add some styling for our component with the src/components/Checklist/style.css
file.
Our styling for this component is going to be simple, we want it to be held in a div that is centered, has a rounded border, a bit of box-shadow, and some padding:
.checklist-box { margin-left: auto; margin-right: auto; width: 85%; border-radius: 5px; border: 1px solid #e7eaec; box-shadow: 0px 0px 4px rgba(134, 134, 134, 0.21); padding: 15px 15px 15px 15px;}
ChecklistItem Component
The ChecklistItem
component will be the individual listings that appear within the Checklist
. We will start by creating a new folder ChecklistItem
within our existing Checklist
folder. As before, we will add the following 4 files:
- ChecklistItem.vue
- template.html
- script.js
- style.css
Matching the way we made the Checklist
component, the ChecklistItem.vue
file will look like this:
<template src="./template.html" /><script src="./script.js" /><style src="./style.css" />
Building the Template
First, open the template file located at src/components/Checklist/ChecklistItem/template.html
.
For our template, we want an item to look like this:
+----------------------------------+|[ ] [ <Item Description> ] [X]|+----------------------------------+ (A) (B) (C)
Here is a breakdown of what makes up this component:
A) Checkbox to mark an item as being finished B) Text field that includes a description of the item, it will be possible for user’s to edit this field C) Button to remove the item from the Checklist
Item Finished Checkbox
We just need a simple checkbox that is bound to a data value of the component:
<b-checkbox v-model="itemChecked" />
Item Description Text
This will be a text span that will hold the description of the item:
<span class="item-static" :class="{'item-checked': itemChecked}" v-show="!editingItem" @dblclick="editItem"> {{itemTitleDone}}</span>
A few things to take note of here:
- We are conditionally adding a class called
item-checked
if the value ofitemChecked
is true - We are only rendering this text span if
editingItem
is false; aka don’t show this text if the item is being edited - When the text span is double clicked, we are calling the
editItem
function
Edit Item Description Input
We will need a way to allow for an item’s text to be edited, for that we will add a text input field:
<!-- Edit Item Description Input --><b-input v-model="itemTitleEdit" v-show="editingItem" :disabled="itemChecked" @blur="doneEdit" @keyup.native.enter="doneEdit" @keyup.native.esc="cancelEdit" expanded/>
Things to note:
- We only show this field when
editingItem
is true. In tandem with the logic of the text span, the input will seamlessly appear when the user double-clicks the text field - The input is disabled if
itemChecked
is true - The input will call
doneEdit
if the user presses enter, or clicks somewhere outside of the input - The input will call
cancelEdit
if the user presses esc
Delete Item Button
The last thing we will need for this template is a button to remove the item from the Checklist
<!-- Delete Item Button --><p class="control"> <button class="button is-danger" @click="deleteItem"> <b-icon icon="close" /> </button></p>
Finalizing
To finish the template, we will wrap all those pieces into a <b-field>
tag
<b-field> <!-- Item Finished Checkbox --> <b-checkbox v-model="itemChecked" /> <!-- Item Description Text --> <span class="item-static" :class="{'item-checked': itemChecked}" v-show="!editingItem" @dblclick="editItem" > {{itemTitleDone}} </span> <!-- Edit Item Description Input --> <b-input v-model="itemTitleEdit" v-show="editingItem" :disabled="itemChecked" @blur="doneEdit" @keyup.native.enter="doneEdit" @keyup.native.esc="cancelEdit" expanded /> <!-- Delete Item Button --> <p class="control"> <button class="button is-danger" @click="deleteItem"> <b-icon icon="close" /> </button> </p></b-field>
Writing the Script
Next, we will work on the JavaScript file located at src/components/Checklist/ChecklistItem/script.js
.
Name
The name of the component with be ChecklistItem
:
const name = 'ChecklistItem';
Components
We don't have any components to register for this component.
Props
The ChecklistItem
component requires two props, which we get from the checklistItems
list in the Checklist
component.
const props = { indexKey: { type: Number, required: true }, itemTitle: { type: String, required: true },};
Data
According to our template, we will need four properties in our data object:
- Whether or not the current item is checked
- Whether or not the current item is being edited
- The description of an item to show in the text span
- The description of an item to show in the edit item input field
const data = function () { return { itemChecked: false, editingItem: false, itemTitleDone: this.itemTitle, itemTitleEdit: this.itemTitle, };};
Methods
The methods we will need for this component are operations to edit item, confirm edit, cancel edit, and delete item.
editItem
editItem
will set the value of editingItem
to true if the
item hasn’t already been checked.
const editItem = function () { if (!this.itemChecked) { this.editingItem = true; }};
cancelItem
cancelItem
will revert the item back to its previous description and
hides the editing input field.
const cancelEdit = function () { this.editingItem = false; this.itemTitleEdit = this.itemTitleDone;};
doneEdit
doneEdit
will confirm the edited changes, and will set the text value
of the span to be equal to those changes. If the value that is entered is blank,
then the item will be deleted.
const doneEdit = function () { this.itemTitleDone = this.itemTitleEdit; this.editingItem = false; if (this.itemTitleDone === '') { this.deleteItem(); }};
deleteItem
deleteItem
will delete the item from the parent
Checklist
component.
const deleteItem = function () { this.$parent.deleteItem(this.indexKey);};
Exporting the script
With the script logic written, we need to export it in the same way you export in a standard Vue file:
export default { name, components: {}, props, data, computed: {}, methods: { editItem, cancelEdit, doneEdit, deleteItem, },};
Finishing the Script
// ==================// NAME// ==================const name = 'ChecklistItem';// ==================// COMPONENTS// ==================const components = {};// ==================// PROPS// ==================const props = { indexKey: { type: Number, required: true }, itemTitle: { type: String, required: true },};// ================// DATA// ================const data = function () { return { itemChecked: false, editingItem: false, itemTitleDone: this.itemTitle, itemTitleEdit: this.itemTitle, };};// ================// COMPUTED// ================const computed = {};// =================// METHODS// =================/** * Sets the value of 'editingItem' to true if the item * hasn't already been checked off as completed */const editItem = function () { if (!this.itemChecked) { this.editingItem = true; }};/** * Reverts the item back to its previous description * and hides the editing input field */const cancelEdit = function () { this.editingItem = false; this.itemTitleEdit = this.itemTitleDone;};/** * Sets the description of the item to the value of the * input field that is used to edit the description. If the user * has entered a blank value, the item is deleted */const doneEdit = function () { this.itemTitleDone = this.itemTitleEdit; this.editingItem = false; if (this.itemTitleDone === '') { this.deleteItem(); }};/** * Deletes the item from the parent Checklist component */const deleteItem = function () { this.$parent.deleteItem(this.indexKey);};export default { name, components, props, data, computed, methods: { editItem, cancelEdit, doneEdit, deleteItem, },};
Writing the CSS
Finally, we will add a bit of styling in the src/components/Checklist/ChecklistItem/style.css
file.
Like the Checklist
component, we will be keeping the styling simple here. We want the text span of the item to expand to fill the empty space within its container, and we want to give it a border to make the transition between editing and non-editing modes more seamless.
We also want to create a rule for the item-checked class, to put a line through the text of the completed item in the Checklist
.item-static { width: 100%; width: -moz-available; width: -webkit-fill-available; width: fill-available; display: flex; align-items: center; border: 1px #dbdbdb solid; padding-left: 10px;}.item-checked { text-decoration: line-through;}
Lets Compare
Alright, our components are in place and
This is what the Checklist.vue
file would look like if you followed the format specified in the official Vue documentation.
<template> <div class="container is-fluid"> <div class="section"> <div class="checklist-box"> <!-- Header --> <h1>Checklist</h1> <!-- New Item Input --> <b-field> <b-input v-model="newItem" expanded @keyup.native.enter="addItem" /> <p class="control"> <button class="button is-primary" @click="addItem">Add</button> </p> </b-field> <hr v-show="checklistItems.length > 0" /> <!-- Checklist Items --> <ChecklistItem v-for="item in checklistItems" :key="item.key" :itemTitle="item.title" :indexKey="item.key" /> </div> </div> </div></template><script> import ChecklistItem from '@/components/Checklist/ChecklistItem/ChecklistItem'; export default { name: 'Checklist', components: { ChecklistItem: ChecklistItem, }, props: {}, data() { return { checklistItems: [], newItem: '', }; }, computed: {}, methods: { addItem: function () { let key = 0; if (this.checklistItems.length !== 0) { key = this.checklistItems[this.checklistItems.length - 1].key + 1; } this.checklistItems.push({ key, title: this.newItem }); this.newItem = ''; }, deleteItem: function (indexKey) { for (let i = 0; i < this.checklistItems.length; i++) { if (this.checklistItems[i].key === indexKey) { this.checklistItems.splice(i, 1); break; } } }, }, };</script><style> .checklist-box { margin-left: auto; margin-right: auto; width: 85%; border-radius: 5px; border: 1px solid #e7eaec; box-shadow: 0px 0px 4px rgba(134, 134, 134, 0.21); padding: 15px 15px 15px 15px; }</style>
and here is the ChecklistItem.vue
file with the same standard format:
<template> <b-field> <b-checkbox v-model="itemChecked" /> <span class="item-static" :class="{'item-checked': itemChecked}" v-show="!editingItem" @dblclick="editItem" > {{itemTitleDone}} </span> <b-input v-model="itemTitleEdit" v-show="editingItem" :disabled="itemChecked" @blur="doneEdit" @keyup.native.enter="doneEdit" @keyup.native.esc="cancelEdit" expanded /> <p class="control"> <button class="button is-danger" @click="deleteItem"> <b-icon icon="close" /> </button> </p> </b-field></template><script> export default { name: 'ChecklistItem', props: { indexKey: { type: Number, required: true }, itemTitle: { type: String, required: true }, }, data() { return { itemChecked: false, editingItem: false, itemTitleDone: this.itemTitle, itemTitleEdit: this.itemTitle, }; }, computed: {}, methods: { editItem: function () { if (!this.itemChecked) { this.editingItem = true; } }, cancelEdit: function () { this.editingItem = false; this.itemTitleEdit = this.itemTitleDone; }, doneEdit: function () { this.itemTitleDone = this.itemTitleEdit; this.editingItem = false; if (this.itemTitleDone === '') { this.deleteItem(); } }, deleteItem: function () { this.$parent.deleteItem(this.indexKey); }, }, };</script><style> .item-static { width: 100%; width: -moz-available; width: -webkit-fill-available; width: fill-available; display: flex; align-items: center; border: 1px #dbdbdb solid; padding-left: 10px; } .item-checked { text-decoration: line-through; }</style>
While both of these examples are manageable, they are certainly becoming quite long files. These are fairly simple components as well; so trying to build something more involved simply will not scale well. Also notice that none of the functions have any comments or documentation. I find that the structure of the <script/>
tag makes the code difficult to read to begin with, as it forces you to compress everything into a small space. Notice how the methods begin with three levels of indentation; this is horizontal space that would be nice to have back, especially for writing comments or longer statements. Separating the JavaScript into its own file allows you to make everything more readable, and gives you more space to work.
Splitting the CSS file out of the .vue
file can also be very helpful, especially if you use classes that are repeated in many different component templates.
Wrapping Up
Vue is a great framework to quickly get an app up and running, and has great developer support as well. I think it is a worthy alternative to other frameworks like React and Angular. My initial impression of Vue is that it would be a good choice for a weekend project or small prototype, and that it wouldn’t scale well because of how .vue files are recommended to be formatted. By splitting the .vue files into smaller pieces, it seems to me that Vue is a much more viable option for larger projects. If you have been on the fence about starting a project in Vue, I highly recommend giving it a shot.
How do you structure your Vue projects? Do you use the standard .vue
format? How have you tackled issues of scalability, maintainability, and readability. Feel free to reach out and let us know!