Tagged “javascript”
-
Migrating a Vue 3 Project from Vue CLI to Vite
javascript
vue
-
Vue 3 Sorting Tables by Column with TypeScript and Composition API
composition api
javascript
typescript
vue
-
Filtering a Select List
javascript
Recently I migrated a Vue 3 project from Vue CLI to Vite. During the process, I took a bunch of notes. This project was using Javascript as opposed to TypeScript, so you might need to take that into account.
In my project, I will not be supporting older browsers, but Vite does have an official plugin for legacy browsers which you can find here:
https://github.com/vitejs/vite/tree/main/packages/plugin-legacy
package.json #
First things first, you want to remove the @vue/cli-service package from your project as well as any other Vue CLI plugins you might have. You can remove the @vue/cli-service package with this command:
npm un @vue/cli-service
In your package.json file, you also want to make sure you remove any dependencies that start with "@vue/cli-plugin". Here's an example of what was in mine:
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
// plus others
}
You can remove all of those at one with a command like this:
npm un @vue/cli-plugin-babel @vue/cli-plugin-eslint @vue/cli-plugin-router @vue/cli-plugin-vuex
Since I'm not supporting older browsers, I'm also removing babel:
npm un babel-eslint
Also, I need to delete the babel.config.js file.
Once all of those have been removed, add Vite and the @vitejs/plugin-vue as dev dependencies to your project.
npm i vite @vitejs/plugin-vue -D
Now, inside your package.json file, you will also want to add a "dev" script that you will use to start the local server for testing instead of using "serve" anymore as with Vue CLI. There will still be a "serve" script, but it is used to preview the production build locally, so instead of using "npm run serve" to test and develop, use "npm run dev" instead.
You will also want to change your build and lint scripts in that file as well. Just replacing vue-cli-service with vite should do the trick. Here's my old scripts section:
"scripts": {
"serve": "vue-cli-service serve",
"build-dev": "vue-cli-service build --mode development", // This is one I added
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
}
And my new one:
"scripts": {
"dev": "vite", // Use this to test locally
"serve": "vite preview",
"build-dev": "vite build --mode development", // This is one I added
"build": "vite build",
"lint": "vite lint"
}
index.html #
With Vue CLI, the index.html file is in public/index.html. You should move that file over to your project root directly instead.
You won't need to use the BASE_URL variable to find your favicon anymore, so replace:
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
With this:
<link rel="icon" href="/favicon.ico">
Vite will check your public folder for the favicon.
Vite does not auto-inject the main.js file into the index.html page, so you will need to add this line underneath the line:
<script type="module" src="/src/main.js"></script>
vite.config.js #
In the root folder, create the file vite.config.js. Inside, paste this code:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()]
})
If in your old vue.config.js file you have publicPath defined, you can add that right underneath the plugins line in your vite.config.js like this:
export default defineConfig({
plugins: [vue()],
base: '/YourBasePath/'
})
And if you need to have a different port number, you can also do so by adding this line under the plugins line:
server: { port: 8080 } // Or specify a different port number
If you've been using the @ alias in your paths, you will also want to add this under the plugins line:
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(__dirname, 'src')
}
]
}
And to use the above, you will also need to import path like so:
import path from "path"
Once you have that all configured, you can delete the old vue.config.js file. Here's what my completed vite.config.js file looks like:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
base: '/NewBasePath/',
server: { port: 8080 },
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(__dirname, 'src')
}
]
}
})
Environment Variables #
If you are using any environment variables (.env, .env.development, .env.production, etc.), you will want to change the VUE_APP_
prefix to VITE_
.
Whenever you need to use your environment variables in your code, you should now use import.meta.env.YOUR_VARIABLE_NAME
instead of process.env.YOUR_VARIABLE_NAME
.
For example, instead of using:
process.env.VUE_APP_TITLE
Use:
import.meta.env.VITE_TITLE
.vue Extensions #
One more thing you want to be aware of is when you are importing your components, you will now want to include the .vue extension, otherwise, Vite will not be able to find it. If you miss that step anywhere, you'll get an error in the console like this:
Uncaught (in promise) TypeError: Cannot destructure property ‘default' of ‘undefined' as it is undefined.
That's It #
Hopefully, this guide was able to help you out and you start enjoying how fast you can work with Vite is.
You can view the source code here:
https://github.com/kungfuphil/vue-sort-example
See it in action here:
First things first, this example uses Material Icons, so you’ll want to include this in your HTML:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
For this example, I wanted to use a simple data set.
Here's what I came up with:
let data = [
{"id": 1, "name": "Phil"},
{"id": 2, "name": "Bobby"},
{"id": 3, "name": "Susie"},
];
Because this is TypeScript and we’re going to need it later, we also add an interface for this array.
interface dataInfo {
id: number,
name: string
}
Then we update the declaration of the data variable.
let data: Array<dataInfo> = [
{"id": 1, "name": "Phil"},
{"id": 2, "name": "Bobby"},
{"id": 3, "name": "Susie"},
];
Then we add a few more lines to allow us to access the data in the template.
const tableData = ref(data);
return {tableData};
The most simple table before we start adding the sorting logic looks like this.
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.id">
<td></td>
<td></td>
</tr>
</tbody>
</table>
Adding the sort icons is very simple. You can search and look through the Material icons here:
https://fonts.google.com/icons
<th>ID <span class="material-icons">sort</span></th>
In this example, I wanted to show one icon to show that sorting is possible for that column and another icon to show if we are sorting that column and in what direction. To do that, we need to include a few more variables.
const sortColumn = ref("id");
const sortDirection = ref(1);
By default, we will display the data sorted by ID in ascending order. You can change these values to suit your data’s needs. To make it a bit easier on ourselves, we will also include a variable to hold the name of the icon that shows the sort direction.
const arrowIconName = ref("arrow_drop_up");
Now to sort the data in different ways, we create this function to sort the data depending on what column we select.
const sortByColumn = (columnName: keyof dataInfo) => {
sortColumn.value = columnName;
sortDirection.value = -1 * sortDirection.value;
if (sortDirection.value == 1) {
arrowIconName.value = "arrow_drop_up";
tableData.value.sort((a, b) => (a[columnName] > b[columnName] ? 1 : -1));
} else {
arrowIconName.value = "arrow_drop_down";
tableData.value.sort((a, b) => (a[columnName] < b[columnName] ? 1 : -1));
}
}
Finally, we give the column headers a click event and tie that to the function above to do the sorting. We also need to update the icons to show whether each column is being sorted or not as well as show the direction of the sort.
<th @click="sortByColumn('id')">ID
<span v-if="sortColumn == 'id'" class="material-icons"></span>
<span v-else class="material-icons">sort</span>
</th>
<th @click="sortByColumn('name')">Name
<span v-if="sortColumn == 'name'" class="material-icons"></span>
<span v-else class="material-icons">sort</span>
</th>
Finally here’s what the entire component looks like:
<template>
<table>
<thead>
<tr>
<th @click="sortByColumn('id')">ID
<span v-if="sortColumn == 'id'" class="material-icons"></span>
<span v-else class="material-icons">sort</span>
</th>
<th @click="sortByColumn('name')">Name
<span v-if="sortColumn == 'name'" class="material-icons"></span>
<span v-else class="material-icons">sort</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in tableData" :key="row.id">
<td></td>
<td></td>
</tr>
</tbody>
</table>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
interface dataInfo {
id: number,
name: string
}
let data: Array<dataInfo> = [
{"id": 1, "name": "Phil"},
{"id": 2, "name": "Bobby"},
{"id": 3, "name": "Susie"},
];
const tableData = ref(data);
const sortColumn = ref("id");
const sortDirection = ref(1);
const arrowIconName = ref("arrow_drop_up");
const sortByColumn = (columnName: keyof dataInfo) => {
sortColumn.value = columnName;
sortDirection.value = -1 * sortDirection.value;
if (sortDirection.value == 1) {
arrowIconName.value = "arrow_drop_up";
tableData.value.sort((a, b) => (a[columnName] > b[columnName] ? 1 : -1));
} else {
arrowIconName.value = "arrow_drop_down";
tableData.value.sort((a, b) => (a[columnName] < b[columnName] ? 1 : -1));
}
}
return {tableData, sortColumn, sortDirection, arrowIconName, sortByColumn};
},
})
</script>
<style scoped>
table {
border-collapse: collapse;
margin: auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.15);
}
thead tr {
background-color: gray;
color: white;
}
tbody tr {
border-bottom: 1px solid black;
}
th, td {
padding: 12px 15px;
}
.material-icons {
vertical-align: -6px;
}
th {
cursor: pointer;
}
</style>
Here’s an example of how to filter a select list by user input.
const completeList = document.getElementById("myList").cloneNode(true);
let filteredList = document.getElementById("myList");
const filterText = document.getElementById("filterText");
const updateList = (e) => {
// Copy the completeList to the filteredList
// then go through the filteredList and add
// any options that match the filter criteria
// Clear out the filteredList of all options
filteredList.options.length = 0;
Array.from(completeList.options).forEach((option) => {
let optionText = option.text.toLowerCase();
if (optionText.includes(filterText.value.toLowerCase())) {
const newOption = new Option(option.text, option.value);
filteredList.options.add(newOption);
}
});
}
filterText.addEventListener('input', updateList);
First, we clone the complete list of options already available on the page. If you try to create a variable of the list without cloning it like so:
const completeList = document.getElementById("myList");
You will end up editing the actual list directly. We make a clone so that we always have that master list when the filter changes, otherwise when the user changes the filter criteria it will filter from what is left of the filtered list.
Next is the function updateList(). This is what we want to happen every time the user enters a key into the input box. What it does is empties out the filteredList and then goes through all the options in the completeList, if the option text matches what the user wants to filter by, it is added to the filteredList.
Lastly, we set the event listener to call the updateList() function whenever the input box is updated.
You can see it in action here:
And here you can see the entire example in one HTML file:
<!DOCTYPE html>
<html>
<head>
<title>Filter List Test</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<style>
.container {
padding: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="form-group">
<label for="filterText">Filter:</label>
<input type="text" id="filterText" class="form-control">
</div>
<div class="form-group">
<select multiple="multiple" size="10" class="form-control" id="myList">
<option value="1">First Option</option>
<option value="2">Second Option</option>
<option value="3">Third Option</option>
</select>
</div>
</div>
<script>
const completeList = document.getElementById("myList").cloneNode(true);
let filteredList = document.getElementById("myList");
const filterText = document.getElementById("filterText");
const updateList = (e) => {
// Copy the completeList to the filteredList
// then go through the filteredList and add
// any options that match the filter criteria
// Clear out the filteredList of all options
filteredList.options.length = 0;
Array.from(completeList.options).forEach((option) => {
let optionText = option.text.toLowerCase();
if (optionText.includes(filterText.value.toLowerCase())) {
const newOption = new Option(option.text, option.value);
filteredList.options.add(newOption);
}
});
}
filterText.addEventListener('input', updateList);
</script>
</body>
</html>
See all tags.