A couple weeks ago I got this crazy idea to try out a new front end framework that isn’t React. Given the recent hype behind Vue, I figured that this would be a good opportunity to get dirty with it.
I usually start learning a new technology by going through half a dozen tutorials or video courses. Then, after I’ve digested enough good examples, I’ll start making my own projects by changing the names of variables and then slap my branding on them.
This time I would like to conduct an experiment, however.
I am going to learn to construct a user interface with Vue by consulting only the official Vue documentation. For styling purposes, as I tend to get discouraged by a drab webpage, I will use Vuetifyjs. Hopefully, I will be able gain a new perspective on the learning process by doing it this way.
“React things” that I need to figure out in Vue
Since I am well-versed in React, I expect to know some things about constructing a web app with components. That is, I know that what we can pass down data as props in React, can we also do so in Vue?
How do I communicate information from child components to their parents?
How exactly should I handle state? Assume that I don’t want to reach for a Redux-like library like Vuex.
How does routing working? Is vue-router much like react-router?
If I want to fetch some data when my component is mounted, how will I accomplish this without a
componentDidMount
lifecycle method?I am using a style framework called Vuetify, which comes with many components I will need for layouts. How do I define and use my own styles?
These are just a few questions I hope to answer while constructing this app.
App: Daily Riff
This web app will be a log that lets people post a video of them playing a riff or entire song along with a credit to the original artist, along with some links to the tab or original video. Well it wont be quite that advanced; for brevity's sake I'll skip the important audio/video bit and focus more on the Vue.
It sounds rather specific, but the inspiration comes from my current desire to practice playing guitar more frequently. I’ve always wanted to start a rock band; I had one for a short while in high school but we since gone our separate ways. Well, actually, they live down the street. We’re just too damn lazy to lug our equipment around and jam.
But once they see this app they’ll realize what they’re missing. If you want to see the app’s source code, check it out here on github. Let’s hit it.
Getting Started
I started by downloading the Vue CLI and using it to initialize a new project. This will run us through some prompts not so different from initializing an npm project.
vue init webpack daily-riff
Once that installs our starter boilerplate (I checked the vue-router) we can fire up the project with yarn dev or npm run dev and see it running at http://localhost:8080.
The boilerplate includes a bunch of potentially helpful links, but we’re gonna smite that and put in our own code. Let’s add Vuetify so that we can use it to build our layout.
yarn add vuetify # or npm install --save vuetify
Then inside src/main.js
update the code to produce the following.
// src/main.js
import Vue from 'vue'
import Vuetify from 'vuetify'
import App from './App'
import router from './router'
import 'vuetify/dist/vuetify.min.css' // Ensure you are using css-loader
Vue.use(Vuetify)
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
So far this is pretty similar to building a React app, except rather than using react-dom to render into our html template, we define an instance of this Vue class that allows you to set an element to render to and with which components. This is also where we set the router.
Vuetify works sort of like a middleware by calling Vue.use(Vuetify)
, if you’re familiar with middlewares in Redux or Express. By setting this in the root of our application, we will be able to use its built-in templates in our own components.
Building the App Layout
Before we get rid of all of the boilerplate code, lets add in our own top bar. Usually when I get started on a new project, I put in a lot of effort to make a decent top bar (or navbar or toolbar depending on where you come from). I then lose interest in the project because everything else is harder but hey its progress. Start by creating a Toolbar.vue
file in the components directory. Note the file ending, it is not .js nor is it .jsx.
<template>
<v-toolbar
color="red"
dense
fixed
clipped-left
app
>
<v-toolbar-title class="mr-5 align-center">
<span class="title white-text">Daily Riff</span>
</v-toolbar-title>
</v-toolbar>
</template>
<style>
.white-text {
color: #fff;
}
</style>
The white text is there so that the branding shows up more nicely over the red toolbar. The format of one of these files looks a little different from your standard JavaScript file. We have template tags where we can put our markup, style tags for our styling rules, and as we’ll see in the next file, we can use script tags to define our JavaScript. Let us set up the App component now, change src/App.vue
to the following.
// src/App.vue
<template>
<div id="app">
<v-app>
<toolbar />
<v-content>
<v-container fluid>
<router-view/>
</v-container>
</v-content>
</v-app>
</div>
</template>
<script>
import Toolbar from './components/Toolbar'
export default {
name: 'App',
components: {
Toolbar
}
}
</script>
<style>
#app {
font-family: 'Roboto', sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Remember when we used App as our root component inside of src/main.js
? This file’s template is the markup used inside of that root. That is to say, when we import Vue component files, we are getting their templates as well as the data exported. Here, App.vue uses the toolbar component so we must import the Toolbar and then define it in the components field of the object to be exported. This lets App.vue know how to react when it sees <Toolbar />
or <toolbar />
inside its template.
Something else that’s aesthetically different in Vue is that when we import and define a component, the component’s tag is case insensitive. The convention in Vue tends to lean toward “kebab-case” tag markup rather than “camelCase”.
Building The Home Page
Direct your attention to src/router/index.js
where we shall update the naming so that it better reflects the Home page component we will soon create.
// src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
}
]
})
I was actually pleasantly surprised to see this. I thought vue-router would have a more complicated browser routing scheme, maybe something more similar to react-router. In this file we import a Router class whose instance is exported for our use in the root at src/main.js
. All we have to do is define a path, component name, and the actual component that we want to render.
The actual home component will be fairly simple, it will basically be in charge of rendering the list of records that are stored by some data source. In this case, we’ll be using one that I spun up for just this occasion. More on that later.
Lets start by adding the code for rendering our list of records and then describe a little of what’s happening. Also, be sure to run
npm install --save axios
Heres the Home.vue component.
// src/components/Home.vue
<template>
<v-container>
<v-layout row wrap>
<v-flex v-for="(record, i) in records" :key="i" xs4>
<record :record="record" />
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import axios from 'axios'
import Record from '@/components/Record'
export default {
name: 'Home',
data: () => ({
records: [],
isLoading: false
}),
mounted() {
this.getRecords()
},
methods: {
getRecords() {
this.isLoading = true
axios
.get('https://secret-headland-43248.herokuapp.com/records')
.then(({ data }) => {
this.records = data
this.isLoading = false
})
.catch(err => {
this.isLoading = false
console.error(err)
})
}
},
components: {
Record
}
}
</script>
Home Template: Rendering lists and passing props
The markup here is rather minimal, it essentially describes how to create a “3 by X” layout using Vuetify. The pattern to remember goes something like
v-container -> v-layout -> v-flex (iterate over these!)
If you’re coming from bootstrap, this hierarchy of classes will make more sense, your page needs a container class, a row, and columns for the row. Vuetify works like a combination of flexbox and bootstrap (since we can add props like xs4 or offset-xs4). That’s something else to consider, you could always roll a different style solution.
The interesting part is the v-for attribute we give to the v-flex tag. I think it feels a little strange to put JavaScript in markup attributes; I still prefer the React style of rendering lists. In the template attributes, we have direct access to the some of the values we exported in the script, such as the fields returned in the data or methods functions.
In particular, we use the records array exported from data in order to render our list of records. At this point, any fields enclosed by the iterating tag with the v-for attribute can access the current item or index in the iteration, which in this case is the record object.
Notice we also called the record component, “record”. Fortunately, the Vue template is able to distinguish between data properties and the other Vue components in its markup.
One other “gotcha” in this line is the way we pass props down to child components. to pass a prop we can write an attribute like
<record v-bind:record="record">
// equivalent to
<record :record="record">
The :<attr>
is one of the shorthand notations we can use to make our components less verbose. Notice also, that attributes or props in quotations are not necessarily strings. It is easier to think of the characters in quotes to be executed as JavaScript. The React equivalent in JSX would look more like this:
<Record record={this.state.record} />
Home Script: Defining lifecycle hooks, methods, and data
Looking past the template markup, we can immediately notice some similarities to React’s lifecycle methods. When a view component is rendered onto the DOM, its lifespan can be described by the terms created, mounted, updated, and destroyed. These terms are, by no coincidence, some of the functions we can export in our Home.vue
file.
In this particular implementation, I only care about when the component is mounted, since that is where I want to make the API request to fetch my record data.
Taking a look at the data and methods fields exported from this file, these are how we define “state” and “class properties” respectively for this component. Unlike with React, we can update state in a Vue component by merely assigning its property a value, i.e.
this.records = data // instead of this.setState({ records: data })
An more explicit example get be found in the getRecords
method we have implemented, which makes a call to an API I threw together for just this occasion. Methods defined within our exported methods field can be accessed anywhere in our Vue component’s life-cycle hooks.
The only field we have not discussed yet is the name field. It is a little miscellaneous, but by defining it we could recursively render it within our template if we want. The name field also helps out in debugging in case you’re using Vue’s devtools.
Record Component
Woo okay we finally have the home page rendered and explained.
Now that we have the component that manages the state, a smart component, if you will, lets create the “dumb” child component that it renders.
// src/components/Record.vue
<template>
<v-card width="350px">
<v-card-media :src="record.imageurl" height="200px" class="card-media">
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 >
<span class="headline">{{record.title}}</span><br/>
<span>{{record.artist}}</span>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-card-title primary-title>
<div>
<div>{{record.description}}</div>
</div>
</v-card-title>
<v-card-actions>
<v-btn flat color="orange" :href="record.taburl">Tab</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: {
record: {
title: String,
artist: String,
description: String,
taburl: String,
imageurl: String
}
}
}
</script>
<style>
.card-media {
text-align: left;
color: #fff;
}
</style>
There is a little more markup in this file, but less logic. I am making liberal use of Vuetify Cards in the markup. The only other intriguing bit in the template is how we access the record prop. When its being used in attribute quotations, you will see that we can access properties just like any other JavaScript object.
Similarly, we can do the same within the actual tags by using the double curly-brace notation, i.e.
<div>{{record.description}}</div>
In Vue, we are somewhat forced to define what we call prop-types in React. In order for a component to act on the props it receives, it must declare which props it expects. In our case, I even defined the types expected by each field in the record prop. I could have also defined it without those types by merely specifying record in an array:
export default { props: ['record'] }
In that case, as long as the record component receives a prop called “record”, there would be no errors.
In this file we also see that we are free to define styles within our .vue files. A neat part about Vue styling is you can even give the attribute “scoped” to the styles tag so that those styles only affect that file’s components.
At this point, if you’ve been following along, you might be able to fire up the server with yarn dev
or npm run dev
and check out the application we currently have.
Not bad so far? Hopefully that worked!
Now let’s talk about adding content.
Uploading Content
Now, I’ll be honest, I intended to have a more exciting web form, one that would allow you or I to record a sound snippet or video and directly upload it to Soundcloud or Youtube. Well, I suppose that is still possible, but it is out of the scope of our little Vue tutorial. I cannot keep you here all day, after all.
Nevertheless, let us press on, add this code to a new file called Upload.vue
// src/components/Upload.vue
<template>
<v-layout>
<v-flex sm8 offset-sm2>
<h3 class="headline pb-4">Upload a Killer Riff!</h3>
<v-form v-model="valid" ref="form" lazy-validation>
<v-text-field
label="Song Title"
v-model="title"
:rules="titleRules"
placeholder="Add song title"
required
></v-text-field>
<v-text-field
label="Artist"
v-model="artist"
:rules="artistRules"
placeholder="Add artist"
required
></v-text-field>
<v-text-field
label="Description"
v-model="description"
:rules="descriptionRules"
placeholder="Add description"
multi-line
></v-text-field>
<v-text-field
label="Image url"
v-model="imageurl"
:rules="imageurlRules"
placeholder="Add url of image"
></v-text-field>
<v-text-field
label="Tab url"
v-model="taburl"
:rules="taburlRules"
placeholder="Add url of tab"
></v-text-field>
<v-btn
@click="submit"
:disabled="!valid"
>
submit
</v-btn>
<v-btn @click="clear">clear</v-btn>
</v-form>
</v-flex>
</v-layout>
</template>
Its quite a lot of text, I know. This is really just a whole bunch of fields for a form, but there are a couple of interesting bits to take away from it. One of those is the v-model attribute. This attribute is some syntactic sugar for two-way data binding between component state and user input.
In React, we’d usually give our input component an onChange
prop and use it to update state. It’s a little simpler here.
If we want stricter validation logic, say for email address validation, we can define a set of rules for that particular field and pass them to the input. More on that in a little bit.
There is also the @click prop, which is shorthand for v-on:click
and allows us to define a method for handling user input events. We have two buttons with these click properties; one button is passed the submit method and the other the clear method.
Now here is the rest of the code:
// src/components/Upload.vue
// <template> ... </template>
<script>
import axios from 'axios'
export default {
data: () => ({
valid: true,
title: '',
titleRules: [
v => !!v || 'Title is required',
v => (v && v.length <= 140) || 'Title must be less than 140 characters'
],
artist: '',
artistRules: [
v => !!v || 'Artist is required',
v => (v && v.length <= 140) || 'Artist must be less than 140 characters'
],
description: '',
descriptionRules: [
v => !!v || 'Description is required',
v => (v && v.length <= 300) || 'Title must be less than 300 characters'
],
taburl: '',
taburlRules: [v => !!v || 'taburl is required'],
imageurl: '',
imageurlRules: [v => !!v || 'imageurl is required']
}),
methods: {
submit() {
if (this.$refs.form.validate()) {
axios
.post('https://secret-headland-43248.herokuapp.com/records',
{
title: this.title,
artist: this.artist,
description: this.description,
taburl: this.taburl,
imageurl: this.imageurl
},
{
headers: {
'content-type': 'application/json'
}
})
.then(res => {
if (res.status === 200) {
console.log('good!')
}
})
.catch(err => {
console.log('bad!')
console.error(err)
})
}
},
clear() {
this.$refs.form.reset()
}
}
}
</script>
The data field is rather straightforward in this case; there are fields that are bound to the input fields and rules for each of the fields. These are defined as an array of validation functions, taking the input value and returning a boolean that describes whether the input is valid. The validity of the overall form is also described here.
Under methods there are two, one that submits the form, launching an axios POST request to our backend, and one that clears the values in the form.
There are a number of instance properties available to Vue components, such as this.$refs
as seen in this form. I think these instance properties are mostly used under the hood to conduct event handling and life-cycles, but we seem to also have access to them.
Lets now hook it up by setting a new entry in our router:
// src/router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Upload from '@/components/Upload'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/upload',
name: 'Upload',
component: Upload
}
]
})
And finally, add a FAB to the Home.vue file so we can get to our form from the home page.
// src/components/Home.vue
<template>
<v-container>
<v-layout row wrap>
<v-flex v-for="(record, i) in records" :key="i" xs4>
<record :record="record" />
</v-flex>
</v-layout>
<router-link to="/upload">
<v-btn fixed dark fab bottom right color="pink">
<v-icon>add</v-icon>
</v-btn>
</router-link>
</v-container>
</template>
// other Home.vue code
You’ll see that I just added the v-btn wrapped in a router-link here, no complicated routing here. Just a couple buttons. If all went well you should be able to fire it up!
https://thepracticaldev.s3.amazonaws.com/i/8b8sckeaz8oxr7m9dqq7.png
That about wraps it up. Again, this app arose from my desire to practice shredding on my guitar more consistently. Thankfully, I can say that I actually have gotten more consistent at that — despite the fact it took over a week to roll this post out!
Ideally, the form would contain an audio or video recording feature. This wouldn’t be too difficult, but for the scope of this particular blog post, I think it would be wise to save that for a distant sequel.
If you're looking for a good getting started guide with Vue, check out this post by Víctor Adrían.
See you next time.
Curious for more posts or witty remarks? Follow me on Medium, Github and Twitter!
Top comments (0)