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>
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>
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>
2
3
4
5
6
7
8
9
10
el
: The id of the element our app should be rendered indata
: ( a.k.a.state
) data saved within our app. This is anObject
.
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"
}
});
...
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>
...
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::
]
v-model
Two-way data binding: 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"
}
...
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>
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: {}
});
...
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,
}
},
...
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() {}
}
...
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 totrue
- Call
resetSearch
(we'll build thismethod
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
inrepos
- If we get no repos back, then we want to set the
search.hasError
totrue
.
...
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() {}
}
...
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 = [];
}
}
...
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
.
v-on
Event handling: 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>
...
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>
...
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.
...
...
2
3
4
5
6
v-if
Conditional Rendering: 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>
...
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.
v-for
Render loops: 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>
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
...
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>
...
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.
v-bind
Directives: 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>
...
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>
2
3
4
5
6
7
8
9
Let's test the application, and make sure everything is working fine.
Vue.component
Component Registration: 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', {});
...
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 {};
}
});
...
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...",
};
},
});
...
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...",
};
},
});
...
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>
...
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>
...
2
3
4
5
...
Vue.component('errors', {
template: `
<section class="errors">
<h2>{{ errorMessage }}</h2>
</section>
`,
data() {
return {
errorMessage: "Oops, nothing here",
};
},
});
...
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> ...
...
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: '',
}
}
});
...
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>
...
`,
...
});
...
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> ...
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}`);
...
},
...
}
...
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> ...
...
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>
`
});
...
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> ...
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
},
});
...
2
3
4
5
6
7
8
9
10
11
12
Check it out. Yay, our application works again!