Vue CDN

NOTE ☝️

There are Development and Production versions of both.

  • Development version includes helpful console warnings. (we'll be using this one)
  • Production version is optimized for size and speed.

Start by opening up the project cdn--START. I have provided some starter HTML and CSS in order to speed along the process, and we will be building out all the functionality together.

Vue CDN script

Add the CDN at the bottom of the body

<body>
  ...
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</body>
1
2
3
4

The Vue Instance

Add an id to the part of our page where the application will live. We'll use the id app and add it to the .wrapper.

<body>
  <div id="app" class="wrapper">...</div>
</body>
1
2
3

Next, instantiate the Vue instance in a new script tag, below the cdn. This is done by creating a new Vue() and passing in a config Object as the argument:

...
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

  <script>
    const app = new Vue({
      el: "#app",
      data: {}
    });
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
  • el: The id of the element our app should be rendered in
  • data: ( a.k.a. state ) data saved within our app. This is an Object.

Vue.js devtools

We can confirm that our Vue app is initiating by checking the devtools. Install the Vue Devtools for you preferred browser:

Open up the browser inspector/devtools, and click on the Vue tab.

We'll be returning to those tools a bunch throughout the process.

Devtools gotchas

If the page uses a production/minified build of Vue.js, devtools inspection is disabled by default so the Vue pane won't show up.

To make it work for pages opened via file:// protocol, you need to check "Allow access to file URLs" for this extension in Chrome's extension management panel.

* From Vue-devtools documentation: https://github.com/vuejs/vue-devtools

State

Before we start adding some information to our app, we need to understand an important feature of the Vue instance: state.

The app's state keeps track of any dynamic data which may be used in various parts of the application. This could be pre-populated data, user input data, or data fetched from an API. You can think of state as your app's local storage.

This data could be changing rapidly with every user interaction, and the state will reflect the current data in real-time.

Declarative Rendering

Let's add some dynamic messages to the searching and error sections of the app, by declaring a key and value on the data Object, otherwise known as state.

...
  const app = new Vue({
    el: "#app",
    data: {
      searchingMessage: "Searching...",
      errorMessage: "Oops, nothing here"
    }
  });
...
1
2
3
4
5
6
7
8
9

We can then render these messages inside of our app template using the mustache syntax, like .

...
<section class="searching"><p>{{ searchingMessage }}</p></section>

<section class="errors"><h2>{{ errorMessage }}</h2></section>
...
1
2
3
4
5

TIP

Notice that we can reference the property on the state directly, without having to reference the main app Object, like app.data.searchingMessage.

Directives

There are a number of directives in Vue which let us apply logic inside of our templates:

  • v-model (two-way data binding)
  • v-on (event binding) [ shorthand: @ ]
  • v-if (conditional rendering)
  • v-for (render loop)
  • v-bind (attributes) [ shorthand: : ]

Two-way data binding: v-model

In order to search for repos on Github, we need to grab the user input value from the search field, and pass it into the API call. Let's add a placeholder for this query as an empty string to our state, and call it q.

...
  data: {
    q: "",
    searchingMessage: "Searching...",
    errorMessage: "Oops, nothing here"
  }
...
1
2
3
4
5
6
7

We need a way to get this information from the input[type="search"], and save it to our q property for use later.

But! At the same time, if we have a property saved in our state already, we want to make sure that we update the input to reflect that data 🤔.

The simplest way to achieve this is using Vue's build in two-way data binding with v-model!

<form>
  <input type="search" name="search" id="search" required v-model="q" /> <label for="search">Repo search</label>
</form>
1
2
3

Open up the Vue devtools, and notice that as we type in the input, it automatically updates the state

Also notice that if we change, or pre-populate the state, this is automatically updated in the bound input. This is the power of two-way data binding!

Methods

Now that we have the users search query, we want to use it to call the github API, and return some data. We'll create a couple of methods:

The first method is what we will call to fetch the data from the API, and the second is a method to reset our app before making another API call.

We'll start by adding a methods property to our app. methods is an Object.

...
  const app = new Vue({
    el: "#app",
    data: {
      q: "",
      searchingMessage: "Searching...",
      errorMessage: "Oops, nothing here"
    },
    methods: {}
  });
...
1
2
3
4
5
6
7
8
9
10
11

The endpoint we'll use to fetch this data is https://api.github.com/search/repositories?q={query}.

We'll start by adding a couple of properties to our state to keep track of when we the search isSearching, or when it hasError. We also need to add a property to save our returned repos into. This will be an Array:

...
  data: {
    q: "",
    repos: [],
    searchingMessage: "Searching...",
    errorMessage: "Oops, nothing here",
    search: {
      isSearching: false,
      hasError: false,
    }
  },
...
1
2
3
4
5
6
7
8
9
10
11
12

Creating methods

Let's build two methods, called searchRepos and resetSearch

...
  methods: {
    searchRepos() {},
    resetSearch() {}
  }
...
1
2
3
4
5
6

searchRepos method

The searchRepos method which will take care of the following steps:

  • Prevent the form from refreshing the page on submit
  • Set our search.isSearching property to true
  • Call resetSearch (we'll build this method next)
  • Parse the response into usable json
  • Change the search.isSearching property to false, when finished searching.
  • If we got some responses, we want to save it to our state in repos
  • If we get no repos back, then we want to set the search.hasError to true.
...
  methods: {
    async searchRepos(event) {
      event.preventDefault();
      this.search.isSearching = true;
      this.resetSearch();
      const response = await fetch(`https://api.github.com/search/repositories?q=${this.q}`);

      const json = await response.json();
      this.search.isSearching = false;
      if (json.items.length) {
        this.repos = json.items;
      } else {
        this.search.hasError = true;
      }
    },
    resetSearch() {}
  }
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

resetSearch method

Next we will create the method resetSearch to reset the search.hasError, and return repos to an empty Array when starting a new search.

...
  methods: {
    async searchRepos(event) {
      ...
    },
    resetSearch() {
      this.search.hasError = false;
      this.repos = [];
    }
  }
...
1
2
3
4
5
6
7
8
9
10
11

Note

Notice the this keyword in our methods? This is how we will reference any data property or method from within another part of our app, instead of referencing app.

Event handling: v-on

We need the form to call the searchRepos method on submit. We can use the directive v-on to listed to DOM events, such as submit. We then reference the name of the method we want to call.

...
<form v-on:submit="searchRepos">...</form>
...
1
2
3

Now that our searchRepos method has been called, we should have up to 30 Objects in our repos Array. Check in out in the DevTools.

Event modifiers

Now, calling event.preventDefault() on the submit event is fine, but there's also a Vue way of modifying these types of events, which is more concise. These are called event modifiers, and are appended to the event using dot notation.

v-on:submit.prevent="searchRepos"

  • Prevent default submit behaviour (page refresh).

v-model.number="calculateAge"

  • Forces a chosen data type.

We can tack on a modifier to the event handler, instead of stating it in the method.

...
<form v-on:submit.prevent="searchRepos">...</form>
...
1
2
3

Remove event.preventDefault() from the searchRepos method

...
  methods: {
    async searchRepos() {
      // event.preventDefault(); <== Remove this line! And notice the `event` argument is gone too.
    ...
...
1
2
3
4
5
6

Conditional Rendering: v-if

Using conditional rendering with v-if, we can decide to only show the results section if there are repos, and search.isSearching and search.hasError sections when applicable.

...
<section class="results" v-if="repos.length">...</section>
...
<section class="searching" v-if="search.isSearching"><p>{{ searchingMessage }}</p></section>

<section class="errors" v-if="search.hasError"><h2>{{ errorMessage }}</h2></section>
...
1
2
3
4
5
6
7

Note

There is a similar directive to v-if called v-show. The main difference is that while v-if will render the component conditionally, v-show will always render the component in the DOM, but toggle the display CSS property.

Render loops: v-for

Now that we have some repos in our data, it's time to loop over, and display them on the page. We can loop over an Array of items in our state using v-for.

Use this directive on the element which needs to be repeated. In this case, the <li>.

...
<section class="results">
  <ul>
    <li class="repo" v-for="repo in repos">... ...</li>
  </ul>
</section>
1
2
3
4
5
6

Now we have a variable repo which represents the current repo in our loop. We can use this to replace some of the dummy content on our cards.

Here's an example of the data included in one of the returned repos:

























 











 












 





 







 































repos: Array[30]
  0: Object
    archive_url:"https://api.github.com/repos/vuejs/vue/{archive_format}{/ref}"
    archived:false
    assignees_url:"https://api.github.com/repos/vuejs/vue/assignees{/user}"
    blobs_url:"https://api.github.com/repos/vuejs/vue/git/blobs{/sha}"
    branches_url:"https://api.github.com/repos/vuejs/vue/branches{/branch}"
    clone_url:"https://github.com/vuejs/vue.git"
    collaborators_url:"https://api.github.com/repos/vuejs/vue/collaborators{/collaborator}"
    comments_url:"https://api.github.com/repos/vuejs/vue/comments{/number}"
    commits_url:"https://api.github.com/repos/vuejs/vue/commits{/sha}"
    compare_url:"https://api.github.com/repos/vuejs/vue/compare/{base}...{head}"
    contents_url:"https://api.github.com/repos/vuejs/vue/contents/{+path}"
    contributors_url:"https://api.github.com/repos/vuejs/vue/contributors"
    created_at:"2013-07-29T03:24:51Z"
    default_branch:"dev"
    deployments_url:"https://api.github.com/repos/vuejs/vue/deployments"
    description:"🖖 A progressive, incrementally-adoptable JavaScript framework for building UI on the web."
    downloads_url:"https://api.github.com/repos/vuejs/vue/downloads"
    events_url:"https://api.github.com/repos/vuejs/vue/events"
    fork:false
    forks:16135
    forks_count:16135
    forks_url:"https://api.github.com/repos/vuejs/vue/forks"
==> full_name:"vuejs/vue"
    git_commits_url:"https://api.github.com/repos/vuejs/vue/git/commits{/sha}"
    git_refs_url:"https://api.github.com/repos/vuejs/vue/git/refs{/sha}"
    git_tags_url:"https://api.github.com/repos/vuejs/vue/git/tags{/sha}"
    git_url:"git://github.com/vuejs/vue.git"
    has_downloads:true
    has_issues:true
    has_pages:false
    has_projects:true
    has_wiki:false
    homepage:"http://vuejs.org"
    hooks_url:"https://api.github.com/repos/vuejs/vue/hooks"
==> html_url:"https://github.com/vuejs/vue"
    id:11730342
    issue_comment_url:"https://api.github.com/repos/vuejs/vue/issues/comments{/number}"
    issue_events_url:"https://api.github.com/repos/vuejs/vue/issues/events{/number}"
    issues_url:"https://api.github.com/repos/vuejs/vue/issues{/number}"
    keys_url:"https://api.github.com/repos/vuejs/vue/keys{/key_id}"
    labels_url:"https://api.github.com/repos/vuejs/vue/labels{/name}"
    language:"JavaScript"
    languages_url:"https://api.github.com/repos/vuejs/vue/languages"
    license:Object
    merges_url:"https://api.github.com/repos/vuejs/vue/merges"
    milestones_url:"https://api.github.com/repos/vuejs/vue/milestones{/number}"
    mirror_url:null
==> name:"vue"
    node_id:"MDEwOlJlcG9zaXRvcnkxMTczMDM0Mg=="
    notifications_url:"https://api.github.com/repos/vuejs/vue/notifications{?since,all,participating}"
    open_issues:313
    open_issues_count:313
    owner: Object
  ==> avatar_url:"https://avatars1.githubusercontent.com/u/6128107?v=4"
      events_url:"https://api.github.com/users/vuejs/events{/privacy}"
      followers_url:"https://api.github.com/users/vuejs/followers"
      following_url:"https://api.github.com/users/vuejs/following{/other_user}"
      gists_url:"https://api.github.com/users/vuejs/gists{/gist_id}"
      gravatar_id:""
      html_url:"https://github.com/vuejs"
      id:6128107
  ==> login:"vuejs"
      node_id:"MDEyOk9yZ2FuaXphdGlvbjYxMjgxMDc="
      organizations_url:"https://api.github.com/users/vuejs/orgs"
      received_events_url:"https://api.github.com/users/vuejs/received_events"
      repos_url:"https://api.github.com/users/vuejs/repos"
      site_admin:false
      starred_url:"https://api.github.com/users/vuejs/starred{/owner}{/repo}"
      subscriptions_url:"https://api.github.com/users/vuejs/subscriptions"
      type:"Organization"
      url:"https://api.github.com/users/vuejs"
    private:false
    pulls_url:"https://api.github.com/repos/vuejs/vue/pulls{/number}"
    pushed_at:"2018-09-21T13:10:43Z"
    releases_url:"https://api.github.com/repos/vuejs/vue/releases{/id}"
    score:159.14955
    size:23754
    ssh_url:"git@github.com:vuejs/vue.git"
    stargazers_count:114632
    stargazers_url:"https://api.github.com/repos/vuejs/vue/stargazers"
    statuses_url:"https://api.github.com/repos/vuejs/vue/statuses/{sha}"
    subscribers_url:"https://api.github.com/repos/vuejs/vue/subscribers"
    subscription_url:"https://api.github.com/repos/vuejs/vue/subscription"
    svn_url:"https://github.com/vuejs/vue"
    tags_url:"https://api.github.com/repos/vuejs/vue/tags"
    teams_url:"https://api.github.com/repos/vuejs/vue/teams"
    trees_url:"https://api.github.com/repos/vuejs/vue/git/trees{/sha}"
    updated_at:"2018-09-23T18:20:31Z"
    url:"https://api.github.com/repos/vuejs/vue"
    watchers:114632
    watchers_count:114632
  ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
...
<li class="repo" v-for="repo in repos">
  ...
  <div class="repo__meta">
    <p><strong>Dev:</strong> {{ repo.owner.login }}</p>
    <p><strong>Repo:</strong> {{ repo.name }}</p>
  </div>
  ...
</li>
...
1
2
3
4
5
6
7
8
9
10

Note

While we can use the syntax to render text content, we cannot use this within attributes. Follow along to the next step to see how we bind data to these attributes.

Directives: v-bind

In order to bind data to attributes in vue, we have to use another directive called ( you guessed it ) v-bind. This will allow us to dynamically update attributes.

In our cards above, we still need to hook up the <a href="">, and <img src="" alt=">.

When using v-bind, the contents of the attribute will be treated as javascript. So we can pass variables right into it.

...
<li class="repo" v-for="repo in repos">
  <a v-bind:href="repo.html_url" class="repo__link">
    <div class="repo__image"><img v-bind:src="repo.owner.avatar_url" v-bind:alt="repo.full_name" /></div>
    ...
  </a>
</li>
...
1
2
3
4
5
6
7
8

Directive shorthands

Writing v-on and v-bind on everything can get a little tedious after a while, so there are some convenient shorthands we can use to save some space/time.

  • v-on: can be replaced with @
  • v-bind: can be written as simply :

Let's update our code as follows:

...
<form @submit="searchRepos">...</form>
...
<li class="repo" v-for="repo in repos">
  <a :href="repo.html_url" class="repo__link">
    <div class="repo__image"><img :src="repo.owner.avatar_url" :alt="repo.full_name" /></div>
    ...
  </a>
</li>
1
2
3
4
5
6
7
8
9

Let's test the application, and make sure everything is working fine.

Component Registration: Vue.component

Now that we have a working application, we can think about splitting the parts of our app into smaller components, in order to make them easier to maintain.

It might not seem necessary at this scale, but it becomes increasingly helpful as your application expands.

Let's start by separating some of the smaller parts of our app into components, such as the searching and errors sections.

First, we need to register a new component, using Vue.component(). This method takes two arguments: a name: String, and a config: Object.

...
  Vue.component('searching', {});
...
1
2
3

Inside the config Object, we can add data specific to this component, such as a template: String and data: Function (ie. state).

...
  Vue.component('searching', {
    template: ``,
    data() {
      return {};
    }
  });
...
1
2
3
4
5
6
7
8

Note

Notice that the data property on our component look a little different than before? Because components are reusable, we can no longer declare our state as an Object.

If we did, every instance of that component would share the same data. Instead, we make data a function, which returns an Object. This way, each component will have unique state!

Now, we can move the HTML searchingMessage in our app to the data in our component.

...
  Vue.component('searching', {
    template: ``,
    data() {
      return {
        searchingMessage: "Searching...",
      };
    },
  });
...
1
2
3
4
5
6
7
8
9
10

Let's take the HTML for the searching section, and put it inside of our template. You can use back-ticks ( ` ` ) in order make them multi-line, with template literals!

...
  Vue.component('searching', {
    template: `
      <section class="searching">
        <p>{{ searchingMessage }}</p>
      </section>
    `,
    data() {
      return {
        searchingMessage: "Searching...",
      };
    },
  });
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Note

Remember, component templates must have a single root element.

Note

Since we are now including the searchingMessage in this component, we need to remember to remove it from the main Vue Object.

Finally, we can render our component in our app, by using the component name as the element. Let's also include the v-if from before, but this time at the component level.

...
<div id="app" class="wrapper">... <searching v-if="search.isSearching"></searching> ...</div>
...
1
2
3

Note

You can't use self-closing components like <my-component /> in Vue DOM templates (CDN), because it is not valid HTML. But it is still valid and recommended in single-file components (CLI). Components must also be lower-case, and cabab-case.

Let's do the same thing for the errors section.

...
<div id="app" class="wrapper">
  ... <searching v-if="search.isSearching"></searching> <errors v-if="search.hasError"></errors> ...
</div>
...
1
2
3
4
5
...
  Vue.component('errors', {
    template: `
      <section class="errors">
        <h2>{{ errorMessage }}</h2>
      </section>
    `,
    data() {
      return {
        errorMessage: "Oops, nothing here",
      };
    },
  });
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14

Note

Since we are now including the errorMessage in this component, we need to remember to remove it from the main Vue Object.

Now that we have a little practice building components, let's take care of the more complex components of our app: Search, and Results!

... <search></search> ...
1
...
  Vue.component('search', {
    template: `
      <section class="search">
        <h1>Search for Github repo</h1>
        <form @submit.prevent="searchRepos">
          <input
            type="search"
            name="search"
            id="search"
            required
            v-model="q"
          >
          <label for="search">Repo search</label>
        </form>
      </section>
    `,
    data() {
      return {
        q: '',
      }
    }
  });
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Note

Since we are now including the q in this component, we need to remember to remove it from the main Vue Object.

Let's try out the app and make sure it still works!

⚠️ Uh oh!

Nothing is happening when we search, and we have an error in the console which looks like [Vue warn]: Property or method "searchRepos" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property 🤔. But isn't it thought?

The problem is that we are trying to call a Method that doesn't exist on this component. We need to find a way to call up to the main Vue Object to trigger our searchRepos method, and send along our query, q.

Emit events

Vue has an elegant way of handling these sorts of events, where we need to trigger something in the parent Object, and pass along some arguments, using $emit.

We can start by replacing the name of the method we were trying to call directly, with $emit(). This will take two arguments: an eventName, so we can reference the event by name later, and the [...args] or payload. We'll call our event "search-event", and pass our query q as the second argument.

...
  Vue.component('search', {
    template: `
      ...
      <form @submit.prevent="$emit('search-event', q)">
        ...
      </form>
      ...
    `,
    ...
  });
...
1
2
3
4
5
6
7
8
9
10
11
12

Now in our HTML, when we reference the search component, we can listed for our custom event called search-event. This is done the same way we listen for standard events with v-on or @, but referencing the custom name of the event, search-event. The argument that is expected is represented by the special $event property, containing the payload that was passed through.

... <search @search-event="searchRepos($event)"></search> ...
1

By doing so we are passing the q to the searchRepos method.

Note

It's worth noting that HTML attribute names are case sensitive, and the browser will treat any uppercase letters as lowercase. So when using in-DOM templates like with the CDN, we must remember to use kabab-case on prop attributes.

We must also update our searchRepos method to expect the $event payload to be passed in. We'll name the argument q.

⚠️ Gotcha Alert ⚠️

Since we are no longer referencing q from the state, we can remove this. from the url template string. Remember, this refers to the parent Object: either the main Vue Object, or the current component. We want to use the argument being passed into this method instead.

...
  methods: {
    async searchRepos(q) { // <== The argument we expect, called `q`
    ...
      // const response = await fetch(`https://api.github.com/search/repositories?q=${this.q}`); <== remove `this.`
      const response = await fetch(`https://api.github.com/search/repositories?q=${q}`);
      ...
    },
    ...
  }
...
1
2
3
4
5
6
7
8
9
10
11

Let's convert our remaining results section into its own component.

... <results v-if="repos.length"></results> ...
1
...
  Vue.component('results', {
    template: `
      <section class="results">
        <ul>
          <li v-for="repo in repos" class="repo">
            <a :href="repo.html_url" class="repo__link">
              <div class="repo__image">
                <img :src="repo.owner.avatar_url" :alt="repo.full_name">
              </div>
              <div class="repo__meta">
                <p>
                  <strong>Dev:</strong> {{ repo.owner.login }}
                </p>
                <p>
                  <strong>Repo:</strong> {{ repo.name }}
                </p>
              </div>
            </a>
          </li>
        </ul>
      </section>
    `
  });
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

Let's try out the app and make sure it still works!

⚠️ Uh oh! ⚠️

Another error, similar to before [Vue warn]: Property or method "repos" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.

Here we are referencing repos, but this component does not have access to it in the component. Let's solve this in the next step, using props.

Props

In order to allow our results component access to the repos in the main Vue Object, we need to pass props to the component.

props is short for "properties", and is used to pass data to components, similar to the way we pass arguments to functions.

We pass a prop to a component using v-bind, similar to when we need to dynamically update an attribute. We start be giving our results component an attribute with the name we want to represent it, and pass in the repos in our data Object as the argument. We can also use the short-form, as follows:

... <results :repos="repos" v-if="repos.length"></results> ...
1

Note

We use v-bind or : to pass dynamic props to a component. But you can also pass hard-coded props by omitting the prefix, like title="Vue JS Workshop". In this case, the prop is treated as a String.

In the component itself, we need to register the prop, so that the component knows what to expect. In its simplest form, props can be represented by an Array of Strings.

However, you can also make props an Object, with the key being the name of the prop, and the value being the prop type, such as String, Array, Object etc. This can help offer useful warnings if passing an unexpected prop type.

...
  Vue.component('results', {
    template: `
      <section class="results">
      ...
      </section>
    `,
    props: {
      repos: Array
    },
  });
...
1
2
3
4
5
6
7
8
9
10
11
12

Check it out. Yay, our application works again!