Vue CLI 3
The Vue CLI (Command Line Interface) is the official tool for spinning up a project with all the boilerplate needed for a fully functional web-application. If you have used create-react-app
before, it is similar to this.
Install Vue CLI
In order to create a project with the Vue CLI, we need to first install it with npm
or yarn
in your terminal. We'll be using npm
for all of these examples.
Getting started instructions: https://cli.vuejs.org/,
Open up your terminal, and run one of the following commands. I'll be using npm
.
npm install -g @vue/cli
# OR
yarn global add @vue/cli
2
3
Note
Vue CLI requires Node.js version 8.9
or above (8.11.0+
recommended).
Creating a project
There are two ways to create a new project with Vue CLI:
- Using
vue create
- Using the brand new
vue ui
Let's take a look at both!
Vue Create
To create a new Vue app we type vue create
followed by the <project-name>
of the project. This will launch a series of configuration options, and you can choose either a preset, or fully configure it to your needs.
Let's create a test-project
project. I'm going to cd
to my desktop, since this will be a temporary example.
vue create test-project
This will begin a series of steps in your terminal to create your project. For this example, we will choose the default (babel, eslint)
option.
Vue CLI v3.0.5
? Please pick a preset: (Use arrow keys)
❯ default (babel, eslint)
Manually select features
2
3
4
When complete you should get the message:
🎉 Successfully created project test-project.
👉 Get started with the following commands:
$ cd test-project
$ npm run serve
2
3
4
5
Follow the commands above to start your server and see your boilerplate app. Your application is now running at localhost:8080/
Ok, now quit the server, and throw the test project in the trash 🗑. We're starting over with vue ui
!
Note
To kill the server at any time, hit control + c
Vue UI
The new Vue UI will launch a localhost with a GUI (Graphical User Interface), which makes setting up a project a bit more user friendly. And you can use your mouse!
In the terminal, type:
vue ui
This will open Vue UI in the browser, and you will see many of the same options as in the console, but in a more clickable fashion.
Let's walk through the steps to create a test-project
project again.
- Click the
Create
Tab - Navigate to location you want to store it on your computer
- Click
Create New Project Here
- Name it
- Select
Package Manager
(npm
) Additional options
,Git repository
(default)- Click
Next
- Select a preset (Manual)
- Select your features
- Configuration
- Click
Create Project
Starting Dev Server
With a project created, we can now start our Vue server, and see the results. The CLI Service is built on top of webpack
, but luckily we won't have to do anything to get it up and running, it's pre configured.
You can leave the UI running, or hit control + c
to terminate it. It doesn't have to be running to launch your application. cd
into your project and run:
npm run serve
Your application is now running at localhost:8080/
Note
To kill the server at any time, hit control + c
Boilerplate files tour
Inside of your test project, you will see these starter files and folders:
/public
favicon.ico
index.html
/src
/assets
/components
App.vue
main.js
.gitignore
babel.config.js
package-lock.json
package.json
README.md
You can again trash this test project if you want 🗑.
Download example project starter files:
cli--START
To make the process more efficient, I have created a starter project for you, with all the work we have done previously, broken into components.
Open up cli--START
in the starter files, or download them at https://github.com/rbnhmll/vue-workshop
Locate and cd
into this folder and run:
npm install
This will install all of the required node_modules
. It can take a minute or two. Once completed, run:
npm run serve
The application is now running at localhost:8080/
Note
To kill the server at any time, hit control + c
Adding features to our app
I'm going to open up the final product (cli--COMPLETE
) in the browser, and show you what features we are going to be adding.
The goal is to expand the functionality of our app so we can toggle between searching for repos
, and searching for repos by developer
.
Let's get started!
Getting to know the starter files
This starter pack includes the same files as the boilerplate we created with the Vue CLI. The only difference is that I have created single file components for everything we worked on previously.
Let's take a look.
/public
/src
App.vue
main.js
/components
Errors.vue
Results.vue
Search.vue
Searching.vue
SearchType.vue
<== We'll be adding this component
Single File Components
Single file components are written in .vue
files, and consist of three sections:
<template>
<script>
<style>
These represent the component's HTML
, JavaScript
, and CSS
.
A typical starter file will look like this:
<template>
<div><!-- HTML --></div>
</template>
<script>
export default {
// JavaScript
};
</script>
<style>
/* CSS */
</style>
2
3
4
5
6
7
8
9
10
11
12
13
Each section of the component pertains to that component alone.
Note
If your syntax highlighting in .vue
files is not working properly, you may still have to install the appropriate code editor package noted near the beginning of these notes.
SearchType.vue
Create a new Component: To make a new component, we will create a new .vue
file inside the components
directory. We'll call it SearchType.vue
.
This purpose of this component will be to switch between searching for Repos and Developers.
Once created, we will input the base code above to get our component started.
Let's add some starter code to give the component some structure:
<!-- SearchType.vue -->
<template>
<div>
<h2>Search Type</h2>
<!-- Loop over searchMethods -->
<label for="">[[TYPE]] <input type="radio" name="selectedSearchMethod" id="" value="" /> </label>
</div>
</template>
2
3
4
5
6
7
8
And some basic styles:
<!-- SearchType.vue -->
<style lang="sass" scoped>
// Some base styles so it looks 👌
h2
font-size: 2rem
margin-bottom: 0
label
margin: 0 10px
</style>
2
3
4
5
6
7
8
9
Note
Note that we've have included the lang
attribute, and assigned sass
. This will allow us to use the SASS syntax inside of our styles. The SASS package was pre-installed in these starter files.
Additionally, we have a scoped
attribute, which will insure that these styles pertain to this component alone. Without this, they would apply globally.
Search.vue
Add component to In order to see this component in our app, we need to import it and render it on the page. We're going to use this component inside of Search.vue
, because it will directly effect what we are searching for.
First, let's import and register the component:
<!-- SearchType.vue -->
<script>
import SearchType from '@/components/SearchType.vue';
export default {
...
components: {
SearchType,
}
};
...
</script>
2
3
4
5
6
7
8
9
10
11
12
Note
Notice how we can use the @
symbol as an alias for the src
folder. This can make it a lot easier to find files, instead of having to go up the directory tree using ../
. Also helps when you need to organize components into folders without breaking the imports.
Now with the component imported, we can add it to our template
<!-- SearchType.vue -->
<template>
...
<form @submit.prevent="searchRepos">
...
<SearchType />
</form>
...
</template>
2
3
4
5
6
7
8
9
Note
While it's best practice to add components to the template using kabab case when using the CDN, it is a common pattern to use PascalCase when using the CLI. And since our component doesn't have any content, it can also be a self-closing element.
We should now see some content inside of our form, displaying a radio
button. This is the basic template of our component.
Building the SearchType functionality
We want to give our user the ability to choose between repo
and developer
. Depending on which option is selected, we can change the type of search we want to perform.
Let's put these as options inside our SearchType
state
, so we can loop over them later. searchMethods
will be an Array
of Strings
.
<!-- SearchType.vue -->
<script>
export default {
name: 'SearchType',
data() {
return {
searchMethods: ['repo', 'developer'],
};
},
};
</script>
2
3
4
5
6
7
8
9
10
11
Note
Remember, in components data
must be a function which returns an Object
, or it will not work properly.
Now in our template, we can loop over these options in order to show the different radio buttons. There's a few steps to hooking up these inputs correctly, so we'll go through it step by step.
First, let's loop over the searchMethods
using v-for
:
<!-- SearchType.vue -->
<template>
<div>
<h2>Search Type</h2>
<label v-for="type in searchMethods" for=""
>[[TYPE]] <input type="radio" name="selectedSearchMethod" id="" value="" />
</label>
</div>
</template>
2
3
4
5
6
7
8
9
Error ⚠️
If we look at our terminal, we should see an error, warning us about:
error: Elements in iteration expect to have 'v-bind:key' directives
When looping over items in Vue it is important to provide a key along with each element, so that Vue knows which element to update if the data changes. This should be a unique identifier.
The data for searchMethods
doesn't have any unique identifiers, other than the name itself. We can use the name as the key
, or invoke another variable as the iterator, i
, like so:
<!-- SearchType.vue -->
<template>
<div>... <label v-for="(type, i) in searchMethods" :key="i" for="">[[TYPE]] ... </label></div>
</template>
2
3
4
Note
Remember that :key
is the shorthand for v-bind:key
Now that we're looping over searchMethods
, we can use the type
to fill in some of the missing info like the label text, binding the label to the input using for
and id
, and setting a value
.
<!-- SearchType.vue -->
<template>
<div>
...
<label v-for="(type, i) in searchMethods" :key="i" :for="type"
>{{ type }} <input type="radio" name="selectedSearchMethod" :id="type" :value="type" />
</label>
</div>
</template>
2
3
4
5
6
7
8
9
Emitting Events
Emit change event back to parent component
You can now select the search type by radio button, but it doesn't really do anything yet. We need to pass this selection info back up to the main component, App.vue
where our main application state
is.
We need to watch the radio buttons for a change
event, and then pass the value up the chain to the App
component data
Object
.
Start by watching for a change event on the radio inputs, and the calling a method that we're going to create called updateSearchType
:
<!-- SearchType.vue -->
<template>
<div>
... ...
<input type="radio" name="selectedSearchMethod" :id="type" :value="type" @change="updateSearchType($event)" /> ...
</div>
</template>
2
3
4
5
6
7
Note
If you want access the original DOM event, you can pass it into a method using the special $event
variable.
Create the updateSearchType
method:
<!-- SearchType.vue -->
<script>
export default {
name: 'SearchType',
...
methods: {
updateSearchType(event) {
console.log(event);
},
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
What we want to do next is $emit
an event called handleChange
, and pass the data up to the parent component and finally to App.vue
. For clarity, we'll create an Object
with two properties: key
and val
.
<!-- SearchType.vue -->
<script>
export default {
name: 'SearchType',
...
methods: {
updateSearchType(event) {
this.$emit('handleChange', {
key: event.target.name,
val: event.target.value
});
},
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
If you check the Events
tab in the Vue devtools, you can see the event being called whenever the Search Type is changed.
handleChange $emit by <SearchType>
Select an event, and in event info
, we can confirm the payload:
event info
name: "handleChange"
type: "$emit"
source: "<SearchType>"
payload: Array[1]
0: Object
key: "selectedSearchMethod"
val: "repo"
2
3
4
5
6
7
8
Listening for custom events
handleChange
$event
in Search.vue
Listen for Inside our Search
component, we want to watch for an event called handleChange
. We can use v-on:
, or the @
shorthand like this:
<!-- Search.vue -->
<template>
<section class="search">
...
<SearchType @handleChange="" />
...
</template>
2
3
4
5
6
7
Now, Search.vue
doesn't need this info directly, but it does need to continue passing the info back up to its parent component, App.vue
. We'll create another $emit
event, and pass the $event
as the payload.
<!-- Search.vue -->
<template>
<section class="search">
...
<SearchType @handleChange="$emit('handleChange', $event)" />
...
</template>
2
3
4
5
6
7
handleChange
$event
in App.vue
Listen for Like before, we will listen for the handleChange
$event
in the App.vue
component. But in this case we are going to pass the payload to a method
called handleChange
.
<!-- App.vue -->
<template>
...
<Search
@handleChange="handleChange($event)"
@handleSearch="handleSearch($event)"
@resetSearch="resetSearch"
/>
...
</template>
2
3
4
5
6
7
8
9
10
First, we'll set the default selectedSearchMethod
to repo
ion our state
.
Next, create a handleChange
method. It will accept the $event
payload as an argument. Then grab the key
and val
properties, and use that to set the property on the state
for use later
<!-- App.vue -->
...
<script>
...
data() {
return {
selectedSearchMethod: 'repo',
...
};
},
export default {
name: 'App',
...
methods: {
...
handleChange(event) {
this[event.key] = event.val;
},
},
};
</script>
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
If we check the Vue devtools, we should now be able to see the selectedSearchMethod
property update when we select the Search Type.
Bug 🐛
There is sometimes a bug which sometimes prevents live update of the data object in the devtools. There's a refresh button in the upper right corner of the tools.
Update template
selectedSearchMethod
Update Heading and input label to reflect We can now update the text in the Search.vue
<h1>
to reflect the type of search we want to perform. In order for Search.vue
to have access to selectedSearchMethod
, we need to pass it as a prop
, using v-bind:
, or the :
shorthand:
<!-- App.vue -->
<template>
<div id="app" class="wrapper">
<Search
@handleChange="handleChange($event)"
@handleSearch="handleSearch($event)"
@resetSearch="resetSearch"
:selectedSearchMethod="selectedSearchMethod"
/>
...
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
Then, inside of Search.vue
, we need to register the props that the component expects:
<!-- Search.vue -->
<script>
...
export default {
name: 'Search',
components: { SearchType },
data() {
return {
q: '',
};
},
props: {
selectedSearchMethod: String
},
...
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Reference the prop to update the <h1>
and <label>
content.
<!-- Search.vue -->
<template>
<section class="search">
<h1>Search by Github {{ selectedSearchMethod }}</h1>
<form @submit.prevent="searchRepos">... <label for="search">{{ selectedSearchMethod }} search</label> ...</form>
</section>
</template>
2
3
4
5
6
7
Reactive Input state
Pre-select radio based on parent state
The Search Type is updating nicely, but the radio button is not pre-selected on page load. We'll pass the selectedSearchMethod
prop down to SearchType.vue
:
<!-- Search.vue -->
<template>
<section class="search">
...
<form @submit.prevent="searchRepos">
...
<SearchType @handleChange="$emit('handleChange', $event)"
:selectedSearchMethod='selectedSearchMethod' />
</form>
</section>
</template>
2
3
4
5
6
7
8
9
10
11
Now register the expected prop in SearchType.vue
:
<!-- SearchType.vue -->
<script>
export default {
name: 'SearchType',
...
props: {
selectedSearchMethod: String,
},
...
};
</script>
2
3
4
5
6
7
8
9
10
11
In the template, we can check to see if the selectedSearchMethod
is the same as the radio button type
. If it is, we want to add the checked attribute using v-bind:
, or the :
shorthand:
<!-- SearchType.vue -->
<template>
<div>
... ...
<input
type="radio"
name="selectedSearchMethod"
:id="type"
:value="type"
@change="updateSearchType($event)"
:checked="selectedSearchMethod === type"
/>
...
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
You'll notice that if you refresh the page, the "repo" option will now be pre-selected.
Note
When binding to attributes which have optional arguments, like checked
, Vue will apply the attribute if the statement is true
, and will apply nothing if it is false
.
Update search function
One issue still remains: our app's search functionality doesn't work when the "Search Type" is set to developer
.
This is because there are different API end points to search for repos and developers:
- Repos:
https://api.github.com/search/repositories?q=[QUERY]
- Developers:
https://api.github.com/users/[QUERY]/repos
This means that depending on what we are searching for we need to be able to change the end point dynamically. In addition to this, each endpoint returns the data in a different way, and we need to account for this.
To prepare for how the data comes back to us, let's make a small change to our searchRepos
method:
<!-- Search.vue -->
<script>
...
export default {
name: 'Search',
...
methods: {
async searchRepos() {
...
const response = await fetch(`https://api.github.com/search/repositories?q=${this.q}`);
const json = await response.json();
// const items = json.items; <== updating this
const items = this.selectedSearchMethod === 'repo' ?
json.items :
json;
...
},
},
...
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Next we update our end point based on selectedSearchMethod
.
Computed properties
computed
properties are similar to those stored inside the state
, except that they are re-evaluated any time their dependencies change. Dependencies can be in the state
, or other computed
properties. This makes it very convenient to keep a property up to date when other data changes.
If you look inside of Search.vue
, we are currently hard-coding the api endpoint in the searchRepos
method. We can change this to a computed
property which returns
the endpoint, and reference it inside the searchRepos
method:
<!-- Search.vue -->
<script>
...
export default {
name: 'Search',
...
methods: {
async searchRepos() {
...
// const response = await fetch(`https://api.github.com/search/repositories?q=${this.q}`);
const response = await fetch(this.searchEndpoint);
...
},
},
computed: {
searchEndpoint() {
return `https://api.github.com/search/repositories?q=${this.q}`;
},
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
At this point, endpoint will update every time the query input changes, giving us a dynamic end point to call.
Note
It's important to note that a computed
property is a Function
that returns a value.
Let's enhance the searchEndpoint
computed
property further, to change the endpoint based on the selectedSearchMethod
:
<!-- Search.vue -->
<script>
...
export default {
name: 'Search',
...
computed: {
searchEndpoint() {
return this.selectedSearchMethod === "repo" ?
`https://api.github.com/search/repositories?q=${this.q}` :
`https://api.github.com/users/${this.q}/repos`;
},
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Now our search works again for both repo
and developer
!
Filters
Capitalize
One thing still isn't quite right!
The selectedSearchMethod
is in lowercase when when use it in the template. It would look better if it was Capitalized. We could do it by changing the data to use capitalized strings, but this would mean making changes to the actual data, and we don't want that. We could also wrap those words in <span>
and capitalize them with CSS.
But there's a much more interesting way to do it in Vue, using filters
.
A filter
is used to apply text formatting on render, and can be used in both mustache interpolations and v-bind:
expressions, and uses the pipe syntax:
{{ String | filterName }}
We'll create a new filter that will capitalize the first letter of a string. To make it globally accessible, we will add it to the main Vue Object
, inside of main.js
using Vue.filter()
. This will accept a name
and a Function
as its arguments, and returns a String
, as follows
// main.js
import Vue from 'vue';
import App from './App.vue';
Vue.config.productionTip = false;
// Custom filter to capitalize first letter
Vue.filter('capitalize', function(value) {
if (!value) return '';
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
});
new Vue({
render: h => h(App),
}).$mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Once initiated, it will be available in all components. Let's update the Search.vue
component:
<!-- Search.vue -->
<template>
<section class="search">
<h1>Search Github by {{ selectedSearchMethod | capitalize }}</h1>
<form @submit.prevent="searchRepos">
<input type="search" name="search" id="search" v-model="q" required>
<label for="search">{{ selectedSearchMethod | capitalize }} search</label>
<SearchType @handleChange="$emit('handleChange', $event)" :selectedSearchMethod='selectedSearchMethod'/>
</form>
</section>
</template>
2
3
4
5
6
7
8
9
10
11
Mission Complete!
🔥🔥🔥 That's it! We did it! 🔥🔥🔥
You now have a fully functional search application, and have learned the fundamental principals of Vue.js!