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
1
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:

  1. Using vue create
  2. 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
1

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
1
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
1
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
1

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.

  1. Click the Create Tab
  2. Navigate to location you want to store it on your computer
  3. Click Create New Project Here
  4. Name it
  5. Select Package Manager (npm)
  6. Additional options, Git repository (default)
  7. Click Next
  8. Select a preset (Manual)
  9. Select your features
  10. Configuration
  11. 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
1

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
1

This will install all of the required node_modules. It can take a minute or two. Once completed, run:

npm run serve
1

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>
1
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.

Create a new Component: SearchType.vue

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>
1
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>
1
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.

Add component to Search.vue

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>
1
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>
1
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>
1
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>
1
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
1

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>
1
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>
1
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>
1
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>
1
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>
1
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>
1

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"
1
2
3
4
5
6
7
8

Listening for custom events

Listen for handleChange $event in Search.vue

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>
1
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>
1
2
3
4
5
6
7

Listen for handleChange $event in App.vue

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>
1
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>
...
1
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

Update Heading and input label to reflect selectedSearchMethod

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>
1
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>
1
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>
1
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>
1
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>
1
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>
1
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>
1
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>
1
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>
1
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 }}
1

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');
1
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>
1
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!