Look for the 🛠️️ emoji if you'd like to skim through the content while focusing on the build steps.
In this article, you'll learn how to build a Vue website that will showcase events hosted by an organization. Users will sign in to view the details page of an event, so you'll also add authentication to the website. You'll learn some Vue background, Vue application structure, creating components, setting up routing, and styling with Bulma.
You can find the final code in this GitHub repository if you'd like to see the end result now.
❗ If you already have a Vue application and just want to add authentication, jump to this section to learn how.
⚡️ This is a two-part tutorial! Part one does not cover authorization. You will set the stage for part two by building the website and integrating user login. Then in part two, you'll learn how to only show certain data if a user is signed in and has been authorized to access that data.
Requirements
This application is using the latest versions (at the time of writing — 8/8/2020) of the following:
Note: This tutorial uses Vue 2. The release for Vue 3 is scheduled for the third quarter of 2020, but that doesn't mean you shouldn't start with Vue 2! This tutorial lays down the foundations of Vue that will carry over into the next release as well.
You can download Node.js here, which automatically installs npm, a Node package manager, as well.
Npm allows you to quickly install millions of libraries and easily manage any dependencies you have in your applications. You'll see how beneficial this is soon.
This tutorial will walk you through how to create everything from scratch, so you don't need to have any prior knowledge of Vue or Node to follow along! You will, however, need a code editor (VS Code is my favorite) and access to the terminal to follow this tutorial.
Let's get started!
Why Learn Vue?
Vue history and popularity
Vue.js is a JavaScript framework created by Evan You that has blown up in popularity over the last few years.
Source: 2018 State of JS Survey Interest in Vue.js has nearly tripled from 2016 to 2018 with an increase in those who are interested in learning it and those who would use it again.
Source: 2018 State of JS Survey In 2018, the majority of people surveyed have heard of Vue.js and would like to learn it or have already used it and would use it again.
Evan You launched the project after having worked on several AngularJS projects when he worked for Google. Vue isn't backed by huge companies like Google (Angular) or Facebook (React), but as you can see in the charts above, Vue is still able to stand on its own among the top JavaScript frameworks.
Because of Evan You's history at Google, you might even notice some similarities between Vue and AngularJS (old Angular).
I figured, what if I could just extract the part that I really liked about Angular and build something really lightweight.
— Evan You on creating Vue.js
Vue vs. Angular vs. React
One of the best things about Vue is the low barrier to entry.
Source: 2018 State of JS Survey For developers who chose "would use again", their favorite Vue.js features were the easy learning curve, elegant programming style, and good documentation.
A lot of other frameworks may require (or strongly suggest) that you learn their own syntax or integrate other technologies (e.g., TypeScript for Angular or JSX for React). Of course, there's nothing wrong with this, but it does make it a little harder for a beginner to get started when they have to know all the things right away. Vue allows you to use TypeScript or even JSX if you'd like, but it's not required. This isn't to say one method is better than the other, but this HTML-like syntax is one possible explanation as to why developers find Vue easier to learn initially.
Again, when it comes to comparing the popular frameworks, none of them are better than the others. At the end of the day, it comes down to what you feel most comfortable using.
Vue is a great option if you're looking to get something up and running quickly. It has great community support, robust documentation in several languages, and it's just fun to use!
Setting Up
Alright, let's get started with some code.
First, open up your terminal and switch to the directory that you want to store the project in.
You're going to create this new project using the Vue CLI.
🛠️️ The Vue CLI tool helps get up and running with Vue quickly by letting you choose from pre-configured build setups. To use the CLI, run the following in your terminal:
npx @vue/cli create events-app
This creates a new folder called events-app
and starts the installation process in that folder.
Note:
npx
allows you to use the Vue CLI without installing it globally. It's available in versions of npm >= 5.2.0.
The CLI will now ask you a few questions so that it can set up the app correctly. The options that this tutorial uses are listed below. Press ENTER
to select.
🛠️️ Step 1: Pick a preset — Manually select features
? Please pick a preset:
default (babel, eslint)
> Manually select features
🛠️️ Step 2: Check the features needed — Babel
, Router
, CSS Pre-processors
, Linter/Formatter
Press
SPACE
to select multiple features andENTER
once you have all features selected.
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
◉ Babel
◯ TypeScript
◯ Progressive Web App (PWA) Support
◉ Router
◯ Vuex
◉ CSS Pre-processors
◉ Linter / Formatter
◯ Unit Testing
◯ E2E Testing
🛠️️ Step 3: History mode — Y
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) Y
Note: This will remove the default hash (#) from URLs.
🛠️️ Step 4: CSS pre-processor — Sass/SCSS (with dart-sass)
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): (Use arrow keys)
❯ Sass/SCSS (with dart-sass)
Sass/SCSS (with node-sass)
Less
Stylus
Note: Dart Sass is the primary implementation of Sass.
🛠️️ Step 5: Pick a linter/formatter —ESLint with error prevention only
? Pick a linter / formatter config: (Use arrow keys)
❯ ESLint with error prevention only
ESLint + Airbnb config
ESLint + Standard config
ESLint + Prettier
🛠️️ Step 6: Additional lint features — Lint and fix on commit
? Pick additional lint features:
◯ Lint on save
❯ ◉ Lint and fix on commit
🛠️️ Step 7: Config location — In dedicated config files
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
In package.json
🛠️️ Step 8: Save as preset — N
? Save this as a preset for future projects? (y/N) N
Now the CLI will configure your application. It will also install the dependencies, so give it a little time (~30 seconds).
🛠️️ Once it's finished, you can start the application by running npm run serve
or yarn serve
:
cd events-app
npm run serve
See it in action at http://localhost:8080/
! Go ahead and keep this running in the background so that you can see the app's progress as you go through the tutorial.
"The Vue CLI helps you get up and running with Vue.js in seconds!"
Tweet This
Vue App Architecture
Now open up your project in your code editor, and you'll see some default files that the CLI created. Let's clean some of this up.
🛠️️ Delete these files:
src/components/HelloWorld.vue
src/assets/logo.png
🛠️️ Now open up src/views/Home.vue
and replace it with the following:
<template>
<div class="home"></div>
</template>
<script>
export default {
name: 'home',
components: {},
};
</script>
This will leave you with a blank homepage and a nav bar with two links: Home and About.
Let's take a quick look at the important remaining files so that you can see how they work together.
main.js
The first file to note is the src/main.js
file. This will be the entry point for your Vue application.
import Vue from 'vue';
import App from './App.vue';
import router from './router';
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App),
}).$mount('#app');
You have your imports at the top of the file:
- Vue itself
- The
App
component (which we'll go over in a moment) - The router (created by the CLI).
Next, you have Vue.config.productionTip = false
, which sets the app in development mode.
And finally, you're creating a new Vue instance at the bottom.
A Vue instance is required for every Vue application. The Vue instance will accept an options
object that contains information about your application, such as the DOM element that the Vue instance will be mounted on, data the instance will use, functions that run at some point during the instance's lifecycle, and more.
In this case, the Vue instance is using the router, rendering the app with the App
template (more on this soon), and then mounting it to the DOM element with the id
of app
.
index.html
Next, open up public/index.html
, and you'll see a fairly standard index
HTML file.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong
>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work
properly without JavaScript enabled. Please enable it to
continue.</strong
>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
If you look toward the bottom of the file, you'll see where that mysterious #app
in the src/main.js
file comes into play. That div
is where the Vue instance will be injected.
App.vue
The next important file is src/App.vue
. This is your first component!
You don't need to know all of the details of this file yet, just know that this is the next "building block" of your Vue application. When you reviewed the src/main.js
file, you saw it was importing and rendering a template called App
, which is what this file is.
Before diving into the details of this file, let's first learn about what a component even is by reviewing Vue's component system.
Using Components in Vue
The concept of components, in general, is sometimes over-complicated, but in reality, it's quite simple. A component is a modular and reusable block of code. It contains all of the HTML, JavaScript, and CSS that it requires for its functionality.
Imagine you have a simple website that has two pages: a home page and a portfolio page.
- Home page — Contains some images, basic information, and a carousel with some testimonials
- Portfolio page — Contains some images and descriptions of your work
You decide that you want your Portfolio page to include all of the testimonials that are on your homepage as well.
In this scenario, you can copy/paste all of the code required for the testimonials onto both pages, but then you'd be breaking the golden DRY ("Don't Repeat Yourself") rule.
This is where the idea of components starts to make sense. Instead of creating duplicate code, you can pull that testimonial code out and package it into its own file. This standalone chunk of code will contain the HTML, the styles, and any JS needed to make the carousel work.
Now, whenever you want to insert that testimonial "component" somewhere, all you have to do is import that component!
Your application will begin to take on a sort of tree structure. You'll have your "root" component, which in this case is src/App.vue
, and that component will import other components. All of these small chunks of code come together to build your application.
Source: Vuejs.org
Now that you have the idea of components down, let's see what a Vue component looks like.
Vue components
There are a lot of ways to structure components in Vue. Let's look at the most popular way: Single file components.
Single file components package up the template, logic, and styles used by a component into one file with a .vue
extension. This method requires a build setup, which the CLI has already set up for you. Let's take a look at an example component.
<template>
<div class="our-component">
<h1>I'm a component!</h1>
<ChildComponent />
</div>
</template>
<script>
import ChildComponent from '@/components/ChildComponent';
export default {
name: 'MyComponent',
components: {
ChildComponent,
},
};
</script>
<style lang="scss" scoped>
.our-component {
text-align: center;
}
</style>
The HTML is enclosed in a <template></template>
tag. Inside, you'll see <ChildComponent />
. This is an example of how you can use another component inside of this one.
Note: You must always have a parent
<div>
element that encloses the rest of the HTML after the template tag.
Next, there is a script
tag, which includes:
- An
import
statement that allows you to import and use other components inside this component. - An
export
object that allows you to define and export this named component to reuse it across your application. - A
components
object inside the export where you can list all of the child components used in this component.
Note: This is a pretty basic example, but there are loads of other component options you can include in your component object.
Next, there is the <style>
tag. The styles defined here apply to this component and all of its children. In this case, however, there is a scoped
property added, which restricts these styles to only this component. And finally, you're also specifying that you're using SCSS as the stylesheet language.
Let's take a look at that App.vue
file again now that you have a little more background about component structure.
App.vue
Open up src/App.vue
.
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view />
</div>
</template>
<style lang="scss">
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>
This is the first view template to be rendered in your application. The rest of the components will start from here.
Take a look at <router-link>
and <router-view>
. This is how your application will handle routing. If you chose to have the router built-in using the CLI, you already have a nice template here to use!
We'll go more in-depth on routing soon, but just know that when you click on one of those links, this same template will still be rendered, but <router-view />
will be replaced with whatever route is active. In this case, either the Home
or About
component. This is how you'll be able to reuse this same layout and navbar across the entire application.
Try clicking those links to see for yourself!
Building Application Components
Now that you know the anatomy of a Vue application and how to build a component, it's time to start building!
🛠️️ Make sure you're still in the events-app
folder in the terminal and then create these files/folders:
cd src/views
touch EventSingle.vue
Note: If you're on Windows, the
touch
command may not work, depending on your setup. You can always create the file manually instead.
🛠️️ Now make the components that will be shared among these pages:
cd ../components
touch EventsList.vue
touch EventCard.vue
mkdir partials
touch partials/Nav.vue
That should do it for now! Let's start filling these files in and go over the purpose of each file.
Using Bulma for Styling
To make styling a little easier, you're going to use Bulma, which is an open-source CSS framework.
🛠️️ Switch to the events-app
folder and then enter the command below to install Bulma:
cd ../..
npm install bulma
🛠️️ Now open up src/main.js
in your editor and import Bulma by adding this to the top of the file after all of the other imports:
// ...
import 'bulma/css/bulma.css';
// ...
🛠️️ Start the app up one more time so that you can see your work in progress as you continue building in the next section.
npm run serve
You can just leave this running in the background for the rest of this tutorial, and you'll always be able to view it in the browser at http://localhost:8080
.
Home Component
Now let's start working on the components.
🛠️️ Open up the Home Component in src/views/Home.vue
and paste in the following:
<template>
<div class="home">
<section class="hero is-dark">
<div class="hero-body">
<div class="container">
<h1 class="title">Welcome to the Animal Rescue League</h1>
<h2 class="subtitle">
Make sure you check out our upcoming events below
</h2>
<div class="button-block">
<button class="button is-xl is-dark">
Sign Up to Browse Events
</button>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'home',
components: {},
};
</script>
<style lang="scss" scoped>
.hero {
text-align: center;
background-image: url('https://cdn.auth0.com/blog/vue-meetup/event-banner.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
height: 400px;
}
.hero-body .title {
text-shadow: 4px 4px 4px rgba(0, 0, 0, 0.6);
padding: 40px 0 20px 0;
font-size: 60px;
}
.subtitle {
text-shadow: 4px 4px 4px rgba(0, 0, 0, 0.7);
font-size: 30px;
}
.button-block {
text-align: center;
margin-left: auto;
margin-right: auto;
width: 100%;
position: absolute;
bottom: -150px;
.button {
margin-right: 50px;
padding-left: 50px;
padding-right: 50px;
}
.welcome {
width: 400px;
padding: 10px;
margin-left: auto;
margin-right: auto;
}
}
.is-xl {
font-size: 1.7rem;
}
</style>
This will give you a big banner and button at the top of the page. The button will lead to the signup form, which you'll wire up later.
About Component
The About
component comes default with the Vue install, but let's just spruce it up a little.
🛠️️ Open up src/views/About.vue
, and replace all of it with this:
<template>
<div class="about">
<div class="hero is-primary">
<div class="hero-body">
<div class="container">
<h1 class="title is-size-1">About Animal Rescue League</h1>
</div>
</div>
</div>
<div class="container">
<p class="org-description is-size-4">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius quia
aperiam eligendi dolorum reprehenderit ea amet, aliquid dolorem beatae,
iste aliquam ullam. Sequi ab eligendi consectetur neque laudantium,
libero asperiores.
</p>
<p class="org-description is-size-4">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Eius quia
aperiam eligendi dolorum reprehenderit ea amet, aliquid dolorem beatae,
iste aliquam ullam. Sequi ab eligendi consectetur neque laudantium,
libero asperiores.
</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.org-description {
margin-top: 50px;
}
</style>
Nav Component
🛠️️ Now fix up the nav. Open src/components/partials/Nav.vue
and paste this in:
<template>
<nav class="navbar container" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a class="navbar-item" href="/">
<strong class="is-size-4">Animal Rescue League</strong>
</a>
<a
role="button"
class="navbar-burger burger"
aria-label="menu"
aria-expanded="false"
data-target="navbarBasicExample"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbar" class="navbar-menu">
<div class="navbar-start">
<router-link to="/" class="navbar-item">Home</router-link>
<router-link to="/about" class="navbar-item">About</router-link>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-dark">
<strong>Sign In</strong>
</a>
</div>
</div>
</div>
</div>
</nav>
</template>
<script>
export default {
name: 'Nav',
};
</script>
<style lang="scss" scoped>
nav {
margin-top: 25px;
margin-bottom: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #d88d00;
}
}
}
</style>
If you refresh, you'll notice the nav doesn't change. That's because you're not actually using this component anywhere.
🛠️️ Open up src/App.vue
and replace it with this:
<template>
<div id="app">
<nav />
<router-view />
</div>
</template>
<script>
import Nav from './components/partials/Nav.vue';
export default {
name: 'app',
components: {
Nav,
},
};
</script>
<style lang="scss">
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
</style>
Since you're defining the router links in the Nav
component now, you can get rid of them in this file. All you need to do is import the Nav
component. The old #nav
styles have also been deleted since the Nav
component has its own styles.
Now, if you refresh, you should see your new nav! You can even click around, and it'll open up the different routes you've specified in the Nav
component.
Next up, you'll add a component to your Home
page.
EventsList Component
🛠️️ Open src/components/EventsList.vue
and paste this in:
<template>
<div class="events container">
<h2 class="subtitle is-3">Check out our upcoming events</h2>
<div class="columns is-multiline">
<div class="column is-one-quarter">
<EventCard />
</div>
</div>
</div>
</template>
<script>
import EventCard from '@/components/EventCard';
export default {
name: 'EventsList',
components: {
EventCard,
},
};
</script>
<style lang="scss" scoped>
.events {
margin-top: 100px;
text-align: center;
}
</style>
This will create a block of cards that takes up 1/4 of the row (by using Bulma's column
with is-one-quarter
). Each of those 1/4 slots will be filled with the EventCard
component.
🛠️️ For now, just repeat that card code a couple of times so that you can see how the structure will look.
<div class="columns is-multiline">
<div class="column is-one-quarter">
<EventCard />
</div>
<div class="column is-one-quarter">
<EventCard />
</div>
</div>
Next you need to:
- Import this into the
Home
component - Add it to the list of components that
Home
uses inexport default {}
- Slot it into the appropriate place in the
Home
template
🛠️️ Open up src/views/Home.vue
and replace the script
section with this:
<script>
import EventsList from '../components/EventsList';
export default {
name: 'home',
components: {
EventsList,
},
};
</script>
🛠️️ Now in the template section, call the component with <EventsList />
.
<template>
<div class="home">
<section class="hero is-dark">
<!-- ... -->
</section>
<EventsList />
</div>
</template>
You should now see the subtitle text, "Check out our upcoming events", rendered below the homepage banner. However, none of the individual cards are showing yet. That's because you haven't created them.
Event Card Component
🛠️️ Open up src/components/EventCard.vue
and paste in:
<template>
<div class="event-card">
<div class="card">
<div class="card-content">
<h2 class="is-size-4 has-text-weight-bold">Event name</h2>
<small class="event-date">Event date</small>
<span>Event location</span>
</div>
</div>
</div>
</template>
<script>
export default {};
</script>
<style lang="scss" scoped>
.card {
background-image: url('https://placekitten.com/400/400');
height: 200px;
background-position: center;
background-size: cover;
text-align: center;
}
.card-content {
padding-top: 50px;
position: absolute;
color: #fff;
background-color: rgba(0, 0, 0, 0.35);
top: 0;
padding: 10px;
height: 200px;
width: 100%;
span {
font-size: 18px;
text-align: center;
width: 100%;
position: absolute;
bottom: 10px;
right: 0;
}
h2 {
margin-top: 10px;
}
}
.event-date {
background-color: #151515;
color: #fff;
font-size: 0.75em;
padding: 2px 10px;
position: absolute;
top: 0;
right: 0;
}
</style>
You should now see the two cards filled in with some mock content.
Of course, you're going to want each card to be unique and represent an actual event. You'll come back to this component soon to see how you can feed data in and accomplish this.
For now, finish setting up the rest of your components.
Event Single Component
🛠️️ Open up src/views/EventSingle.vue
and paste this in:
<template>
<div class="event-single">
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
<h1 class="title">Event name</h1>
<h2 class="subtitle ">Event date</h2>
</div>
</div>
</section>
<section class="event-content">
<div class="container">
<p class="is-size-4 description">Event description</p>
<p class="is-size-4">Location:</p>
<p class="is-size-4">Category:</p>
<div class="event-images columns is-multiline has-text-centered">
<div class="column is-one-third">IMAGE PLACEHOLDER</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {};
</script>
<style lang="scss" scoped>
.event-single {
margin-top: 30px;
}
.hero {
margin-bottom: 70px;
}
.event-images {
margin-top: 50px;
}
.description {
margin-bottom: 30px;
}
</style>
This will be the page that appears when a user clicks on an event from the list on the homepage. Because this page doesn't actually exist yet, this is a great time to revisit your router.
Router paths
🛠️️ Open up src/router/index.js
and replace the contents with this:
import Vue from 'vue';
import Router from 'vue-router';
import Home from '../views/Home.vue';
Vue.use(Router);
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue'),
},
{
path: '/event/:id',
name: 'eventSingle',
component: () => import('../views/EventSingle.vue'),
},
],
});
Press save and navigate to http://localhost:8080/event/1
. You should now see the contents of the EventSingle
component!
If you look under path
in the router, you'll see a route parameter, :id
. This is how you'll create separate pages for every event. Once you add in the data, you can navigate to any event by appending its id
to the end of /event/
.
Let's work on adding data now.
Adding Data to your Vue App
To keep this tutorial focused on the basics of Vue, you're just going to create an array of objects (where each object is an event) and store it in the component that needs it.
Ideally, you would want to pull data from an API to dynamically fill in the block of cards, but that's a little outside the scope of this tutorial. If you're interested in making calls to an API using Vue and securing any data you have, make sure you read part 2 of this tutorial where you'll do just that!
Let's revisit those three components that were missing dynamic data and see how to fill them in.
EventsList Component
🛠️️ Head back to the EventsList
component in src/components/EventsList.vue
and scroll down to the bottom where the <script>
tag starts. Replace the entirety of <script></script>
with this:
<script>
import EventCard from '@/components/EventCard';
export default {
name: 'EventsList',
components: {
EventCard,
},
data() {
return {
event: {},
events: [
{
id: 1,
name: 'Charity Ball',
category: 'Fundraising',
description:
'Spend an elegant night of dinner and dancing with us as we raise money for our new rescue farm.',
featuredImage: 'https://placekitten.com/500/500',
images: [
'https://placekitten.com/500/500',
'https://placekitten.com/500/500',
'https://placekitten.com/500/500',
],
location: '1234 Fancy Ave',
date: '12-25-2019',
time: '11:30',
},
{
id: 2,
name: 'Rescue Center Goods Drive',
category: 'Adoptions',
description:
'Come to our donation drive to help us replenish our stock of pet food, toys, bedding, etc. We will have live bands, games, food trucks, and much more.',
featuredImage: 'https://placekitten.com/500/500',
images: ['https://placekitten.com/500/500'],
location: '1234 Dog Alley',
date: '11-21-2019',
time: '12:00',
},
],
};
},
};
</script>
The only thing that's changing here is the addition of a data()
function that returns an empty object called event
and an array called events
.
- The
events
array holds 2 objects that each correspond to a different event - The
event
object will hold a single event to be passed to the child component,EventCard
Next, you need to modify the HTML portion of this file to loop through these events and then send each one down the chain to be rendered by the child component, EventCard
.
🛠️️ Replace everything between the <template></template>
tags with:
<template>
<div class="events container">
<h2 class="subtitle is-3">Check out our upcoming events</h2>
<div class="columns is-multiline">
<div
v-for="event in events"
:event="event"
:key="event.id"
class="column is-one-quarter"
>
<router-link :to="'/event/' + event.id">
<EventCard :event="event" />
</router-link>
</div>
</div>
</div>
</template>
Let's take a closer look at the block of code that changed (spaced out for better readability).
<div
v-for="event in events"
:event="event"
:key="event.id"
class="column is-one-quarter"
>
<router-link :to="'/event/' + event.id">
<EventCard :event="event" />
</router-link>
</div>
You can loop over the events in the events
array using the Vue directive v-for
. This acts as a for
loop and pulls out each event individually so that it can be rendered separately by the EventCard
component.
This is also the first time you're seeing a directive!
A directive is a special type of markup that you can use to do something to a DOM element. In Vue, these all begin with v-
. So, in this case, you're using the shorthand v-for
to create a for
loop. Another common example is v-if
, which creates a conditional inside the template. You can see a full list of Vue directives here.
Here, you're using Vue's :key
attribute to bind a unique key (in this case, id
) to each event.
Note: In Vue,
:
is shorthand forv-bind
.
You're also binding the value of the current event
in the for
loop to the event
object you created earlier with :event="event"
. This will allow you to send this object down to the child component to be rendered separately.
Next, you're using <router-link>
to make each card into a clickable link. Back in the router.js
file, you created the route /event/:id
with a route parameter of id
. This route uses the EventSingle
component, which will render the page for each specific event. You're using the id
from each event to make sure it links to the correct event page.
Go ahead and click one, and you'll see that it links to the single event page that you created.
Finally, you're calling the EventCard
component to fill in each individual card. You're sending a variable called event
down to the card and then passing in the current event
from the for
loop.
Right now, the EventCard
component isn't prepared to accept data, so let's fix that.
EventCard Component
🛠️️ Open the EventCard
component in src/components/EventCard.vue
. Scroll down to where you have the <script></script>
tags and replace it with this:
<script>
export default {
props: ['event'],
};
</script>
This component has something new: props
.
Whenever your component is expecting data, you should add that data's variable name to the props
option. props
is kind of a funny word, but it actually just means property.
A prop is a property on a component.
This is where you define the data that the component should be expecting from its parent component.
This event
prop was passed to the current component from the parent component. This allows you to display it in the HTML template using curly braces:
{{ event }}
This is known as interpolation. Using string interpolation, you're able to display the value of event
in your template. This is a form of data-binding, which means that whenever the value of event
in data
changes, the displayed value in the template will also change.
🛠️️ To see this in action, go ahead and update the template section of EventCard.vue
with this:
<template>
<div class="event-card">
<div class="card">
<div class="card-content">
<h2 class="is-size-4 has-text-weight-bold">{{ event.name }}</h2>
<small class="event-date">{{ event.date }}</small>
<span>{{ event.location }}</span>
</div>
</div>
</div>
</template>
If you go back to the homepage in your browser, you'll see the cards now have the correct data!
Just to summarize, the parent component, EventsList
, sent the event data down from the for
loop two separate times. Each time the EventCard
component was called, it received the event
data into props
, which allowed you to render the name, date, and location.
EventSingle Component
The last part of the application that needs updated data is the EventSingle
component. Open up src/views/EventSingle.vue
.
You can see the current state of this component in the browser at http://localhost:8080/event/1
. Everything is still hard-coded in and waiting for data.
This component is a great example of how passing data starts to get tricky.
Think back to how the link to this page was set up in EventsList.vue
:
<router-link :to="'/event/' + event.id">
<EventCard :event="event" />
</router-link>
It uses the event.id
from the for
loop to create the link for each card/event, but how do you send the actual data through?
You can use a method similar to the one you used before to fill in the data from EventCard
and send some data down into a prop
on EventSingle
. But then you'd be using the router to manage data, which is a little strange.
You also have to consider what would happen if instead of clicking on the link, someone went directly to http://localhost:8080/event/1
. How would it receive the props that it's expecting if that specific <router-link>
code was never fired?
It wouldn't.
So for this application, your best option is to just pull the data again.
Here's the gist of what needs to happen:
- Grab the
id
from the route parameter (in the URL) - Use that to pull out the correct event from the list of all events
- Fill in the template with data from that specific event
🛠️️ To do this, open up EventSingle.vue
and replace it entirely with:
<template>
<div class="event-single">
<section class="hero is-primary">
<div class="hero-body">
<div class="container">
<h1 class="title">{{ event.name }}</h1>
<h2 class="subtitle ">
<strong>Date:</strong> {{ event.date }}
<br />
<strong>Time:</strong> {{ event.time }}
</h2>
</div>
</div>
</section>
<section class="event-content">
<div class="container">
<p class="is-size-4 description">{{ event.description }}</p>
<p class="is-size-5"><strong>Location:</strong> {{ event.location }}</p>
<p class="is-size-5"><strong>Category:</strong> {{ event.category }}</p>
<div class="event-images columns is-multiline has-text-centered">
<div
v-for="image in event.images"
:key="image.id"
class="column is-one-third"
>
<img :src="image" :alt="event.name" />
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
name: 'EventSingle',
data() {
return {
events: [
{
id: 1,
name: 'Charity Ball',
category: 'Fundraising',
description:
'Spend an elegant night of dinner and dancing with us as we raise money for our new rescue farm.',
featuredImage: 'https://placekitten.com/500/500',
images: [
'https://placekitten.com/500/500',
'https://placekitten.com/500/500',
'https://placekitten.com/500/500',
],
location: '1234 Fancy Ave',
date: '12-25-2019',
time: '11:30',
},
{
id: 2,
name: 'Rescue Center Goods Drive',
category: 'Adoptions',
description:
'Come to our donation drive to help us replenish our stock of pet food, toys, bedding, etc. We will have live bands, games, food trucks, and much more.',
featuredImage: 'https://placekitten.com/500/500',
images: ['https://placekitten.com/500/500'],
location: '1234 Dog Alley',
date: '11-21-2019',
time: '12:00',
},
],
event: {},
};
},
created() {
const ID = Number(this.$route.params.id);
let event = this.events.find((event) => event.id === ID);
this.event = event;
},
};
</script>
Scroll down to the <script>
section and you'll see that the data you've been using has also been added to this component. But this time, there's a new chunk of code:
created() {
const ID = Number(this.$route.params.id);
const event = this.events.find(event => event.id === ID);
this.event = event;
}
First, let's go over what created()
is used for.
The Vue instance goes through many steps when it's created. It sets up data observation, compiles the template, mounts the Vue instance to the DOM, and updates the DOM when changes are detected.
But what if you need to jump in at some point between these steps and run some code? Vue offers functions called lifecycle hooks that let you do just that.
The created()
hook lets you run some code right after the instance is created.
There are quite a few lifecycle hooks available, as you can see in the image below.
Image Source: Vue.js Guide - Instance
So right after the instance is created, the created()
function runs.
created() {
const ID = Number(this.$route.params.id);
const event = this.events.find(event => event.id === ID);
this.event = event;
}
This function creates a variable called ID
that will hold the id
of the event you want to render. You're able to pull this id
from the route parameter id
that you set up earlier in src/router/index.js
.
Next, you're creating a variable called event
, which will hold the event object. It uses the JavaScript function find()
on the events
array, which will loop through the array until it finds an event with the id
of ID
(the route parameter).
Then you're setting the event
variable to the result of this. And now, you have the data needed to render this specific event!
Ideally, you'd want to pull this data from an API. In a case like this, you would just hit the endpoint that returns the specific event that you want by sending over the route parameter, but since you don't have an API yet, this method is fine. In the follow up of this tutorial, you'll refactor this code to use an actual API.
Another cool thing you're doing is looping over the images in the specific event. Let's take a closer look at the block of code that displays the images.
<div class="event-images columns is-multiline has-text-centered">
<div
v-for="image in event.images"
:key="image.id"
class="column is-one-third"
>
<img :src="`${image}`" :alt="`${event.name}`" />
</div>
</div>
This uses Vue's v-for
directive again to loop through the images. Once it grabs a single image, it binds the image URL, img
, to the :src
attribute. It's good practice to always set an alt
attribute, so you can bind the value for event.name
to the alt
attribute of these images.
Now head back to the homepage and click around. All of the components are rendering the correct data, so it's time to add authentication!
Adding Authentication to your Vue App
The final thing left to do is add login functionality to the application. You're going to use Auth0's authentication service to do this.
Once you're registered, you'll be taken to the Auth0 management dashboard.
- Click on "Applications" in the left sidebar
- Click on the big red button that says "Create Application"
- Name it "Vue Events" (or anything you'd like)
- Click on "Single Page Web Applications" for "application type"
- Click "Create"
🛠️️ Next, click into "Settings" to fill in some information that Auth0 needs to configure authentication for your application.
Allowed Callback URLs — http://localhost:8080
This is where Auth0 will redirect the user after they have authenticated.
Allowed Logout URLs — http://localhost:8080
This is the URL that users will return to after they log out of your application.
Allowed Web Origins — http://localhost:8080
This is the URL that Auth0 uses for Cross-origin Authentication.
Scroll down and click the "Save Changes" button.
That's all you need from the dashboard for now, but don't click out yet. You'll need to pull some of these values from the dashboard into your application soon.
Install Auth0 SPA package
🛠️️ Head back to the terminal and install Auth0's auth0-spa-js
package in the application's root directory.
npm install @auth0/auth0-spa-js
Create an authentication wrapper
Next, you're going to create a reusable wrapper Vue object around the Auth0 SDK. You'll also create a Vue plugin that exposes this wrapper to the rest of the application.
🛠️️ Create a new file and folder for this. Make sure you're still in the events-app
root directory and enter:
mkdir src/auth
touch src/auth/index.js
🛠️️ Now open up the newly created src/auth/index.js
file and paste in the following:
import Vue from 'vue';
import createAuth0Client from '@auth0/auth0-spa-js';
/** Define a default action to perform after authentication */
const DEFAULT_REDIRECT_CALLBACK = () =>
window.history.replaceState({}, document.title, window.location.pathname);
let instance;
/** Returns the current instance of the SDK */
export const getInstance = () => instance;
/** Creates an instance of the Auth0 SDK. If one has already been created, it returns that instance */
export const useAuth0 = ({
onRedirectCallback = DEFAULT_REDIRECT_CALLBACK,
redirectUri = window.location.origin,
...options
}) => {
if (instance) return instance;
// The 'instance' is simply a Vue object
instance = new Vue({
data() {
return {
loading: true,
isAuthenticated: false,
user: {},
auth0Client: null,
popupOpen: false,
error: null,
};
},
methods: {
/** Authenticates the user using a popup window */
async loginWithPopup(o) {
this.popupOpen = true;
try {
await this.auth0Client.loginWithPopup(o);
} catch (e) {
// eslint-disable-next-line
console.error(e);
} finally {
this.popupOpen = false;
}
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
},
/** Handles the callback when logging in using a redirect */
async handleRedirectCallback() {
this.loading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
} catch (e) {
this.error = e;
} finally {
this.loading = false;
}
},
/** Authenticates the user using the redirect method */
loginWithRedirect(o) {
return this.auth0Client.loginWithRedirect(o);
},
/** Returns all the claims present in the ID token */
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
},
/** Returns the access token. If the token is invalid or missing, a new one is retrieved */
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
/** Gets the access token using a popup window */
getTokenWithPopup(o) {
return this.auth0Client.getTokenWithPopup(o);
},
/** Logs the user out and removes their session on the authorization server */
logout(o) {
return this.auth0Client.logout(o);
},
},
/** Use this lifecycle method to instantiate the SDK client */
async created() {
// Create a new instance of the SDK client using members of the given options object
this.auth0Client = await createAuth0Client({
domain: options.domain,
client_id: options.clientId,
audience: options.audience,
redirect_uri: redirectUri,
});
try {
// If the user is returning to the app after authentication..
if (
window.location.search.includes('code=') &&
window.location.search.includes('state=')
) {
// handle the redirect and retrieve tokens
const { appState } = await this.auth0Client.handleRedirectCallback();
// Notify subscribers that the redirect callback has happened, passing the appState
// (useful for retrieving any pre-authentication state)
onRedirectCallback(appState);
}
} catch (e) {
this.error = e;
} finally {
// Initialize the internal authentication state
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.auth0Client.getUser();
this.loading = false;
}
},
});
return instance;
};
// Create a simple Vue plugin to expose the wrapper object throughout the application
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
},
};
The comments in this file cover the details of what's happening, but to summarize, you are first creating (or returning) an instance of the Auth0 SDK. The instance is just a Vue object.
- The instance contains the following data:
loading
,isAuthenticated
,user
,auth0Client
,popupOpen
, anderror
- It also includes several methods that will be called later, but take note of them now:
loginWithPopup
,handleRedirectCallback
,loginWithRedirect
,getIdTokenClaims
,getTokenSilently
,getTokenWithPopup
, andlogout
During the created()
lifecycle hook, you're creating an instance of the SDK.
When a user clicks "Log in", they're redirected to the Auth0 Universal Login page (more on this later). They will enter their credentials there and then be redirected back to the application. This is where the "Allowed Callback URLs" from the Auth0 dashboard come into play. handleRedirectCallback()
will run, which will get the authenticated user data, retrieve tokens, and update isAuthenticated
to true.
This instance also contains an options
object (pulled out and pasted below). This object will hold the values for the Auth0 clientId
, domain
, and audience
from the Auth0 dashboard.
// Create a new instance of the SDK client using members of the given options object
this.auth0Client = await createAuth0Client({
domain: options.domain,
client_id: options.clientId,
audience: options.audience,
redirect_uri: redirectUri,
});
These values aren't particularly sensitive, but it's still a good practice to leave them out of your source control (e.g. GitHub). So let's create a file that can be added to .gitignore
so that Git will ignore it.
🛠️️ Make sure you're still in the events-app
directory and run:
touch auth_config.json
If you're using Git or some other version control, open up .gitignore
or its equivalent. Paste in auth_config.json
on any line. Now this file will be ignored the next time you push to your repo.
🛠️️ Next, open up auth_config.json
and paste in the following:
{
"domain": "your-domain.auth0.com",
"clientId": "yourclientid"
}
Finding your auth_config
values:
- Head to the Auth0 dashboard
- Click on "Applications" and select your application
- Click on "Settings"
- Copy the value for "Domain" and paste it into
domain
inauth_config.json
- Copy the value for "Client ID" and paste it into
clientId
inauth_config.json
Next, open src/main.js
and install the plugin with Vue.use
. This plugin will allow you to access the authentication state globally (from anywhere in the application). Vue.use
is a global method used to call plugins, and it must be placed before new Vue()
.
🛠️️ Replace all of src/main.js
with the following:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import 'bulma/css/bulma.css';
// Import the Auth0 configuration
import { domain, clientId } from '../auth_config.json';
// Import the plugin here
import { Auth0Plugin } from './auth';
// Install the authentication plugin here
Vue.use(Auth0Plugin, {
domain,
clientId,
onRedirectCallback: (appState) => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname,
);
},
});
Vue.config.productionTip = false;
new Vue({
router,
render: (h) => h(App),
}).$mount('#app');
Here you're just importing the Auth0 configuration file that you created to get access to the domain
and clientId
values. Next, you're importing the Auth0Plugin
that was created earlier.
Finally, you install and configure the plugin.
Wiring up login and logout buttons
Now that you have the Auth0 authentication plugin configured, it's time to fix up the "Sign in" button so that it actually does something.
🛠️️ Open up src/components/partials/Nav.vue
. Find the block of code that starts with <div class="navbar-end">
and replace it with this:
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<!-- Check that the SDK client is not currently loading before accessing is methods -->
<div v-if="!$auth.loading">
<!-- show login when not authenticated -->
<a v-if="!$auth.isAuthenticated" @click="login" class="button is-dark"
><strong>Sign in</strong></a
>
<!-- show logout when authenticated -->
<a v-if="$auth.isAuthenticated" @click="logout" class="button is-dark"
><strong>Log out</strong></a
>
</div>
</div>
</div>
</div>
The buttons are now wrapped in !$auth.loading
to make sure that the SDK client has finished loading before you to try access the user's state. Next, you're using @click
, which will handle the click event by calling the login
or logout
methods when a user clicks on the respective button.
🛠️️ Let's create those methods now. In that same file, scroll down to the <script>
tag and replace it with this:
<script>
export default {
name: 'Nav',
methods: {
// Log the user in
login() {
this.$auth.loginWithRedirect();
},
// Log the user out
logout() {
this.$auth.logout({
returnTo: window.location.origin
});
}
}
}
</script>
Now head back to the application and click "Sign In" and you should be redirected to the Auth0 Universal Login page.
If you run into an issue, double-check that your values in
auth_config.json
are correct. If you're still having issues, leave a comment below, and I'll help you work through it.
Once you've hit the Auth0 Universal Login page, sign up with a mock user account. You'll then see a screen telling you that the application is requesting access to your profile and email.
Click the checkmark, and you'll be redirected back to the "Allowed Callback URL" you specified in the dashboard, which is your application homepage. Now instead of the "Sign in" button, you should see a "Log out" button.
Optional: Auth0 offers social login options straight from the dashboard! Google is activated by default, and you can turn more on individually in the Auth0 management dashboard.
Click on "Connections" > "Social" in the sidebar. Be sure to use your own dev keys if you'd like to integrate social sign-on. The default Auth0 dev keys are fine for testing, but may cause unexpected errors (such as being signed out on refresh), so we still recommend using your own.
Accessing user information
Auth0 lets you access the information of the logged-in user in your templates with the following:
{
{
$auth.user;
}
}
The contents of $auth.user
look something like this:
{
"nickname": "hollylloyd",
"name": "Holly Lloyd",
"picture": "https://gravatar.com/somefancyimage.png",
"updated_at": "2019-10-09T15:49:28.181Z",
"email": "holly-lloyd@example.com",
"email_verified": false,
"sub": "auth0|xxxxxxxxxxxxxxx"
}
So if you want to add a profile page in the future, you have access to this data (and more) to display.
Now that you know how to add a login button, let's wire up that second button on the homepage.
🛠️️ Open up src/views/Home.vue
and replace everything between <div class="button-block"></div>
with the following:
<div class="button-block">
<button v-if="!$auth.isAuthenticated" @click="login" class="button is-xl is-dark">Sign Up to Browse Events</button>
<h3 v-if="$auth.isAuthenticated" class="is-size-3 has-background-dark welcome">Welcome, {{ $auth.user.name }}!</h3>
</div>
🛠️️ Now you just need to add the methods section with the login()
function. Scroll down to where the <script>
tag is and replace export default {}
with this:
export default {
name: 'home',
components: {
EventsList,
},
methods: {
// Log the user in
login() {
this.$auth.loginWithRedirect();
},
},
};
Now a user can sign in with this button as well, and once they're signed in, it will be replaced with a welcome message with their name using $auth.user.name
.
Set up a Route Guard
So now, the final thing you need to do is redirect unauthenticated users away from the Single Event page. This means they should be able to view the homepage with the list of cards/events, but as soon as they click through to the event detail page, they should be kicked to a login page.
⚡️ IMPORTANT ⚡️ This only prevents the page from loading; it doesn't prevent the data from loading. You should NEVER rely on the frontend to protect your data.
Since you haven't created a backend API yet, you're just storing all of the data in the component on the frontend. Even though you're going to set up a route guard to redirect an unauthenticated user away from a single event page, they can still read through the JavaScript files and find it! I'll show you how in a moment.
{% if page.url contains '/amp/' %} In part 2, you'll build out the API so that this data is stored and protected on the backend instead and only pulled into the frontend if the user is authenticated. {% else %} In part 2, you'll build out the API so that this data is stored and protected on the backend instead and only pulled into the frontend if the user is authenticated. {% endif %}
Let's set up the route guard now so that when the data is coming from the backend, the UI for kicking the user to the login page is already in place.
🛠️️ Create a file called authGuard.js
in the src/auth
directory.
{% prism bash %} touch src/auth/authGuard.js {% endprism %}
🛠️️ Open that up in your editor and paste in the following:
import { getInstance } from './index';
export const authGuard = (to, from, next) => {
const authService = getInstance();
const fn = () => {
// If the user is authenticated, continue with the route
if (authService.isAuthenticated) {
return next();
}
// Otherwise, log in
authService.loginWithRedirect({ appState: { targetUrl: to.fullPath } });
};
// If loading has already finished, check the auth state using `fn()`
if (!authService.loading) {
return fn();
}
// Watch for the loading property to change before checking isAuthenticated
authService.$watch('loading', (loading) => {
if (loading === false) {
return fn();
}
});
};
This uses the getInstance
method from the src/auth/index.js
file, which will implement the function that prevents a route from being accessed if a user is not logged in.
If the user is authenticated, next()
is returned, which allows the user to continue to the clicked route. If the user isn't authenticated, they're redirected to the Auth0 Universal Login page.
Next, you need to add this auth guard to the router so that this runs before any view is returned.
You'll simply check if the user is authenticated. If they are, let them through, if not, send them to the login page.
🛠️️ Open up the router file in src/router/index.js
and add replace it with:
import Vue from 'vue';
import Router from 'vue-router';
import Home from '../views/Home.vue';
import { authGuard } from '../auth/authGuard';
Vue.use(Router);
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/about',
name: 'about',
component: () => import('../views/About.vue'),
},
{
path: '/event/:id',
name: 'eventSingle',
component: () => import('../views/EventSingle.vue'),
beforeEnter: authGuard,
},
],
});
The authGuard
is imported at the top. Since you only want to require authentication for the event details route, beforeEnter: authGuard
has been added to that route.
Now, if you've already logged in previously, you can click on one of those event cards and you should still be able to see the event single page.
But if you open an incognito window and try to access that same route, you'll be kicked to the login page, which is exactly what's expected!
As discussed before, this doesn't prevent a user from finding the data for that page.
If you're curious how this is possible, sign out or open up an incognito window, and then go to http://localhost:8080/js/app.js
in your browser. Now search for one of the event descriptions like "Spend an elegant night of dinner and dancing with us as we raise money for our new rescue farm.", and sure enough, there is the data!
So at this point, you may be asking, "Holly, then what is the point of the redirect?"
I'm glad you asked! This is mostly meant for user experience! Imagine that the data is protected on the backend. You're going to want to signal that a user should be signed in to view that specific page. If you simply just don't render any data on that page, the user will be faced with an empty page and think something is broken. In this scenario, redirecting them to the sign-in page will easily signal that they need to log in to view it!
In part 2 of this tutorial, you'll learn how to create an API in Express and pull the data into your Vue application from there. It uses the same Vue application that you just built, so if you're interested, you can hit the ground running!Wrap Up
If this was your first time working with Vue.js, hopefully this helped you understand how everything comes together in a small application. Just to recap, here are some of the topics that were covered in this tutorial:
- Using the Vue CLI
- Creating reusable components
- Setting up routing with Vue Router
- Styling with Bulma
- Using props and data
- Adding authentication to a Vue.js app
- Protecting routes with Vue navigation guards